diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6288e4d367c60..be34bfd02d315 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,7 +5,10 @@ // installs nodejs into container "ghcr.io/devcontainers/features/node:1": { "version":"20" - } + }, + "ghcr.io/devcontainers/features/git-lfs:1.1.0": {}, + "ghcr.io/devcontainers-contrib/features/poetry:2": {}, + "ghcr.io/devcontainers/features/python:1": {} }, "customizations": { "vscode": { @@ -20,7 +23,7 @@ "Vue.volar", "ms-azuretools.vscode-docker", "zixuanchen.vitest-explorer", - "alexcvzz.vscode-sqlite" + "qwtel.sqlite-viewer" ] } }, diff --git a/.drone.yml b/.drone.yml index d54f3c198d204..5d4e13038ba34 100644 --- a/.drone.yml +++ b/.drone.yml @@ -244,133 +244,6 @@ steps: exclude: - 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 type: docker @@ -506,136 +379,6 @@ steps: exclude: - 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 type: docker @@ -681,50 +424,3 @@ depends_on: - docker-linux-amd64-release-candidate-version - docker-linux-arm64-release-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 diff --git a/.gitattributes b/.gitattributes index 6d4698d6a8c08..7e7a139c9a49c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,5 +5,6 @@ /templates/swagger/v1_json.tmpl linguist-generated /vendor/** -text -eol linguist-vendored /web_src/fomantic/build/** linguist-generated +/web_src/fomantic/_site/globals/site.variables linguist-language=Less /web_src/js/vendor/** -text -eol linguist-vendored Dockerfile.* linguist-language=Dockerfile diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index f0c34df86d361..398fb6eae392c 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -4,24 +4,20 @@ on: workflow_call: outputs: backend: - description: "whether backend files changed" value: ${{ jobs.detect.outputs.backend }} frontend: - description: "whether frontend files changed" value: ${{ jobs.detect.outputs.frontend }} docs: - description: "whether docs files changed" value: ${{ jobs.detect.outputs.docs }} actions: - description: "whether actions files changed" value: ${{ jobs.detect.outputs.actions }} templates: - description: "whether templates files changed" value: ${{ jobs.detect.outputs.templates }} + docker: + value: ${{ jobs.detect.outputs.docker }} jobs: detect: - name: detect which files changed runs-on: ubuntu-latest timeout-minutes: 3 # Map a step output to a job output @@ -31,6 +27,7 @@ jobs: docs: ${{ steps.changes.outputs.docs }} actions: ${{ steps.changes.outputs.actions }} templates: ${{ steps.changes.outputs.templates }} + docker: ${{ steps.changes.outputs.docker }} steps: - uses: actions/checkout@v3 - uses: dorny/paths-filter@v2 @@ -58,3 +55,8 @@ jobs: templates: - "templates/**/*.tmpl" + - "poetry.lock" + docker: + - "Dockerfile" + - "Dockerfile.rootless" + - "docker/**" diff --git a/.github/workflows/pull-docker-dryrun.yml b/.github/workflows/pull-docker-dryrun.yml index 916de6b27d35c..61f1fd5632e11 100644 --- a/.github/workflows/pull-docker-dryrun.yml +++ b/.github/workflows/pull-docker-dryrun.yml @@ -11,8 +11,8 @@ jobs: files-changed: uses: ./.github/workflows/files-changed.yml - docker-dryrun: - if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true' + regular: + if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest steps: @@ -21,3 +21,15 @@ jobs: with: push: false tags: gitea/gitea:linux-amd64 + + rootless: + if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.actions == 'true' + needs: files-changed + runs-on: ubuntu-latest + steps: + - uses: docker/setup-buildx-action@v2 + - uses: docker/build-push-action@v4 + with: + push: false + file: Dockerfile.rootless + tags: gitea/gitea:linux-amd64 diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index 4281c2ca09c21..0e94f5217cb94 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -55,5 +55,38 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: docker/setup-buildx-action@v1 - # build for linux/amd64, and linux/arm64 (possibly include linux/arm/v7 later. not included now because it adds significant amount to the build time) + # fetch all commits instead of only the last as some branches are long lived and could have many between versions + # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 + - run: git fetch --unshallow --quiet --tags --force + - uses: docker/setup-qemu-action@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: + context: . + 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: + context: . + platforms: linux/amd64,linux/arm64 + push: true + file: Dockerfile.rootless + tags: gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless diff --git a/.gitignore b/.gitignore index 581417df61024..6851be742c641 100644 --- a/.gitignore +++ b/.gitignore @@ -53,8 +53,6 @@ cpu.out /bin /dist /custom/* -!/custom/conf -/custom/conf/* !/custom/conf/app.example.ini /data /indexers diff --git a/.gitpod.yml b/.gitpod.yml index f1b2fb195712b..000f534e8539d 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -34,7 +34,7 @@ vscode: - Vue.volar - ms-azuretools.vscode-docker - zixuanchen.vitest-explorer - - alexcvzz.vscode-sqlite + - qwtel.sqlite-viewer ports: - name: Gitea diff --git a/.stylelintrc.yaml b/.stylelintrc.yaml index 4e2315ac95835..8aeb706182513 100644 --- a/.stylelintrc.yaml +++ b/.stylelintrc.yaml @@ -1,5 +1,7 @@ plugins: - stylelint-declaration-strict-value + - stylelint-declaration-block-no-ignored-properties + - stylelint-stylistic ignoreFiles: - "**/*.go" @@ -80,6 +82,7 @@ rules: media-feature-name-no-vendor-prefix: true media-feature-name-unit-allowed-list: null media-feature-name-value-allowed-list: null + media-feature-name-value-no-unknown: true media-feature-range-notation: null named-grid-areas-no-invalid: true no-descending-specificity: null @@ -92,6 +95,7 @@ rules: no-unknown-animations: null no-unknown-custom-properties: null number-max-precision: null + plugin/declaration-block-no-ignored-properties: true property-allowed-list: null property-disallowed-list: null property-no-unknown: true @@ -132,6 +136,82 @@ rules: selector-type-no-unknown: [true, {ignore: [custom-elements]}] shorthand-property-no-redundant-values: 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 unit-allowed-list: null unit-disallowed-list: null diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 66ee37bfa2776..3710aa5fe2f16 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -557,7 +557,7 @@ be reviewed by two maintainers and must pass the automatic tests. - And then push the tag as `git push origin v$vmaj.$vmin.$`. Drone CI will automatically create a release and upload all the compiled binary. (But currently it doesn't add the release notes automatically. Maybe we should fix that.) - If needed send a frontport PR for the changelog to branch `main` and update the version in `docs/config.yaml` to refer to the new version. - Send PR to [blog repository](https://gitea.com/gitea/blog) announcing the release. -- Verify all release assets were correctly published through CI on dl.gitea.io and GitHub releases. Once ACKed: - - bump the version of https://dl.gitea.io/gitea/version.json +- Verify all release assets were correctly published through CI on dl.gitea.com and GitHub releases. Once ACKed: + - bump the version of https://dl.gitea.com/gitea/version.json - merge the blog post PR - announce the release in discord `#announcements` diff --git a/Makefile b/Makefile index 6cb66a7f80428..08d439f422a72 100644 --- a/Makefile +++ b/Makefile @@ -68,7 +68,7 @@ endif EXTRA_GOFLAGS ?= -MAKE_VERSION := $(shell "$(MAKE)" -v | head -n 1) +MAKE_VERSION := $(shell "$(MAKE)" -v | cat | head -n 1) MAKE_EVIDENCE_DIR := .make_evidence ifeq ($(RACE_ENABLED),true) @@ -79,12 +79,15 @@ endif STORED_VERSION_FILE := VERSION HUGO_VERSION ?= 0.111.3 -ifneq ($(DRONE_TAG),) - VERSION ?= $(subst v,,$(DRONE_TAG)) - GITEA_VERSION ?= $(VERSION) +GITHUB_REF_TYPE ?= branch +GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) + +ifneq ($(GITHUB_REF_TYPE),branch) + VERSION ?= $(subst v,,$(GITHUB_REF_NAME)) + GITEA_VERSION ?= $(GITHUB_REF_NAME) else - ifneq ($(DRONE_BRANCH),) - VERSION ?= $(subst release/v,,$(DRONE_BRANCH)) + ifneq ($(GITHUB_REF_NAME),) + VERSION ?= $(subst release/v,,$(GITHUB_REF_NAME)) else VERSION ?= main endif @@ -1011,9 +1014,5 @@ docker: docker build --disable-content-trust=false -t $(DOCKER_REF) . # support also build args docker build --build-arg GITEA_VERSION=v1.2.3 --build-arg TAGS="bindata sqlite sqlite_unlock_notify" . -.PHONY: docker-build -docker-build: - docker run -ti --rm -v "$(CURDIR):/srv/app/src/code.gitea.io/gitea" -w /srv/app/src/code.gitea.io/gitea -e TAGS="bindata $(TAGS)" LDFLAGS="$(LDFLAGS)" CGO_EXTRA_CFLAGS="$(CGO_EXTRA_CFLAGS)" webhippie/golang:edge make clean build - # This endif closes the if at the top of the file endif diff --git a/README.md b/README.md index 0ee1772286203..41793d3d92957 100644 --- a/README.md +++ b/README.md @@ -173,8 +173,8 @@ for the full license text. Looking for an overview of the interface? Check it out! -|![Dashboard](https://dl.gitea.io/screenshots/home_timeline.png)|![User Profile](https://dl.gitea.io/screenshots/user_profile.png)|![Global Issues](https://dl.gitea.io/screenshots/global_issues.png)| +|![Dashboard](https://dl.gitea.com/screenshots/home_timeline.png)|![User Profile](https://dl.gitea.com/screenshots/user_profile.png)|![Global Issues](https://dl.gitea.com/screenshots/global_issues.png)| |:---:|:---:|:---:| -|![Branches](https://dl.gitea.io/screenshots/branches.png)|![Web Editor](https://dl.gitea.io/screenshots/web_editor.png)|![Activity](https://dl.gitea.io/screenshots/activity.png)| -|![New Migration](https://dl.gitea.io/screenshots/migration.png)|![Migrating](https://dl.gitea.io/screenshots/migration.gif)|![Pull Request View](https://image.ibb.co/e02dSb/6.png) -![Pull Request Dark](https://dl.gitea.io/screenshots/pull_requests_dark.png)|![Diff Review Dark](https://dl.gitea.io/screenshots/review_dark.png)|![Diff Dark](https://dl.gitea.io/screenshots/diff_dark.png)| +|![Branches](https://dl.gitea.com/screenshots/branches.png)|![Web Editor](https://dl.gitea.com/screenshots/web_editor.png)|![Activity](https://dl.gitea.com/screenshots/activity.png)| +|![New Migration](https://dl.gitea.com/screenshots/migration.png)|![Migrating](https://dl.gitea.com/screenshots/migration.gif)|![Pull Request View](https://image.ibb.co/e02dSb/6.png) +![Pull Request Dark](https://dl.gitea.com/screenshots/pull_requests_dark.png)|![Diff Review Dark](https://dl.gitea.com/screenshots/review_dark.png)|![Diff Dark](https://dl.gitea.com/screenshots/diff_dark.png)| diff --git a/README_ZH.md b/README_ZH.md index 285a814037988..48eee9214d2fb 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -91,8 +91,8 @@ Fork -> Patch -> Push -> Pull Request ## 截图 -|![Dashboard](https://dl.gitea.io/screenshots/home_timeline.png)|![User Profile](https://dl.gitea.io/screenshots/user_profile.png)|![Global Issues](https://dl.gitea.io/screenshots/global_issues.png)| +|![Dashboard](https://dl.gitea.com/screenshots/home_timeline.png)|![User Profile](https://dl.gitea.com/screenshots/user_profile.png)|![Global Issues](https://dl.gitea.com/screenshots/global_issues.png)| |:---:|:---:|:---:| -|![Branches](https://dl.gitea.io/screenshots/branches.png)|![Web Editor](https://dl.gitea.io/screenshots/web_editor.png)|![Activity](https://dl.gitea.io/screenshots/activity.png)| -|![New Migration](https://dl.gitea.io/screenshots/migration.png)|![Migrating](https://dl.gitea.io/screenshots/migration.gif)|![Pull Request View](https://image.ibb.co/e02dSb/6.png) -![Pull Request Dark](https://dl.gitea.io/screenshots/pull_requests_dark.png)|![Diff Review Dark](https://dl.gitea.io/screenshots/review_dark.png)|![Diff Dark](https://dl.gitea.io/screenshots/diff_dark.png)| +|![Branches](https://dl.gitea.com/screenshots/branches.png)|![Web Editor](https://dl.gitea.com/screenshots/web_editor.png)|![Activity](https://dl.gitea.com/screenshots/activity.png)| +|![New Migration](https://dl.gitea.com/screenshots/migration.png)|![Migrating](https://dl.gitea.com/screenshots/migration.gif)|![Pull Request View](https://image.ibb.co/e02dSb/6.png) +![Pull Request Dark](https://dl.gitea.com/screenshots/pull_requests_dark.png)|![Diff Review Dark](https://dl.gitea.com/screenshots/review_dark.png)|![Diff Dark](https://dl.gitea.com/screenshots/diff_dark.png)| diff --git a/cmd/actions.go b/cmd/actions.go index 346de5b21a6fc..f52a91bd55149 100644 --- a/cmd/actions.go +++ b/cmd/actions.go @@ -42,7 +42,7 @@ func runGenerateActionsRunnerToken(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setting.Init(&setting.Options{}) + setting.MustInstalled() scope := c.String("scope") diff --git a/cmd/cmd.go b/cmd/cmd.go index b148007fbe3be..8076acecaa257 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -58,7 +58,7 @@ func confirm() (bool, error) { } func initDB(ctx context.Context) error { - setting.Init(&setting.Options{}) + setting.MustInstalled() setting.LoadDBSetting() setting.InitSQLLoggersForCli(log.INFO) diff --git a/cmd/doctor.go b/cmd/doctor.go index b596e9ac0cb7a..b79436fc0a9fa 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -91,7 +91,7 @@ func runRecreateTable(ctx *cli.Context) error { golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info)) debug := ctx.Bool("debug") - setting.Init(&setting.Options{}) + setting.MustInstalled() setting.LoadDBSetting() if debug { diff --git a/cmd/dump.go b/cmd/dump.go index 7dda7fd2b32de..0b7c1d32c5b0a 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -182,7 +182,7 @@ func runDump(ctx *cli.Context) error { } fileName += "." + outType } - setting.Init(&setting.Options{}) + setting.MustInstalled() // make sure we are logging to the console no matter what the configuration tells us do to // FIXME: don't use CfgProvider directly diff --git a/cmd/embedded.go b/cmd/embedded.go index e51f8477b445c..204a623cf7040 100644 --- a/cmd/embedded.go +++ b/cmd/embedded.go @@ -99,11 +99,6 @@ type assetFile struct { func initEmbeddedExtractor(c *cli.Context) error { setupConsoleLogger(log.ERROR, log.CanColorStderr, os.Stderr) - // Read configuration file - setting.Init(&setting.Options{ - AllowEmpty: true, - }) - patterns, err := compileCollectPatterns(c.Args()) if err != nil { return err diff --git a/cmd/mailer.go b/cmd/mailer.go index 74bae1ab68c76..eaa5a1afe1c89 100644 --- a/cmd/mailer.go +++ b/cmd/mailer.go @@ -16,7 +16,7 @@ func runSendMail(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setting.Init(&setting.Options{}) + setting.MustInstalled() if err := argsSet(c, "title"); err != nil { return err diff --git a/cmd/restore_repo.go b/cmd/restore_repo.go index 5a7ede4939756..c19e28f13df7f 100644 --- a/cmd/restore_repo.go +++ b/cmd/restore_repo.go @@ -51,7 +51,7 @@ func runRestoreRepository(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setting.Init(&setting.Options{}) + setting.MustInstalled() var units []string if s := c.String("units"); s != "" { units = strings.Split(s, ",") diff --git a/cmd/serv.go b/cmd/serv.go index 87bf1cce20e4e..01102d3800c00 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -61,7 +61,7 @@ func setup(ctx context.Context, debug bool) { } else { setupConsoleLogger(log.FATAL, false, os.Stderr) } - setting.Init(&setting.Options{}) + setting.MustInstalled() if debug { setting.RunMode = "dev" } diff --git a/cmd/web.go b/cmd/web.go index da6c987ff845e..7a257a62a277d 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -101,6 +101,110 @@ func createPIDFile(pidPath string) { } } +func serveInstall(ctx *cli.Context) error { + log.Info("Gitea version: %s%s", setting.AppVer, setting.AppBuiltWith) + log.Info("App path: %s", setting.AppPath) + log.Info("Work path: %s", setting.AppWorkPath) + log.Info("Custom path: %s", setting.CustomPath) + log.Info("Config file: %s", setting.CustomConf) + log.Info("Prepare to run install page") + + routers.InitWebInstallPage(graceful.GetManager().HammerContext()) + + // Flag for port number in case first time run conflict + if ctx.IsSet("port") { + if err := setPort(ctx.String("port")); err != nil { + return err + } + } + if ctx.IsSet("install-port") { + if err := setPort(ctx.String("install-port")); err != nil { + return err + } + } + c := install.Routes() + err := listen(c, false) + if err != nil { + log.Critical("Unable to open listener for installer. Is Gitea already running?") + graceful.GetManager().DoGracefulShutdown() + } + select { + case <-graceful.GetManager().IsShutdown(): + <-graceful.GetManager().Done() + log.Info("PID: %d Gitea Web Finished", os.Getpid()) + log.GetManager().Close() + return err + default: + } + return nil +} + +func serveInstalled(ctx *cli.Context) error { + setting.InitCfgProvider(setting.CustomConf) + setting.LoadCommonSettings() + setting.MustInstalled() + + log.Info("Gitea version: %s%s", setting.AppVer, setting.AppBuiltWith) + log.Info("App path: %s", setting.AppPath) + log.Info("Work path: %s", setting.AppWorkPath) + log.Info("Custom path: %s", setting.CustomPath) + log.Info("Config file: %s", setting.CustomConf) + log.Info("Run mode: %s", setting.RunMode) + log.Info("Prepare to run web server") + + if setting.AppWorkPathMismatch { + log.Error("WORK_PATH from config %q doesn't match other paths from environment variables or command arguments. "+ + "Only WORK_PATH in config should be set and used. Please remove the other outdated work paths from environment variables and command arguments", setting.CustomConf) + } + + rootCfg := setting.CfgProvider + if rootCfg.Section("").Key("WORK_PATH").String() == "" { + saveCfg, err := rootCfg.PrepareSaving() + if err != nil { + log.Error("Unable to prepare saving WORK_PATH=%s to config %q: %v\nYou must set it manually, otherwise there might be bugs when accessing the git repositories.", setting.AppWorkPath, setting.CustomConf, err) + } else { + rootCfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath) + saveCfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath) + if err = saveCfg.Save(); err != nil { + log.Error("Unable to update WORK_PATH=%s to config %q: %v\nYou must set it manually, otherwise there might be bugs when accessing the git repositories.", setting.AppWorkPath, setting.CustomConf, err) + } + } + } + + routers.InitWebInstalled(graceful.GetManager().HammerContext()) + + // We check that AppDataPath exists here (it should have been created during installation) + // We can't check it in `InitWebInstalled`, because some integration tests + // use cmd -> InitWebInstalled, but the AppDataPath doesn't exist during those tests. + if _, err := os.Stat(setting.AppDataPath); err != nil { + log.Fatal("Can not find APP_DATA_PATH %q", setting.AppDataPath) + } + + // Override the provided port number within the configuration + if ctx.IsSet("port") { + if err := setPort(ctx.String("port")); err != nil { + return err + } + } + + // Set up Chi routes + c := routers.NormalRoutes() + err := listen(c, true) + <-graceful.GetManager().Done() + log.Info("PID: %d Gitea Web Finished", os.Getpid()) + log.GetManager().Close() + return err +} + +func servePprof() { + http.DefaultServeMux.Handle("/debug/fgprof", fgprof.Handler()) + _, _, finished := process.GetManager().AddTypedContext(context.Background(), "Web: PProf Server", process.SystemProcessType, true) + // The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment it's not worth to introduce a configurable option for it. + log.Info("Starting pprof server on localhost:6060") + log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", nil)) + finished() +} + func runWeb(ctx *cli.Context) error { if ctx.Bool("verbose") { setupConsoleLogger(log.TRACE, log.CanColorStdout, os.Stdout) @@ -128,75 +232,19 @@ func runWeb(ctx *cli.Context) error { createPIDFile(ctx.String("pid")) } - // Perform pre-initialization - needsInstall := install.PreloadSettings(graceful.GetManager().HammerContext()) - if needsInstall { - // Flag for port number in case first time run conflict - if ctx.IsSet("port") { - if err := setPort(ctx.String("port")); err != nil { - return err - } - } - if ctx.IsSet("install-port") { - if err := setPort(ctx.String("install-port")); err != nil { - return err - } - } - c := install.Routes() - err := listen(c, false) - if err != nil { - log.Critical("Unable to open listener for installer. Is Gitea already running?") - graceful.GetManager().DoGracefulShutdown() - } - select { - case <-graceful.GetManager().IsShutdown(): - <-graceful.GetManager().Done() - log.Info("PID: %d Gitea Web Finished", os.Getpid()) - log.GetManager().Close() + if !setting.InstallLock { + if err := serveInstall(ctx); err != nil { return err - default: } } else { NoInstallListener() } if setting.EnablePprof { - go func() { - http.DefaultServeMux.Handle("/debug/fgprof", fgprof.Handler()) - _, _, finished := process.GetManager().AddTypedContext(context.Background(), "Web: PProf Server", process.SystemProcessType, true) - // The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment it's not worth to introduce a configurable option for it. - log.Info("Starting pprof server on localhost:6060") - log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", nil)) - finished() - }() + go servePprof() } - log.Info("Global init") - // Perform global initialization - setting.Init(&setting.Options{}) - routers.GlobalInitInstalled(graceful.GetManager().HammerContext()) - - // We check that AppDataPath exists here (it should have been created during installation) - // We can't check it in `GlobalInitInstalled`, because some integration tests - // use cmd -> GlobalInitInstalled, but the AppDataPath doesn't exist during those tests. - if _, err := os.Stat(setting.AppDataPath); err != nil { - log.Fatal("Can not find APP_DATA_PATH '%s'", setting.AppDataPath) - } - - // Override the provided port number within the configuration - if ctx.IsSet("port") { - if err := setPort(ctx.String("port")); err != nil { - return err - } - } - - // Set up Chi routes - c := routers.NormalRoutes(graceful.GetManager().HammerContext()) - err := listen(c, true) - <-graceful.GetManager().Done() - log.Info("PID: %d Gitea Web Finished", os.Getpid()) - log.GetManager().Close() - return err + return serveInstalled(ctx) } func setPort(port string) error { @@ -217,9 +265,15 @@ func setPort(port string) error { defaultLocalURL += ":" + setting.HTTPPort + "/" // Save LOCAL_ROOT_URL if port changed - setting.CfgProvider.Section("server").Key("LOCAL_ROOT_URL").SetValue(defaultLocalURL) - if err := setting.CfgProvider.Save(); err != nil { - return fmt.Errorf("Failed to save config file: %v", err) + rootCfg := setting.CfgProvider + saveCfg, err := rootCfg.PrepareSaving() + if err != nil { + return fmt.Errorf("failed to save config file: %v", err) + } + rootCfg.Section("server").Key("LOCAL_ROOT_URL").SetValue(defaultLocalURL) + saveCfg.Section("server").Key("LOCAL_ROOT_URL").SetValue(defaultLocalURL) + if err = saveCfg.Save(); err != nil { + return fmt.Errorf("failed to save config file: %v", err) } } return nil diff --git a/contrib/environment-to-ini/environment-to-ini.go b/contrib/environment-to-ini/environment-to-ini.go index 3405d7d429b49..2cdf4e3943b4f 100644 --- a/contrib/environment-to-ini/environment-to-ini.go +++ b/contrib/environment-to-ini/environment-to-ini.go @@ -81,8 +81,6 @@ func main() { }, } app.Action = runEnvironmentToIni - setting.SetCustomPathAndConf("", "", "") - err := app.Run(os.Args) if err != nil { log.Fatal("Failed to run app with %s: %v", os.Args, err) @@ -90,12 +88,13 @@ func main() { } func runEnvironmentToIni(c *cli.Context) error { - providedCustom := c.String("custom-path") - providedConf := c.String("config") - providedWorkPath := c.String("work-path") - setting.SetCustomPathAndConf(providedCustom, providedConf, providedWorkPath) + setting.InitWorkPathAndCommonConfig(os.Getenv, setting.ArgWorkPathAndCustomConf{ + WorkPath: c.String("work-path"), + CustomPath: c.String("custom-path"), + CustomConf: c.String("config"), + }) - cfg, err := setting.NewConfigProviderFromFile(&setting.Options{CustomConf: setting.CustomConf, AllowEmpty: true}) + cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) if err != nil { log.Fatal("Failed to load custom conf '%s': %v", setting.CustomConf, err) } diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index f53d9ee089000..13820095aee27 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -344,9 +344,6 @@ NAME = gitea USER = root ;PASSWD = ;Use PASSWD = `your password` for quoting if you use special characters in the password. ;SSL_MODE = false ; either "false" (default), "true", or "skip-verify" -;CHARSET = utf8mb4 ;either "utf8" or "utf8mb4", default is "utf8mb4". -;; -;; NOTICE: for "utf8mb4" you must use MySQL InnoDB > 5.6. Gitea is unable to check this. ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; @@ -2159,7 +2156,7 @@ LEVEL = Info ;RUN_AT_START = false ;ENABLE_SUCCESS_NOTICE = false ;SCHEDULE = @every 168h -;HTTP_ENDPOINT = https://dl.gitea.io/gitea/version.json +;HTTP_ENDPOINT = https://dl.gitea.com/gitea/version.json ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/doc/administration/config-cheat-sheet.en-us.md b/docs/content/doc/administration/config-cheat-sheet.en-us.md index 7b94c7a4882e1..77cb784637bec 100644 --- a/docs/content/doc/administration/config-cheat-sheet.en-us.md +++ b/docs/content/doc/administration/config-cheat-sheet.en-us.md @@ -443,7 +443,6 @@ The following configuration set `Content-Type: application/vnd.android.package-a - `SQLITE_TIMEOUT`: **500**: Query timeout for SQLite3 only. - `SQLITE_JOURNAL_MODE`: **""**: Change journal mode for SQlite3. Can be used to enable [WAL mode](https://www.sqlite.org/wal.html) when high load causes write congestion. See [SQlite3 docs](https://www.sqlite.org/pragma.html#pragma_journal_mode) for possible values. Defaults to the default for the database file, often DELETE. - `ITERATE_BUFFER_SIZE`: **50**: Internal buffer size for iterating. -- `CHARSET`: **utf8mb4**: For MySQL only, either "utf8" or "utf8mb4". NOTICE: for "utf8mb4" you must use MySQL InnoDB > 5.6. Gitea is unable to check this. - `PATH`: **data/gitea.db**: For SQLite3 only, the database file path. - `LOG_SQL`: **true**: Log the executed SQL. - `DB_RETRIES`: **10**: How many ORM init / DB connect attempts allowed. @@ -1013,7 +1012,7 @@ Default templates for project boards: - `RUN_AT_START`: **false**: Run tasks at start up time (if ENABLED). - `ENABLE_SUCCESS_NOTICE`: **true**: Set to false to switch off success notices. - `SCHEDULE`: **@every 168h**: Cron syntax for scheduling a work, e.g. `@every 168h`. -- `HTTP_ENDPOINT`: **https://dl.gitea.io/gitea/version.json**: the endpoint that Gitea will check for newer versions +- `HTTP_ENDPOINT`: **https://dl.gitea.com/gitea/version.json**: the endpoint that Gitea will check for newer versions #### Cron - Delete all old system notices from database (`cron.delete_old_system_notices`) diff --git a/docs/content/doc/help/faq.en-us.md b/docs/content/doc/help/faq.en-us.md index f609b6c86704c..ae59a9b8807c2 100644 --- a/docs/content/doc/help/faq.en-us.md +++ b/docs/content/doc/help/faq.en-us.md @@ -396,8 +396,6 @@ Please run `gitea convert`, or run `ALTER DATABASE database_name CHARACTER SET u for the database_name and run `ALTER TABLE table_name CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;` for each table in the database. -You will also need to change the app.ini database charset to `CHARSET=utf8mb4`. - ## Why are Emoji displaying only as placeholders or in monochrome 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. diff --git a/docs/content/doc/help/faq.zh-cn.md b/docs/content/doc/help/faq.zh-cn.md index 3458534524237..6a63b4530e8ce 100644 --- a/docs/content/doc/help/faq.zh-cn.md +++ b/docs/content/doc/help/faq.zh-cn.md @@ -31,7 +31,7 @@ menu: **注意:**此示例也适用于Docker镜像! -在我们的[下载页面](https://dl.gitea.io/gitea/)上,您会看到一个1.7目录,以及1.7.0、1.7.1、1.7.2、1.7.3、1.7.4、1.7.5和1.7.6的目录。 +在我们的[下载页面](https://dl.gitea.com/gitea/)上,您会看到一个1.7目录,以及1.7.0、1.7.1、1.7.2、1.7.3、1.7.4、1.7.5和1.7.6的目录。 1.7目录和1.7.0目录是**不同**的。1.7目录是在每个合并到[`release/v1.7`](https://github.com/go-gitea/gitea/tree/release/v1.7)分支的提交上构建的。 diff --git a/docs/content/doc/installation/from-binary.fr-fr.md b/docs/content/doc/installation/from-binary.fr-fr.md index f5273054bccf9..f3d3110439dfa 100644 --- a/docs/content/doc/installation/from-binary.fr-fr.md +++ b/docs/content/doc/installation/from-binary.fr-fr.md @@ -17,10 +17,10 @@ menu: # Installation avec le binaire pré-compilé -Tous les binaires sont livrés avec le support de SQLite, MySQL et PostgreSQL, et sont construits avec les ressources incorporées. Gardez à l'esprit que cela peut être différent pour les versions antérieures. L'installation basée sur nos binaires est assez simple, il suffit de choisir le fichier correspondant à votre plateforme à partir de la [page de téléchargement](https://dl.gitea.io/gitea). Copiez l'URL et remplacer l'URL dans les commandes suivantes par la nouvelle: +Tous les binaires sont livrés avec le support de SQLite, MySQL et PostgreSQL, et sont construits avec les ressources incorporées. Gardez à l'esprit que cela peut être différent pour les versions antérieures. L'installation basée sur nos binaires est assez simple, il suffit de choisir le fichier correspondant à votre plateforme à partir de la [page de téléchargement](https://dl.gitea.com/gitea). Copiez l'URL et remplacer l'URL dans les commandes suivantes par la nouvelle: ``` -wget -O gitea https://dl.gitea.io/gitea/{{< version >}}/gitea-{{< version >}}-linux-amd64 +wget -O gitea https://dl.gitea.com/gitea/{{< version >}}/gitea-{{< version >}}-linux-amd64 chmod +x gitea ``` diff --git a/docs/content/doc/installation/from-binary.zh-tw.md b/docs/content/doc/installation/from-binary.zh-tw.md index 858cee2193def..78db79775d2eb 100644 --- a/docs/content/doc/installation/from-binary.zh-tw.md +++ b/docs/content/doc/installation/from-binary.zh-tw.md @@ -17,10 +17,10 @@ menu: # 從執行檔安裝 -所有的執行檔皆支援 SQLite, MySQL and PostgreSQL,且所有檔案都已經包在執行檔內,這一點跟之前的版本有所不同。關於執行檔的安裝方式非常簡單,只要從[下載頁面](https://dl.gitea.io/gitea)選擇相對應平台,複製下載連結,使用底下指令就可以完成了: +所有的執行檔皆支援 SQLite, MySQL and PostgreSQL,且所有檔案都已經包在執行檔內,這一點跟之前的版本有所不同。關於執行檔的安裝方式非常簡單,只要從[下載頁面](https://dl.gitea.com/gitea)選擇相對應平台,複製下載連結,使用底下指令就可以完成了: ``` -wget -O gitea https://dl.gitea.io/gitea/{{< version >}}/gitea-{{< version >}}-linux-amd64 +wget -O gitea https://dl.gitea.com/gitea/{{< version >}}/gitea-{{< version >}}-linux-amd64 chmod +x gitea ``` diff --git a/docs/content/doc/installation/on-cloud-provider.en-us.md b/docs/content/doc/installation/on-cloud-provider.en-us.md index 20ca812effeb0..bf3bf4c312496 100644 --- a/docs/content/doc/installation/on-cloud-provider.en-us.md +++ b/docs/content/doc/installation/on-cloud-provider.en-us.md @@ -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. 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/). diff --git a/docs/content/doc/installation/on-kubernetes.en-us.md b/docs/content/doc/installation/on-kubernetes.en-us.md index b46a61df025a7..0847a3b852251 100644 --- a/docs/content/doc/installation/on-kubernetes.en-us.md +++ b/docs/content/doc/installation/on-kubernetes.en-us.md @@ -22,7 +22,7 @@ Gitea provides a Helm Chart to allow for installation on kubernetes. A non-customized install can be done with: ``` -helm repo add gitea-charts https://dl.gitea.io/charts/ +helm repo add gitea-charts https://dl.gitea.com/charts/ helm install gitea gitea-charts/gitea ``` diff --git a/docs/content/doc/installation/on-kubernetes.zh-cn.md b/docs/content/doc/installation/on-kubernetes.zh-cn.md index f5e8f9f762cb6..83647a2eab9a7 100644 --- a/docs/content/doc/installation/on-kubernetes.zh-cn.md +++ b/docs/content/doc/installation/on-kubernetes.zh-cn.md @@ -22,7 +22,7 @@ Gitea 已经提供了便于在 Kubernetes 云原生环境中安装所需的 Helm 默认安装指令为: ```bash -helm repo add gitea https://dl.gitea.io/charts +helm repo add gitea https://dl.gitea.com/charts helm repo update helm install gitea gitea/gitea ``` diff --git a/docs/content/doc/installation/on-kubernetes.zh-tw.md b/docs/content/doc/installation/on-kubernetes.zh-tw.md index 51446911d5998..28dfbda81d328 100644 --- a/docs/content/doc/installation/on-kubernetes.zh-tw.md +++ b/docs/content/doc/installation/on-kubernetes.zh-tw.md @@ -22,7 +22,7 @@ Gitea 提供 Helm Chart 用來安裝於 kubernetes。 非自訂安裝可使用下列指令: ``` -helm repo add gitea-charts https://dl.gitea.io/charts/ +helm repo add gitea-charts https://dl.gitea.com/charts/ helm install gitea gitea-charts/gitea ``` diff --git a/docs/content/doc/installation/upgrade-from-gogs.en-us.md b/docs/content/doc/installation/upgrade-from-gogs.en-us.md index 2e149c6a2bfee..fa545ee025f26 100644 --- a/docs/content/doc/installation/upgrade-from-gogs.en-us.md +++ b/docs/content/doc/installation/upgrade-from-gogs.en-us.md @@ -27,7 +27,7 @@ There are some basic steps to follow. On a Linux system run as the Gogs user: * Create a Gogs backup with `gogs backup`. This creates `gogs-backup-[timestamp].zip` file containing all important Gogs data. You would need it if you wanted to move to the `gogs` back later. -* Download the file matching the destination platform from the [downloads page](https://dl.gitea.io/gitea/). +* Download the file matching the destination platform from the [downloads page](https://dl.gitea.com/gitea/). It should be `1.0.x` version. Migrating from `gogs` to any other version is impossible. * Put the binary at the desired install location. * Copy `gogs/custom/conf/app.ini` to `gitea/custom/conf/app.ini`. @@ -79,11 +79,11 @@ There are some basic steps to follow. On a Linux system run as the Gogs user: After successful migration from `gogs` to `gitea 1.0.x`, it is possible to upgrade `gitea` to a modern version in a two steps process. -Upgrade to [`gitea 1.6.4`](https://dl.gitea.io/gitea/1.6.4/) first. Download the file matching -the destination platform from the [downloads page](https://dl.gitea.io/gitea/1.6.4/) and replace the binary. +Upgrade to [`gitea 1.6.4`](https://dl.gitea.com/gitea/1.6.4/) first. Download the file matching +the destination platform from the [downloads page](https://dl.gitea.com/gitea/1.6.4/) and replace the binary. Run Gitea at least once and check that everything works as expected. -Then repeat the procedure, but this time using the [latest release](https://dl.gitea.io/gitea/{{< version >}}/). +Then repeat the procedure, but this time using the [latest release](https://dl.gitea.com/gitea/{{< version >}}/). ## Upgrading from a more recent version of Gogs diff --git a/docs/content/doc/installation/upgrade-from-gogs.fr-fr.md b/docs/content/doc/installation/upgrade-from-gogs.fr-fr.md index 9a46562f066e8..9d287d111dc58 100644 --- a/docs/content/doc/installation/upgrade-from-gogs.fr-fr.md +++ b/docs/content/doc/installation/upgrade-from-gogs.fr-fr.md @@ -22,7 +22,7 @@ menu: Veuillez suivre les étapes ci-dessous. Sur Unix, toute les commandes s'exécutent en tant que l'utilisateur utilisé pour votre installation de Gogs : * Crééer une sauvegarde de Gogs avec la commande `gogs dump`. Le fichier nouvellement créé `gogs-dump-[timestamp].zip` contient toutes les données de votre instance de Gogs. -* Téléchargez le fichier correspondant à votre plateforme à partir de la [page de téléchargements](https://dl.gitea.io/gitea). +* Téléchargez le fichier correspondant à votre plateforme à partir de la [page de téléchargements](https://dl.gitea.com/gitea). * Mettez la binaire dans le répertoire d'installation souhaité. * Copiez le fichier `gogs/custom/conf/app.ini` vers `gitea/custom/conf/app.ini`. * Si vous avez personnalisé les répertoires `templates, public` dans `gogs/custom/`, copiez-les vers `gitea/custom/`. diff --git a/docs/content/doc/installation/upgrade-from-gogs.zh-tw.md b/docs/content/doc/installation/upgrade-from-gogs.zh-tw.md index 9812efaf946f1..46442845e7801 100644 --- a/docs/content/doc/installation/upgrade-from-gogs.zh-tw.md +++ b/docs/content/doc/installation/upgrade-from-gogs.zh-tw.md @@ -27,7 +27,7 @@ menu: - 使用 `gogs backup` 建立 Gogs 的備份。這會建立檔案 `gogs-backup-[timestamp].zip` 包含所有重要的 Gogs 資料。 如果稍後您要恢復到 `gogs` 時會用到它。 -- 從[下載頁](https://dl.gitea.io/gitea/)下載對應您平臺的檔案。請下載 `1.0.x` 版,從 `gogs` 遷移到其它版本是不可行的。 +- 從[下載頁](https://dl.gitea.com/gitea/)下載對應您平臺的檔案。請下載 `1.0.x` 版,從 `gogs` 遷移到其它版本是不可行的。 - 將二進位檔放到適當的安裝位置。 - 複製 `gogs/custom/conf/app.ini` 到 `gitea/custom/conf/app.ini`。 - 從 `gogs/custom/` 複製自訂 `templates, public` 到 `gitea/custom/`。 @@ -77,10 +77,10 @@ menu: 成功從 `gogs` 升級到 `gitea 1.0.x` 後再用 2 個步驟即可升級到最新版的 `gitea`。 -請先升級到 [`gitea 1.6.4`](https://dl.gitea.io/gitea/1.6.4/),先從[下載頁](https://dl.gitea.io/gitea/1.6.4/)下載 +請先升級到 [`gitea 1.6.4`](https://dl.gitea.com/gitea/1.6.4/),先從[下載頁](https://dl.gitea.com/gitea/1.6.4/)下載 您平臺的二進位檔取代既有的。至少執行一次 Gitea 並確認一切符合預期。 -接著重複上述步驟,但這次請使用[最新發行版本](https://dl.gitea.io/gitea/{{< version >}}/)。 +接著重複上述步驟,但這次請使用[最新發行版本](https://dl.gitea.com/gitea/{{< version >}}/)。 ## 從更新版本的 Gogs 升級 diff --git a/docs/content/doc/usage/template-repositories.en-us.md b/docs/content/doc/usage/template-repositories.en-us.md index 0c278648b3ec5..5687861b8ca4f 100644 --- a/docs/content/doc/usage/template-repositories.en-us.md +++ b/docs/content/doc/usage/template-repositories.en-us.md @@ -51,6 +51,8 @@ a/b/c/d.json In any file matched by the above globs, certain variables will be expanded. +Matching filenames and paths can also be expanded, and are conservatively sanitized to support cross-platform filesystems. + All variables must be of the form `$VAR` or `${VAR}`. To escape an expansion, use a double `$$`, such as `$$VAR` or `$${VAR}` | Variable | Expands To | Transformable | diff --git a/main.go b/main.go index 49093eb8a7038..1c87824c83ce7 100644 --- a/main.go +++ b/main.go @@ -33,30 +33,58 @@ var ( Tags = "" // MakeVersion holds the current Make version if built with make MakeVersion = "" - - originalAppHelpTemplate = "" - originalCommandHelpTemplate = "" - originalSubcommandHelpTemplate = "" ) func init() { setting.AppVer = Version setting.AppBuiltWith = formatBuiltWith() setting.AppStartTime = time.Now().UTC() +} - // Grab the original help templates - originalAppHelpTemplate = cli.AppHelpTemplate - originalCommandHelpTemplate = cli.CommandHelpTemplate - originalSubcommandHelpTemplate = cli.SubcommandHelpTemplate +// cmdHelp is our own help subcommand with more information +// test cases: +// ./gitea help +// ./gitea -h +// ./gitea web help +// ./gitea web -h (due to cli lib limitation, this won't call our cmdHelp, so no extra info) +// ./gitea admin help auth +// ./gitea -c /tmp/app.ini -h +// ./gitea -c /tmp/app.ini help +// ./gitea help -c /tmp/app.ini +// GITEA_WORK_DIR=/tmp ./gitea help +// GITEA_WORK_DIR=/tmp ./gitea help --work-path /tmp/other +// GITEA_WORK_DIR=/tmp ./gitea help --config /tmp/app-other.ini +var cmdHelp = cli.Command{ + Name: "help", + Aliases: []string{"h"}, + Usage: "Shows a list of commands or help for one command", + ArgsUsage: "[command]", + Action: func(c *cli.Context) (err error) { + args := c.Args() + if args.Present() { + err = cli.ShowCommandHelp(c, args.First()) + } else { + err = cli.ShowAppHelp(c) + } + _, _ = fmt.Fprintf(c.App.Writer, ` +DEFAULT CONFIGURATION: + AppPath: %s + WorkPath: %s + CustomPath: %s + ConfigFile: %s + +`, setting.AppPath, setting.AppWorkPath, setting.CustomPath, setting.CustomConf) + return err + }, } func main() { app := cli.NewApp() app.Name = "Gitea" app.Usage = "A painless self-hosted Git service" - app.Description = `By default, gitea will start serving using the webserver with no -arguments - which can alternatively be run by running the subcommand web.` + app.Description = `By default, Gitea will start serving using the web-server with no argument, which can alternatively be run by running the subcommand "web".` app.Version = Version + formatBuiltWith() + app.EnableBashCompletion = true app.Commands = []cli.Command{ cmd.CmdWeb, cmd.CmdServ, @@ -77,118 +105,83 @@ arguments - which can alternatively be run by running the subcommand web.` cmd.CmdRestoreRepository, cmd.CmdActions, } - // Now adjust these commands to add our global configuration options - - // First calculate the default paths and set the AppHelpTemplates in this context - setting.SetCustomPathAndConf("", "", "") - setAppHelpTemplates() // default configuration flags - defaultFlags := []cli.Flag{ + globalFlags := []cli.Flag{ + cli.HelpFlag, cli.StringFlag{ Name: "custom-path, C", - Value: setting.CustomPath, - Usage: "Custom path file path", + Usage: "Set custom path (defaults to '{WorkPath}/custom')", }, cli.StringFlag{ Name: "config, c", Value: setting.CustomConf, - Usage: "Custom configuration file path", + Usage: "Set custom config file (defaults to '{WorkPath}/custom/conf/app.ini')", }, - cli.VersionFlag, cli.StringFlag{ Name: "work-path, w", - Value: setting.AppWorkPath, - Usage: "Set the gitea working path", + Usage: "Set Gitea's working path (defaults to the Gitea's binary directory)", }, } // Set the default to be equivalent to cmdWeb and add the default flags + app.Flags = append(app.Flags, globalFlags...) app.Flags = append(app.Flags, cmd.CmdWeb.Flags...) - app.Flags = append(app.Flags, defaultFlags...) - app.Action = cmd.CmdWeb.Action - - // Add functions to set these paths and these flags to the commands - app.Before = establishCustomPath + app.Action = prepareWorkPathAndCustomConf(cmd.CmdWeb.Action) + app.HideHelp = true // use our own help action to show helps (with more information like default config) + app.Commands = append(app.Commands, cmdHelp) for i := range app.Commands { - setFlagsAndBeforeOnSubcommands(&app.Commands[i], defaultFlags, establishCustomPath) + prepareSubcommands(&app.Commands[i], globalFlags) } - app.EnableBashCompletion = true - err := app.Run(os.Args) if err != nil { - log.Fatal("Failed to run app with %s: %v", os.Args, err) + _, _ = fmt.Fprintf(app.Writer, "\nFailed to run with %s: %v\n", os.Args, err) } log.GetManager().Close() } -func setFlagsAndBeforeOnSubcommands(command *cli.Command, defaultFlags []cli.Flag, before cli.BeforeFunc) { +func prepareSubcommands(command *cli.Command, defaultFlags []cli.Flag) { command.Flags = append(command.Flags, defaultFlags...) - command.Before = establishCustomPath + command.Action = prepareWorkPathAndCustomConf(command.Action) + command.HideHelp = true + if command.Name != "help" { + command.Subcommands = append(command.Subcommands, cmdHelp) + } for i := range command.Subcommands { - setFlagsAndBeforeOnSubcommands(&command.Subcommands[i], defaultFlags, before) + prepareSubcommands(&command.Subcommands[i], defaultFlags) } } -func establishCustomPath(ctx *cli.Context) error { - var providedCustom string - var providedConf string - var providedWorkPath string - - currentCtx := ctx - for { - if len(providedCustom) != 0 && len(providedConf) != 0 && len(providedWorkPath) != 0 { - break - } - if currentCtx == nil { - break - } - if currentCtx.IsSet("custom-path") && len(providedCustom) == 0 { - providedCustom = currentCtx.String("custom-path") - } - if currentCtx.IsSet("config") && len(providedConf) == 0 { - providedConf = currentCtx.String("config") +// prepareWorkPathAndCustomConf wraps the Action to prepare the work path and custom config +// It can't use "Before", because each level's sub-command's Before will be called one by one, so the "init" would be done multiple times +func prepareWorkPathAndCustomConf(a any) func(ctx *cli.Context) error { + if a == nil { + return nil + } + action := a.(func(*cli.Context) error) + return func(ctx *cli.Context) error { + var args setting.ArgWorkPathAndCustomConf + curCtx := ctx + for curCtx != nil { + if curCtx.IsSet("work-path") && args.WorkPath == "" { + args.WorkPath = curCtx.String("work-path") + } + if curCtx.IsSet("custom-path") && args.CustomPath == "" { + args.CustomPath = curCtx.String("custom-path") + } + if curCtx.IsSet("config") && args.CustomConf == "" { + args.CustomConf = curCtx.String("config") + } + curCtx = curCtx.Parent() } - if currentCtx.IsSet("work-path") && len(providedWorkPath) == 0 { - providedWorkPath = currentCtx.String("work-path") + setting.InitWorkPathAndCommonConfig(os.Getenv, args) + if ctx.Bool("help") { + return cmdHelp.Action.(func(ctx *cli.Context) error)(ctx) } - currentCtx = currentCtx.Parent() - - } - setting.SetCustomPathAndConf(providedCustom, providedConf, providedWorkPath) - - setAppHelpTemplates() - - if ctx.IsSet("version") { - cli.ShowVersion(ctx) - os.Exit(0) - } - - return nil -} - -func setAppHelpTemplates() { - cli.AppHelpTemplate = adjustHelpTemplate(originalAppHelpTemplate) - cli.CommandHelpTemplate = adjustHelpTemplate(originalCommandHelpTemplate) - cli.SubcommandHelpTemplate = adjustHelpTemplate(originalSubcommandHelpTemplate) -} - -func adjustHelpTemplate(originalTemplate string) string { - overridden := "" - if _, ok := os.LookupEnv("GITEA_CUSTOM"); ok { - overridden = "(GITEA_CUSTOM)" + return action(ctx) } - - return fmt.Sprintf(`%s -DEFAULT CONFIGURATION: - CustomPath: %s %s - CustomConf: %s - AppPath: %s - AppWorkPath: %s - -`, originalTemplate, setting.CustomPath, overridden, setting.CustomConf, setting.AppPath, setting.AppWorkPath) } func formatBuiltWith() string { diff --git a/models/actions/run_list.go b/models/actions/run_list.go index 56de8eb9169cf..29ab193d57f3e 100644 --- a/models/actions/run_list.go +++ b/models/actions/run_list.go @@ -71,6 +71,7 @@ type FindRunOptions struct { WorkflowFileName string TriggerUserID int64 Approved bool // not util.OptionalBool, it works only when it's true + Status Status } func (opts FindRunOptions) toConds() builder.Cond { @@ -90,6 +91,9 @@ func (opts FindRunOptions) toConds() builder.Cond { if opts.Approved { cond = cond.And(builder.Gt{"approved_by": 0}) } + if opts.Status > StatusUnknown { + cond = cond.And(builder.Eq{"status": opts.Status}) + } return cond } @@ -106,3 +110,34 @@ func FindRuns(ctx context.Context, opts FindRunOptions) (RunList, int64, error) func CountRuns(ctx context.Context, opts FindRunOptions) (int64, error) { return db.GetEngine(ctx).Where(opts.toConds()).Count(new(ActionRun)) } + +type StatusInfo struct { + Status int + DisplayedStatus string +} + +// GetStatusInfoList returns a slice of StatusInfo +func GetStatusInfoList(ctx context.Context) []StatusInfo { + // same as those in aggregateJobStatus + allStatus := []Status{StatusSuccess, StatusFailure, StatusWaiting, StatusRunning} + statusInfoList := make([]StatusInfo, 0, 4) + for _, s := range allStatus { + statusInfoList = append(statusInfoList, StatusInfo{ + Status: int(s), + DisplayedStatus: s.String(), + }) + } + return statusInfoList +} + +// GetActors returns a slice of Actors +func GetActors(ctx context.Context, repoID int64) ([]*user_model.User, error) { + actors := make([]*user_model.User, 0, 10) + + return actors, db.GetEngine(ctx).Where(builder.In("id", builder.Select("`action_run`.trigger_user_id").From("`action_run`"). + GroupBy("`action_run`.trigger_user_id"). + Where(builder.Eq{"`action_run`.repo_id": repoID}))). + Cols("id", "name", "full_name", "avatar", "avatar_email", "use_custom_avatar"). + OrderBy(user_model.GetOrderByName()). + Find(&actors) +} diff --git a/models/actions/variable.go b/models/actions/variable.go new file mode 100644 index 0000000000000..e0bb59ccbe6e1 --- /dev/null +++ b/models/actions/variable.go @@ -0,0 +1,97 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "errors" + "fmt" + "strings" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) + +type ActionVariable struct { + ID int64 `xorm:"pk autoincr"` + OwnerID int64 `xorm:"UNIQUE(owner_repo_name)"` + RepoID int64 `xorm:"INDEX UNIQUE(owner_repo_name)"` + Name string `xorm:"UNIQUE(owner_repo_name) NOT NULL"` + Data string `xorm:"LONGTEXT NOT NULL"` + CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` +} + +func init() { + db.RegisterModel(new(ActionVariable)) +} + +func (v *ActionVariable) Validate() error { + if v.OwnerID == 0 && v.RepoID == 0 { + return errors.New("the variable is not bound to any scope") + } + return nil +} + +func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*ActionVariable, error) { + variable := &ActionVariable{ + OwnerID: ownerID, + RepoID: repoID, + Name: strings.ToUpper(name), + Data: data, + } + if err := variable.Validate(); err != nil { + return variable, err + } + return variable, db.Insert(ctx, variable) +} + +type FindVariablesOpts struct { + db.ListOptions + OwnerID int64 + RepoID int64 +} + +func (opts *FindVariablesOpts) toConds() builder.Cond { + cond := builder.NewCond() + if opts.OwnerID > 0 { + cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) + } + if opts.RepoID > 0 { + cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) + } + return cond +} + +func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*ActionVariable, error) { + var variables []*ActionVariable + sess := db.GetEngine(ctx) + if opts.PageSize != 0 { + sess = db.SetSessionPagination(sess, &opts.ListOptions) + } + return variables, sess.Where(opts.toConds()).Find(&variables) +} + +func GetVariableByID(ctx context.Context, variableID int64) (*ActionVariable, error) { + var variable ActionVariable + has, err := db.GetEngine(ctx).Where("id=?", variableID).Get(&variable) + if err != nil { + return nil, err + } else if !has { + return nil, fmt.Errorf("variable with id %d: %w", variableID, util.ErrNotExist) + } + return &variable, nil +} + +func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) { + count, err := db.GetEngine(ctx).ID(variable.ID).Cols("name", "data"). + Update(&ActionVariable{ + Name: variable.Name, + Data: variable.Data, + }) + return count != 0, err +} diff --git a/models/issues/comment_code.go b/models/issues/comment_code.go index 304ac4569f86e..d447d7542cd26 100644 --- a/models/issues/comment_code.go +++ b/models/issues/comment_code.go @@ -18,11 +18,11 @@ import ( type CodeComments map[string]map[int64][]*Comment // FetchCodeComments will return a 2d-map: ["Path"]["Line"] = Comments at line -func FetchCodeComments(ctx context.Context, issue *Issue, currentUser *user_model.User) (CodeComments, error) { - return fetchCodeCommentsByReview(ctx, issue, currentUser, nil) +func FetchCodeComments(ctx context.Context, issue *Issue, currentUser *user_model.User, showOutdatedComments bool) (CodeComments, error) { + return fetchCodeCommentsByReview(ctx, issue, currentUser, nil, showOutdatedComments) } -func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *user_model.User, review *Review) (CodeComments, error) { +func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *user_model.User, review *Review, showOutdatedComments bool) (CodeComments, error) { pathToLineToComment := make(CodeComments) if review == nil { review = &Review{ID: 0} @@ -33,7 +33,7 @@ func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *u ReviewID: review.ID, } - comments, err := findCodeComments(ctx, opts, issue, currentUser, review) + comments, err := findCodeComments(ctx, opts, issue, currentUser, review, showOutdatedComments) if err != nil { return nil, err } @@ -47,15 +47,17 @@ func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *u return pathToLineToComment, nil } -func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issue, currentUser *user_model.User, review *Review) ([]*Comment, error) { +func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issue, currentUser *user_model.User, review *Review, showOutdatedComments bool) ([]*Comment, error) { var comments CommentList if review == nil { review = &Review{ID: 0} } conds := opts.ToConds() - if review.ID == 0 { + + if !showOutdatedComments && review.ID == 0 { conds = conds.And(builder.Eq{"invalidated": false}) } + e := db.GetEngine(ctx) if err := e.Where(conds). Asc("comment.created_unix"). @@ -118,12 +120,12 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu } // FetchCodeCommentsByLine fetches the code comments for a given treePath and line number -func FetchCodeCommentsByLine(ctx context.Context, issue *Issue, currentUser *user_model.User, treePath string, line int64) ([]*Comment, error) { +func FetchCodeCommentsByLine(ctx context.Context, issue *Issue, currentUser *user_model.User, treePath string, line int64, showOutdatedComments bool) ([]*Comment, error) { opts := FindCommentsOptions{ Type: CommentTypeCode, IssueID: issue.ID, TreePath: treePath, Line: line, } - return findCodeComments(ctx, opts, issue, currentUser, nil) + return findCodeComments(ctx, opts, issue, currentUser, nil, showOutdatedComments) } diff --git a/models/issues/comment_test.go b/models/issues/comment_test.go index 43bad1660f1f5..d766625be3dc8 100644 --- a/models/issues/comment_test.go +++ b/models/issues/comment_test.go @@ -50,7 +50,7 @@ func TestFetchCodeComments(t *testing.T) { issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - res, err := issues_model.FetchCodeComments(db.DefaultContext, issue, user) + res, err := issues_model.FetchCodeComments(db.DefaultContext, issue, user, false) assert.NoError(t, err) assert.Contains(t, res, "README.md") assert.Contains(t, res["README.md"], int64(4)) @@ -58,7 +58,7 @@ func TestFetchCodeComments(t *testing.T) { assert.Equal(t, int64(4), res["README.md"][4][0].ID) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - res, err = issues_model.FetchCodeComments(db.DefaultContext, issue, user2) + res, err = issues_model.FetchCodeComments(db.DefaultContext, issue, user2, false) assert.NoError(t, err) assert.Len(t, res, 1) } diff --git a/models/issues/review.go b/models/issues/review.go index 3a1ab7468afca..3685c65ce581c 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -141,7 +141,7 @@ func (r *Review) LoadCodeComments(ctx context.Context) (err error) { if err = r.loadIssue(ctx); err != nil { return } - r.CodeComments, err = fetchCodeCommentsByReview(ctx, r.Issue, nil, r) + r.CodeComments, err = fetchCodeCommentsByReview(ctx, r.Issue, nil, r, false) return err } diff --git a/models/issues/tracked_time.go b/models/issues/tracked_time.go index ac65d654f20a3..698014afeba54 100644 --- a/models/issues/tracked_time.go +++ b/models/issues/tracked_time.go @@ -199,8 +199,8 @@ func addTime(ctx context.Context, user *user_model.User, issue *Issue, amount in return tt, db.Insert(ctx, tt) } -// TotalTimes returns the spent time for each user by an issue -func TotalTimes(options *FindTrackedTimesOptions) (map[*user_model.User]string, error) { +// TotalTimes returns the spent time in seconds for each user by an issue +func TotalTimes(options *FindTrackedTimesOptions) (map[*user_model.User]int64, error) { trackedTimes, err := GetTrackedTimes(db.DefaultContext, options) if err != nil { return nil, err @@ -211,7 +211,7 @@ func TotalTimes(options *FindTrackedTimesOptions) (map[*user_model.User]string, totalTimesByUser[t.UserID] += t.Time } - totalTimes := make(map[*user_model.User]string) + totalTimes := make(map[*user_model.User]int64) // Fetching User and making time human readable for userID, total := range totalTimesByUser { user, err := user_model.GetUserByID(db.DefaultContext, userID) @@ -221,7 +221,7 @@ func TotalTimes(options *FindTrackedTimesOptions) (map[*user_model.User]string, } return nil, err } - totalTimes[user] = util.SecToTime(total) + totalTimes[user] = total } return totalTimes, nil } diff --git a/models/issues/tracked_time_test.go b/models/issues/tracked_time_test.go index 99b977cca5264..baa170b201265 100644 --- a/models/issues/tracked_time_test.go +++ b/models/issues/tracked_time_test.go @@ -86,8 +86,8 @@ func TestTotalTimes(t *testing.T) { assert.NoError(t, err) assert.Len(t, total, 1) for user, time := range total { - assert.Equal(t, int64(1), user.ID) - assert.Equal(t, "6 minutes 40 seconds", time) + assert.EqualValues(t, 1, user.ID) + assert.EqualValues(t, 400, time) } total, err = issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: 2}) @@ -95,9 +95,9 @@ func TestTotalTimes(t *testing.T) { assert.Len(t, total, 2) for user, time := range total { if user.ID == 2 { - assert.Equal(t, "1 hour 1 minute", time) + assert.EqualValues(t, 3662, time) } else if user.ID == 1 { - assert.Equal(t, "20 seconds", time) + assert.EqualValues(t, 20, time) } else { assert.Error(t, assert.AnError) } @@ -107,8 +107,8 @@ func TestTotalTimes(t *testing.T) { assert.NoError(t, err) assert.Len(t, total, 1) for user, time := range total { - assert.Equal(t, int64(2), user.ID) - assert.Equal(t, "1 second", time) + assert.EqualValues(t, 2, user.ID) + assert.EqualValues(t, 1, time) } total, err = issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: 4}) diff --git a/models/migrations/base/tests.go b/models/migrations/base/tests.go index dd99a1eda280c..c3100ba665948 100644 --- a/models/migrations/base/tests.go +++ b/models/migrations/base/tests.go @@ -147,9 +147,9 @@ func MainTest(m *testing.M) { os.Exit(1) } + setting.CustomPath = filepath.Join(setting.AppWorkPath, "custom") setting.AppDataPath = tmpDataPath - setting.SetCustomPathAndConf("", "", "") unittest.InitSettings() if err = git.InitFull(context.Background()); err != nil { fmt.Printf("Unable to InitFull: %v\n", err) diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 4eb512ab49f5c..1d443b3d152cb 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -503,6 +503,9 @@ var migrations = []Migration{ // v260 -> v261 NewMigration("Drop custom_labels column of action_runner table", v1_21.DropCustomLabelsColumnOfActionRunner), + + // v261 -> v262 + NewMigration("Add variable table", v1_21.CreateVariableTable), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v1_21/v261.go b/models/migrations/v1_21/v261.go new file mode 100644 index 0000000000000..4ec1160d0b3eb --- /dev/null +++ b/models/migrations/v1_21/v261.go @@ -0,0 +1,24 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_21 //nolint + +import ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func CreateVariableTable(x *xorm.Engine) error { + type ActionVariable struct { + ID int64 `xorm:"pk autoincr"` + OwnerID int64 `xorm:"UNIQUE(owner_repo_name)"` + RepoID int64 `xorm:"INDEX UNIQUE(owner_repo_name)"` + Name string `xorm:"UNIQUE(owner_repo_name) NOT NULL"` + Data string `xorm:"LONGTEXT NOT NULL"` + CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` + } + + return x.Sync(new(ActionVariable)) +} diff --git a/models/secret/secret.go b/models/secret/secret.go index 8b23b6c35cf8c..5a17cc37a5dc0 100644 --- a/models/secret/secret.go +++ b/models/secret/secret.go @@ -5,38 +5,17 @@ package secret import ( "context" - "fmt" - "regexp" + "errors" "strings" "code.gitea.io/gitea/models/db" secret_module "code.gitea.io/gitea/modules/secret" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) -type ErrSecretInvalidValue struct { - Name *string - Data *string -} - -func (err ErrSecretInvalidValue) Error() string { - if err.Name != nil { - return fmt.Sprintf("secret name %q is invalid", *err.Name) - } - if err.Data != nil { - return fmt.Sprintf("secret data %q is invalid", *err.Data) - } - return util.ErrInvalidArgument.Error() -} - -func (err ErrSecretInvalidValue) Unwrap() error { - return util.ErrInvalidArgument -} - // Secret represents a secret type Secret struct { ID int64 @@ -74,24 +53,11 @@ func init() { db.RegisterModel(new(Secret)) } -var ( - secretNameReg = regexp.MustCompile("^[A-Z_][A-Z0-9_]*$") - forbiddenSecretPrefixReg = regexp.MustCompile("^GIT(EA|HUB)_") -) - -// Validate validates the required fields and formats. func (s *Secret) Validate() error { - switch { - case len(s.Name) == 0 || len(s.Name) > 50: - return ErrSecretInvalidValue{Name: &s.Name} - case len(s.Data) == 0: - return ErrSecretInvalidValue{Data: &s.Data} - case !secretNameReg.MatchString(s.Name) || - forbiddenSecretPrefixReg.MatchString(s.Name): - return ErrSecretInvalidValue{Name: &s.Name} - default: - return nil + if s.OwnerID == 0 && s.RepoID == 0 { + return errors.New("the secret is not bound to any scope") } + return nil } type FindSecretsOptions struct { diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 5351ff1139989..f926a65538efa 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -42,12 +42,14 @@ func fatalTestError(fmtStr string, args ...interface{}) { os.Exit(1) } -// InitSettings initializes config provider and load common setttings for tests +// InitSettings initializes config provider and load common settings for tests func InitSettings(extraConfigs ...string) { - setting.Init(&setting.Options{ - AllowEmpty: true, - ExtraConfig: strings.Join(extraConfigs, "\n"), - }) + if setting.CustomConf == "" { + setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini") + _ = os.Remove(setting.CustomConf) + } + setting.InitCfgProvider(setting.CustomConf, strings.Join(extraConfigs, "\n")) + setting.LoadCommonSettings() if err := setting.PrepareAppDataPath(); err != nil { log.Fatalf("Can not prepare APP_DATA_PATH: %v", err) @@ -69,7 +71,7 @@ type TestOptions struct { // MainTest a reusable TestMain(..) function for unit tests that need to use a // test database. Creates the test database, and sets necessary settings. func MainTest(m *testing.M, testOpts *TestOptions) { - setting.SetCustomPathAndConf("", "", "") + setting.CustomPath = filepath.Join(testOpts.GiteaRootPath, "custom") InitSettings() var err error diff --git a/models/user/setting_keys.go b/models/user/setting_keys.go index 10255735b3177..72b3974eee435 100644 --- a/models/user/setting_keys.go +++ b/models/user/setting_keys.go @@ -8,6 +8,8 @@ const ( SettingsKeyHiddenCommentTypes = "issue.hidden_comment_types" // SettingsKeyDiffWhitespaceBehavior is the setting key for whitespace behavior of diff SettingsKeyDiffWhitespaceBehavior = "diff.whitespace_behaviour" + // SettingsKeyShowOutdatedComments is the setting key wether or not to show outdated comments in PRs + SettingsKeyShowOutdatedComments = "comment_code.show_outdated" // UserActivityPubPrivPem is user's private key UserActivityPubPrivPem = "activitypub.priv_pem" // UserActivityPubPubPem is user's public key diff --git a/modules/context/api.go b/modules/context/api.go index 092ad73f31118..3c4d02041334e 100644 --- a/modules/context/api.go +++ b/modules/context/api.go @@ -20,6 +20,8 @@ import ( "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/log" "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" ) @@ -41,6 +43,12 @@ type APIContext struct { 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: // * 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 diff --git a/modules/context/base.go b/modules/context/base.go index c8238050f9266..839f3e10df480 100644 --- a/modules/context/base.go +++ b/modules/context/base.go @@ -96,7 +96,11 @@ func (b *Base) SetTotalCountHeader(total int64) { // Written returns true if there are something sent to web browser 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 @@ -136,6 +140,10 @@ func (b *Base) JSONRedirect(redirect string) { b.JSON(http.StatusOK, map[string]any{"redirect": redirect}) } +func (b *Base) JSONOK() { + b.JSON(http.StatusOK, map[string]any{"ok": true}) // this is only a dummy response, frontend seldom uses it +} + func (b *Base) JSONError(msg string) { b.JSON(http.StatusBadRequest, map[string]any{"errorMessage": msg}) } diff --git a/modules/context/context.go b/modules/context/context.go index 9e351432c4cd7..93d448fca5550 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -21,7 +21,9 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/translation" + "code.gitea.io/gitea/modules/web" "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/session" @@ -58,6 +60,12 @@ type Context struct { 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. // This is useful if the locale message is intended to only produce HTML content. func (ctx *Context) TrHTMLEscapeArgs(msg string, args ...string) string { diff --git a/modules/context/private.go b/modules/context/private.go index 41ca8a4709cea..2e9b31140ab56 100644 --- a/modules/context/private.go +++ b/modules/context/private.go @@ -11,6 +11,8 @@ import ( "code.gitea.io/gitea/modules/graceful" "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 @@ -21,6 +23,12 @@ type PrivateContext struct { 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 func (ctx *PrivateContext) Deadline() (deadline time.Time, ok bool) { if ctx.Override != nil { diff --git a/modules/context/response.go b/modules/context/response.go index 8708d77da0a4f..2f271f211b83a 100644 --- a/modules/context/response.go +++ b/modules/context/response.go @@ -5,15 +5,20 @@ package context import ( "net/http" + + web_types "code.gitea.io/gitea/modules/web/types" ) // ResponseWriter represents a response writer for HTTP type ResponseWriter interface { http.ResponseWriter http.Flusher - Status() int + web_types.ResponseStatusProvider + 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{} @@ -46,6 +51,10 @@ func (r *Response) Write(bs []byte) (int, error) { return size, nil } +func (r *Response) Status() int { + return r.status +} + func (r *Response) Size() int { return r.written } @@ -71,8 +80,8 @@ func (r *Response) Flush() { } } -// Status returned status code written -func (r *Response) Status() int { +// WrittenStatus returned status code written +func (r *Response) WrittenStatus() int { return r.status } diff --git a/modules/doctor/doctor.go b/modules/doctor/doctor.go index 10838a751217d..ceee32285218b 100644 --- a/modules/doctor/doctor.go +++ b/modules/doctor/doctor.go @@ -28,7 +28,7 @@ type Check struct { } func initDBSkipLogger(ctx context.Context) error { - setting.Init(&setting.Options{}) + setting.MustInstalled() setting.LoadDBSetting() if err := db.InitEngine(ctx); err != nil { return fmt.Errorf("db.InitEngine: %w", err) diff --git a/modules/doctor/paths.go b/modules/doctor/paths.go index 957152349c25f..3f62d587ab4c2 100644 --- a/modules/doctor/paths.go +++ b/modules/doctor/paths.go @@ -66,7 +66,7 @@ func checkConfigurationFiles(ctx context.Context, logger log.Logger, autofix boo return err } - setting.Init(&setting.Options{}) + setting.MustInstalled() configurationFiles := []configurationFile{ {"Configuration File Path", setting.CustomConf, false, true, false}, diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 5e5e4fecbbb7f..a8d7ba7948ded 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -10,6 +10,7 @@ import ( "strings" "testing" + "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" @@ -28,9 +29,7 @@ var localMetas = map[string]string{ } func TestMain(m *testing.M) { - setting.Init(&setting.Options{ - AllowEmpty: true, - }) + unittest.InitSettings() if err := git.InitSimple(context.Background()); err != nil { log.Fatal("git init failed, err: %v", err) } diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 4bd2ca8d4107e..f2322b25544bf 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" @@ -33,9 +34,7 @@ var localMetas = map[string]string{ } func TestMain(m *testing.M) { - setting.Init(&setting.Options{ - AllowEmpty: true, - }) + unittest.InitSettings() if err := git.InitSimple(context.Background()); err != nil { log.Fatal("git init failed, err: %v", err) } diff --git a/modules/repository/generate.go b/modules/repository/generate.go index 31d5ebbb11f40..cb25daa10b330 100644 --- a/modules/repository/generate.go +++ b/modules/repository/generate.go @@ -11,6 +11,7 @@ import ( "os" "path" "path/filepath" + "regexp" "strings" "time" @@ -48,7 +49,7 @@ var defaultTransformers = []transformer{ {Name: "TITLE", Transform: util.ToTitleCase}, } -func generateExpansion(src string, templateRepo, generateRepo *repo_model.Repository) string { +func generateExpansion(src string, templateRepo, generateRepo *repo_model.Repository, sanitizeFileName bool) string { expansions := []expansion{ {Name: "REPO_NAME", Value: generateRepo.Name, Transformers: defaultTransformers}, {Name: "TEMPLATE_NAME", Value: templateRepo.Name, Transformers: defaultTransformers}, @@ -74,6 +75,9 @@ func generateExpansion(src string, templateRepo, generateRepo *repo_model.Reposi return os.Expand(src, func(key string) string { if expansion, ok := expansionMap[key]; ok { + if sanitizeFileName { + return fileNameSanitize(expansion) + } return expansion } return key @@ -191,10 +195,24 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r } if err := os.WriteFile(path, - []byte(generateExpansion(string(content), templateRepo, generateRepo)), + []byte(generateExpansion(string(content), templateRepo, generateRepo, false)), 0o644); err != nil { return err } + + substPath := filepath.FromSlash(filepath.Join(tmpDirSlash, + generateExpansion(base, templateRepo, generateRepo, true))) + + // Create parent subdirectories if needed or continue silently if it exists + if err := os.MkdirAll(filepath.Dir(substPath), 0o755); err != nil { + return err + } + + // Substitute filename variables + if err := os.Rename(path, substPath); err != nil { + return err + } + break } } @@ -353,3 +371,13 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ return generateRepo, nil } + +var fileNameSanitizeRegexp = regexp.MustCompile(`(?i)\.\.|[<>:\"/\\|?*\x{0000}-\x{001F}]|^(con|prn|aux|nul|com\d|lpt\d)$`) + +// Sanitize user input to valid OS filenames +// +// Based on https://github.com/sindresorhus/filename-reserved-regex +// Adds ".." to prevent directory traversal +func fileNameSanitize(s string) string { + return strings.TrimSpace(fileNameSanitizeRegexp.ReplaceAllString(s, "_")) +} diff --git a/modules/repository/generate_test.go b/modules/repository/generate_test.go index 1cb9a50f6731a..b0f97d0ffb4e0 100644 --- a/modules/repository/generate_test.go +++ b/modules/repository/generate_test.go @@ -54,3 +54,14 @@ func TestGiteaTemplate(t *testing.T) { }) } } + +func TestFileNameSanitize(t *testing.T) { + assert.Equal(t, "test_CON", fileNameSanitize("test_CON")) + assert.Equal(t, "test CON", fileNameSanitize("test CON ")) + assert.Equal(t, "__traverse__", fileNameSanitize("../traverse/..")) + assert.Equal(t, "http___localhost_3003_user_test.git", fileNameSanitize("http://localhost:3003/user/test.git")) + assert.Equal(t, "_", fileNameSanitize("CON")) + assert.Equal(t, "_", fileNameSanitize("con")) + assert.Equal(t, "_", fileNameSanitize("\u0000")) + assert.Equal(t, "目标", fileNameSanitize("目标")) +} diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go index 526d69bbdc7dd..94dd989850aea 100644 --- a/modules/setting/config_provider.go +++ b/modules/setting/config_provider.go @@ -4,6 +4,7 @@ package setting import ( + "errors" "fmt" "os" "path/filepath" @@ -51,12 +52,18 @@ type ConfigProvider interface { GetSection(name string) (ConfigSection, error) Save() error SaveTo(filename string) error + + DisableSaving() + PrepareSaving() (ConfigProvider, error) + IsLoadedFromEmpty() bool } type iniConfigProvider struct { - opts *Options - ini *ini.File - newFile bool // whether the file has not existed previously + file string + ini *ini.File + + disableSaving bool // disable the "Save" method because the config options could be polluted + loadedFromEmpty bool // whether the file has not existed previously } type iniConfigSection struct { @@ -175,53 +182,43 @@ func NewConfigProviderFromData(configContent string) (ConfigProvider, error) { } cfg.NameMapper = ini.SnackCase return &iniConfigProvider{ - ini: cfg, - newFile: true, + ini: cfg, + loadedFromEmpty: true, }, nil } -type Options struct { - CustomConf string // the ini file path - AllowEmpty bool // whether not finding configuration files is allowed - ExtraConfig string - - DisableLoadCommonSettings bool // only used by "Init()", not used by "NewConfigProvider()" -} - // NewConfigProviderFromFile load configuration from file. // NOTE: do not print any log except error. -func NewConfigProviderFromFile(opts *Options) (ConfigProvider, error) { - cfg := ini.Empty() - newFile := true +func NewConfigProviderFromFile(file string, extraConfigs ...string) (ConfigProvider, error) { + cfg := ini.Empty(ini.LoadOptions{KeyValueDelimiterOnWrite: " = "}) + loadedFromEmpty := true - if opts.CustomConf != "" { - isFile, err := util.IsFile(opts.CustomConf) + if file != "" { + isFile, err := util.IsFile(file) if err != nil { - return nil, fmt.Errorf("unable to check if %s is a file. Error: %v", opts.CustomConf, err) + return nil, fmt.Errorf("unable to check if %q is a file. Error: %v", file, err) } if isFile { - if err := cfg.Append(opts.CustomConf); err != nil { - return nil, fmt.Errorf("failed to load custom conf '%s': %v", opts.CustomConf, err) + if err = cfg.Append(file); err != nil { + return nil, fmt.Errorf("failed to load config file %q: %v", file, err) } - newFile = false + loadedFromEmpty = false } } - if newFile && !opts.AllowEmpty { - return nil, fmt.Errorf("unable to find configuration file: %q, please ensure you are running in the correct environment or set the correct configuration file with -c", CustomConf) - } - - if opts.ExtraConfig != "" { - if err := cfg.Append([]byte(opts.ExtraConfig)); err != nil { - return nil, fmt.Errorf("unable to append more config: %v", err) + if len(extraConfigs) > 0 { + for _, s := range extraConfigs { + if err := cfg.Append([]byte(s)); err != nil { + return nil, fmt.Errorf("unable to append more config: %v", err) + } } } cfg.NameMapper = ini.SnackCase return &iniConfigProvider{ - opts: opts, - ini: cfg, - newFile: newFile, + file: file, + ini: cfg, + loadedFromEmpty: loadedFromEmpty, }, nil } @@ -252,22 +249,24 @@ func (p *iniConfigProvider) GetSection(name string) (ConfigSection, error) { return &iniConfigSection{sec: sec}, nil } +var errDisableSaving = errors.New("this config can't be saved, developers should prepare a new config to save") + // Save saves the content into file func (p *iniConfigProvider) Save() error { - filename := p.opts.CustomConf + if p.disableSaving { + return errDisableSaving + } + filename := p.file if filename == "" { - if !p.opts.AllowEmpty { - return fmt.Errorf("custom config path must not be empty") - } - return nil + return fmt.Errorf("config file path must not be empty") } - if p.newFile { + if p.loadedFromEmpty { if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil { - return fmt.Errorf("failed to create '%s': %v", filename, err) + return fmt.Errorf("failed to create %q: %v", filename, err) } } if err := p.ini.SaveTo(filename); err != nil { - return fmt.Errorf("failed to save '%s': %v", filename, err) + return fmt.Errorf("failed to save %q: %v", filename, err) } // Change permissions to be more restrictive @@ -285,9 +284,32 @@ func (p *iniConfigProvider) Save() error { } func (p *iniConfigProvider) SaveTo(filename string) error { + if p.disableSaving { + return errDisableSaving + } return p.ini.SaveTo(filename) } +// DisableSaving disables the saving function, use PrepareSaving to get clear config options. +func (p *iniConfigProvider) DisableSaving() { + p.disableSaving = true +} + +// PrepareSaving loads the ini from file again to get clear config options. +// Otherwise, the "MustXxx" calls would have polluted the current config provider, +// it makes the "Save" outputs a lot of garbage options +// After the INI package gets refactored, no "MustXxx" pollution, this workaround can be dropped. +func (p *iniConfigProvider) PrepareSaving() (ConfigProvider, error) { + if p.file == "" { + return nil, errors.New("no config file to save") + } + return NewConfigProviderFromFile(p.file) +} + +func (p *iniConfigProvider) IsLoadedFromEmpty() bool { + return p.loadedFromEmpty +} + func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting any) { if err := rootCfg.Section(sectionName).MapTo(setting); err != nil { log.Fatal("Failed to map %s settings: %v", sectionName, err) @@ -324,8 +346,8 @@ func NewConfigProviderForLocale(source any, others ...any) (ConfigProvider, erro } iniFile.BlockMode = false return &iniConfigProvider{ - ini: iniFile, - newFile: true, + ini: iniFile, + loadedFromEmpty: true, }, nil } diff --git a/modules/setting/config_provider_test.go b/modules/setting/config_provider_test.go index 17650edea404c..7e7c6be2bb839 100644 --- a/modules/setting/config_provider_test.go +++ b/modules/setting/config_provider_test.go @@ -67,13 +67,14 @@ key = 123 } func TestNewConfigProviderFromFile(t *testing.T) { - _, err := NewConfigProviderFromFile(&Options{CustomConf: "no-such.ini", AllowEmpty: false}) - assert.ErrorContains(t, err, "unable to find configuration file") + cfg, err := NewConfigProviderFromFile("no-such.ini") + assert.NoError(t, err) + assert.True(t, cfg.IsLoadedFromEmpty()) // load non-existing file and save testFile := t.TempDir() + "/test.ini" testFile1 := t.TempDir() + "/test1.ini" - cfg, err := NewConfigProviderFromFile(&Options{CustomConf: testFile, AllowEmpty: true}) + cfg, err = NewConfigProviderFromFile(testFile) assert.NoError(t, err) sec, _ := cfg.NewSection("foo") @@ -84,14 +85,14 @@ func TestNewConfigProviderFromFile(t *testing.T) { bs, err := os.ReadFile(testFile) assert.NoError(t, err) - assert.Equal(t, "[foo]\nk1=a\n", string(bs)) + assert.Equal(t, "[foo]\nk1 = a\n", string(bs)) bs, err = os.ReadFile(testFile1) assert.NoError(t, err) - assert.Equal(t, "[foo]\nk1=a\nk2=b\n", string(bs)) + assert.Equal(t, "[foo]\nk1 = a\nk2 = b\n", string(bs)) // load existing file and save - cfg, err = NewConfigProviderFromFile(&Options{CustomConf: testFile, AllowEmpty: true}) + cfg, err = NewConfigProviderFromFile(testFile) assert.NoError(t, err) assert.Equal(t, "a", cfg.Section("foo").Key("k1").String()) sec, _ = cfg.NewSection("bar") @@ -99,7 +100,7 @@ func TestNewConfigProviderFromFile(t *testing.T) { assert.NoError(t, cfg.Save()) bs, err = os.ReadFile(testFile) assert.NoError(t, err) - assert.Equal(t, "[foo]\nk1=a\n\n[bar]\nk1=b\n", string(bs)) + assert.Equal(t, "[foo]\nk1 = a\n\n[bar]\nk1 = b\n", string(bs)) } func TestNewConfigProviderForLocale(t *testing.T) { @@ -119,3 +120,27 @@ func TestNewConfigProviderForLocale(t *testing.T) { assert.Equal(t, "foo", cfg.Section("").Key("k1").String()) assert.Equal(t, "xxx", cfg.Section("").Key("k2").String()) } + +func TestDisableSaving(t *testing.T) { + testFile := t.TempDir() + "/test.ini" + _ = os.WriteFile(testFile, []byte("k1=a\nk2=b"), 0o644) + cfg, err := NewConfigProviderFromFile(testFile) + assert.NoError(t, err) + + cfg.DisableSaving() + err = cfg.Save() + assert.ErrorIs(t, err, errDisableSaving) + + saveCfg, err := cfg.PrepareSaving() + assert.NoError(t, err) + + saveCfg.Section("").Key("k1").MustString("x") + saveCfg.Section("").Key("k2").SetValue("y") + saveCfg.Section("").Key("k3").SetValue("z") + err = saveCfg.Save() + assert.NoError(t, err) + + bs, err := os.ReadFile(testFile) + assert.NoError(t, err) + assert.Equal(t, "k1 = a\nk2 = y\nk3 = z\n", string(bs)) +} diff --git a/modules/setting/database.go b/modules/setting/database.go index 7a7c7029a430d..709655368c67b 100644 --- a/modules/setting/database.go +++ b/modules/setting/database.go @@ -12,8 +12,6 @@ import ( "path/filepath" "strings" "time" - - "code.gitea.io/gitea/modules/log" ) var ( @@ -36,7 +34,7 @@ var ( SSLMode string Path string LogSQL bool - Charset string + MysqlCharset string Timeout int // seconds SQLiteJournalMode string DBConnectRetries int @@ -60,11 +58,6 @@ func LoadDBSetting() { func loadDBSetting(rootCfg ConfigProvider) { sec := rootCfg.Section("database") Database.Type = DatabaseType(sec.Key("DB_TYPE").String()) - defaultCharset := "utf8" - - if Database.Type.IsMySQL() { - defaultCharset = "utf8mb4" - } Database.Host = sec.Key("HOST").String() Database.Name = sec.Key("NAME").String() @@ -74,10 +67,7 @@ func loadDBSetting(rootCfg ConfigProvider) { } Database.Schema = sec.Key("SCHEMA").String() Database.SSLMode = sec.Key("SSL_MODE").MustString("disable") - Database.Charset = sec.Key("CHARSET").In(defaultCharset, []string{"utf8", "utf8mb4"}) - if Database.Type.IsMySQL() && defaultCharset != "utf8mb4" { - log.Error("Deprecated database mysql charset utf8 support, please use utf8mb4 or convert utf8 to utf8mb4.") - } + Database.MysqlCharset = sec.Key("MYSQL_CHARSET").MustString("utf8mb4") // do not document it, end users won't need it. Database.Path = sec.Key("PATH").MustString(filepath.Join(AppDataPath, "gitea.db")) Database.Timeout = sec.Key("SQLITE_TIMEOUT").MustInt(500) @@ -101,9 +91,9 @@ func loadDBSetting(rootCfg ConfigProvider) { // DBConnStr returns database connection string func DBConnStr() (string, error) { var connStr string - Param := "?" - if strings.Contains(Database.Name, Param) { - Param = "&" + paramSep := "?" + if strings.Contains(Database.Name, paramSep) { + paramSep = "&" } switch Database.Type { case "mysql": @@ -116,15 +106,15 @@ func DBConnStr() (string, error) { tls = "false" } connStr = fmt.Sprintf("%s:%s@%s(%s)/%s%scharset=%s&parseTime=true&tls=%s", - Database.User, Database.Passwd, connType, Database.Host, Database.Name, Param, Database.Charset, tls) + Database.User, Database.Passwd, connType, Database.Host, Database.Name, paramSep, Database.MysqlCharset, tls) case "postgres": - connStr = getPostgreSQLConnectionString(Database.Host, Database.User, Database.Passwd, Database.Name, Param, Database.SSLMode) + connStr = getPostgreSQLConnectionString(Database.Host, Database.User, Database.Passwd, Database.Name, paramSep, Database.SSLMode) case "mssql": host, port := ParseMSSQLHostPort(Database.Host) connStr = fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", host, port, Database.Name, Database.User, Database.Passwd) case "sqlite3": if !EnableSQLite3 { - return "", errors.New("this binary version does not build support for SQLite3") + return "", errors.New("this Gitea binary was not built with SQLite3 support") } if err := os.MkdirAll(path.Dir(Database.Path), os.ModePerm); err != nil { return "", fmt.Errorf("Failed to create directories: %w", err) @@ -136,7 +126,7 @@ func DBConnStr() (string, error) { connStr = fmt.Sprintf("file:%s?cache=shared&mode=rwc&_busy_timeout=%d&_txlock=immediate%s", Database.Path, Database.Timeout, journalMode) default: - return "", fmt.Errorf("Unknown database type: %s", Database.Type) + return "", fmt.Errorf("unknown database type: %s", Database.Type) } return connStr, nil diff --git a/modules/setting/lfs.go b/modules/setting/lfs.go index d68349be86114..140a96f9eda8c 100644 --- a/modules/setting/lfs.go +++ b/modules/setting/lfs.go @@ -59,13 +59,18 @@ func loadLFSFrom(rootCfg ConfigProvider) error { if err != nil || n != 32 { LFS.JWTSecretBase64, err = generate.NewJwtSecretBase64() if err != nil { - return fmt.Errorf("Error generating JWT Secret for custom config: %v", err) + return fmt.Errorf("error generating JWT Secret for custom config: %v", err) } // 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) + saveCfg, err := rootCfg.PrepareSaving() + if err != nil { + return fmt.Errorf("error saving JWT Secret for custom config: %v", err) + } + rootCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64) + saveCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64) + if err := saveCfg.Save(); err != nil { + return fmt.Errorf("error saving JWT Secret for custom config: %v", err) } } diff --git a/modules/setting/mirror.go b/modules/setting/mirror.go index cd6b8d456248d..3aa530a1f4847 100644 --- a/modules/setting/mirror.go +++ b/modules/setting/mirror.go @@ -30,7 +30,7 @@ func loadMirrorFrom(rootCfg ConfigProvider) { // 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 deprecatedSetting(rootCfg, "repository", "DISABLE_MIRRORS", "mirror", "ENABLED", "v1.19.0") - if rootCfg.Section("repository").Key("DISABLE_MIRRORS").MustBool(false) { + if ConfigSectionKeyBool(rootCfg.Section("repository"), "DISABLE_MIRRORS") { Mirror.DisableNewPull = true } diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go index 4dab468c1029c..83c607a416a9c 100644 --- a/modules/setting/oauth2.go +++ b/modules/setting/oauth2.go @@ -120,18 +120,25 @@ func loadOAuth2From(rootCfg ConfigProvider) { OAuth2.JWTSigningPrivateKeyFile = filepath.Join(AppDataPath, OAuth2.JWTSigningPrivateKeyFile) } - key := make([]byte, 32) - n, err := base64.RawURLEncoding.Decode(key, []byte(OAuth2.JWTSecretBase64)) - if err != nil || n != 32 { - key, err = generate.NewJwtSecret() - if err != nil { - log.Fatal("error generating JWT secret: %v", err) - } - - secretBase64 := base64.RawURLEncoding.EncodeToString(key) - rootCfg.Section("oauth2").Key("JWT_SECRET").SetValue(secretBase64) - if err := rootCfg.Save(); err != nil { - log.Fatal("save oauth2.JWT_SECRET failed: %v", err) + if InstallLock { + key := make([]byte, 32) + n, err := base64.RawURLEncoding.Decode(key, []byte(OAuth2.JWTSecretBase64)) + if err != nil || n != 32 { + key, err = generate.NewJwtSecret() + if err != nil { + log.Fatal("error generating JWT secret: %v", err) + } + + secretBase64 := base64.RawURLEncoding.EncodeToString(key) + saveCfg, err := rootCfg.PrepareSaving() + if err != nil { + log.Fatal("save oauth2.JWT_SECRET failed: %v", err) + } + rootCfg.Section("oauth2").Key("JWT_SECRET").SetValue(secretBase64) + saveCfg.Section("oauth2").Key("JWT_SECRET").SetValue(secretBase64) + if err := saveCfg.Save(); err != nil { + log.Fatal("save oauth2.JWT_SECRET failed: %v", err) + } } } } diff --git a/modules/setting/path.go b/modules/setting/path.go new file mode 100644 index 0000000000000..91bb2e9bb777d --- /dev/null +++ b/modules/setting/path.go @@ -0,0 +1,191 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "errors" + "os" + "os/exec" + "path/filepath" + "strings" + + "code.gitea.io/gitea/modules/log" +) + +var ( + // AppPath represents the path to the gitea binary + AppPath string + + // AppWorkPath is the "working directory" of Gitea. It maps to the environment variable GITEA_WORK_DIR. + // If that is not set it is the default set here by the linker or failing that the directory of AppPath. + // It is used as the base path for several other paths. + AppWorkPath string + CustomPath string // Custom directory path. Env: GITEA_CUSTOM + CustomConf string + + appWorkPathBuiltin string + customPathBuiltin string + customConfBuiltin string + + AppWorkPathMismatch bool +) + +func getAppPath() (string, error) { + var appPath string + var err error + if IsWindows && filepath.IsAbs(os.Args[0]) { + appPath = filepath.Clean(os.Args[0]) + } else { + appPath, err = exec.LookPath(os.Args[0]) + } + if err != nil { + if !errors.Is(err, exec.ErrDot) { + return "", err + } + appPath, err = filepath.Abs(os.Args[0]) + } + if err != nil { + return "", err + } + appPath, err = filepath.Abs(appPath) + if err != nil { + return "", err + } + // Note: (legacy code) we don't use path.Dir here because it does not handle case which path starts with two "/" in Windows: "//psf/Home/..." + return strings.ReplaceAll(appPath, "\\", "/"), err +} + +func init() { + var err error + if AppPath, err = getAppPath(); err != nil { + log.Fatal("Failed to get app path: %v", err) + } + + if AppWorkPath == "" { + AppWorkPath = filepath.Dir(AppPath) + } + + appWorkPathBuiltin = AppWorkPath + customPathBuiltin = CustomPath + customConfBuiltin = CustomConf +} + +type ArgWorkPathAndCustomConf struct { + WorkPath string + CustomPath string + CustomConf string +} + +type stringWithDefault struct { + Value string + IsSet bool +} + +func (s *stringWithDefault) Set(v string) { + s.Value = v + s.IsSet = true +} + +// InitWorkPathAndCommonConfig will set AppWorkPath, CustomPath and CustomConf, init default config provider by CustomConf and load common settings, +func InitWorkPathAndCommonConfig(getEnvFn func(name string) string, args ArgWorkPathAndCustomConf) { + tryAbsPath := func(paths ...string) string { + s := paths[len(paths)-1] + for i := len(paths) - 2; i >= 0; i-- { + if filepath.IsAbs(s) { + break + } + s = filepath.Join(paths[i], s) + } + return s + } + + var err error + tmpWorkPath := stringWithDefault{Value: appWorkPathBuiltin} + if tmpWorkPath.Value == "" { + tmpWorkPath.Value = filepath.Dir(AppPath) + } + tmpCustomPath := stringWithDefault{Value: customPathBuiltin} + if tmpCustomPath.Value == "" { + tmpCustomPath.Value = "custom" + } + tmpCustomConf := stringWithDefault{Value: customConfBuiltin} + if tmpCustomConf.Value == "" { + tmpCustomConf.Value = "conf/app.ini" + } + + readFromEnv := func() { + envWorkPath := getEnvFn("GITEA_WORK_DIR") + if envWorkPath != "" { + tmpWorkPath.Set(envWorkPath) + if !filepath.IsAbs(tmpWorkPath.Value) { + log.Fatal("GITEA_WORK_DIR (work path) must be absolute path") + } + } + + envCustomPath := getEnvFn("GITEA_CUSTOM") + if envCustomPath != "" { + tmpCustomPath.Set(envCustomPath) + if !filepath.IsAbs(tmpCustomPath.Value) { + log.Fatal("GITEA_CUSTOM (custom path) must be absolute path") + } + } + } + + readFromArgs := func() { + if args.WorkPath != "" { + tmpWorkPath.Set(args.WorkPath) + if !filepath.IsAbs(tmpWorkPath.Value) { + log.Fatal("--work-path must be absolute path") + } + } + if args.CustomPath != "" { + tmpCustomPath.Set(args.CustomPath) // if it is not abs, it will be based on work-path, it shouldn't happen + if !filepath.IsAbs(tmpCustomPath.Value) { + log.Error("--custom-path must be absolute path") + } + } + if args.CustomConf != "" { + tmpCustomConf.Set(args.CustomConf) + if !filepath.IsAbs(tmpCustomConf.Value) { + // the config path can be relative to the real current working path + if tmpCustomConf.Value, err = filepath.Abs(tmpCustomConf.Value); err != nil { + log.Fatal("Failed to get absolute path of config %q: %v", tmpCustomConf.Value, err) + } + } + } + } + + readFromEnv() + readFromArgs() + + if !tmpCustomConf.IsSet { + tmpCustomConf.Set(tryAbsPath(tmpWorkPath.Value, tmpCustomPath.Value, tmpCustomConf.Value)) + } + + // only read the config but do not load/init anything more, because the AppWorkPath and CustomPath are not ready + InitCfgProvider(tmpCustomConf.Value) + configWorkPath := ConfigSectionKeyString(CfgProvider.Section(""), "WORK_PATH") + if configWorkPath != "" { + if !filepath.IsAbs(configWorkPath) { + log.Fatal("WORK_PATH in %q must be absolute path", configWorkPath) + } + configWorkPath = filepath.Clean(configWorkPath) + if tmpWorkPath.Value != "" && (getEnvFn("GITEA_WORK_DIR") != "" || args.WorkPath != "") { + fi1, err1 := os.Stat(tmpWorkPath.Value) + fi2, err2 := os.Stat(configWorkPath) + if err1 != nil || err2 != nil || !os.SameFile(fi1, fi2) { + AppWorkPathMismatch = true + } + } + tmpWorkPath.Set(configWorkPath) + } + + tmpCustomPath.Set(tryAbsPath(tmpWorkPath.Value, tmpCustomPath.Value)) + + AppWorkPath = tmpWorkPath.Value + CustomPath = tmpCustomPath.Value + CustomConf = tmpCustomConf.Value + + LoadCommonSettings() +} diff --git a/modules/setting/path_test.go b/modules/setting/path_test.go new file mode 100644 index 0000000000000..fc6a2116dc93a --- /dev/null +++ b/modules/setting/path_test.go @@ -0,0 +1,151 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +type envVars map[string]string + +func (e envVars) Getenv(key string) string { + return e[key] +} + +func TestInitWorkPathAndCommonConfig(t *testing.T) { + testInit := func(defaultWorkPath, defaultCustomPath, defaultCustomConf string) { + AppWorkPathMismatch = false + AppWorkPath = defaultWorkPath + appWorkPathBuiltin = defaultWorkPath + CustomPath = defaultCustomPath + customPathBuiltin = defaultCustomPath + CustomConf = defaultCustomConf + customConfBuiltin = defaultCustomConf + } + + fp := filepath.Join + + tmpDir := t.TempDir() + dirFoo := fp(tmpDir, "foo") + dirBar := fp(tmpDir, "bar") + dirXxx := fp(tmpDir, "xxx") + dirYyy := fp(tmpDir, "yyy") + + t.Run("Default", func(t *testing.T) { + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{}) + assert.Equal(t, dirFoo, AppWorkPath) + assert.Equal(t, fp(dirFoo, "custom"), CustomPath) + assert.Equal(t, fp(dirFoo, "custom/conf/app.ini"), CustomConf) + }) + + t.Run("WorkDir(env)", func(t *testing.T) { + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirBar}.Getenv, ArgWorkPathAndCustomConf{}) + assert.Equal(t, dirBar, AppWorkPath) + assert.Equal(t, fp(dirBar, "custom"), CustomPath) + assert.Equal(t, fp(dirBar, "custom/conf/app.ini"), CustomConf) + }) + + t.Run("WorkDir(env,arg)", func(t *testing.T) { + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirBar}.Getenv, ArgWorkPathAndCustomConf{WorkPath: dirXxx}) + assert.Equal(t, dirXxx, AppWorkPath) + assert.Equal(t, fp(dirXxx, "custom"), CustomPath) + assert.Equal(t, fp(dirXxx, "custom/conf/app.ini"), CustomConf) + }) + + t.Run("CustomPath(env)", func(t *testing.T) { + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{"GITEA_CUSTOM": fp(dirBar, "custom1")}.Getenv, ArgWorkPathAndCustomConf{}) + assert.Equal(t, dirFoo, AppWorkPath) + assert.Equal(t, fp(dirBar, "custom1"), CustomPath) + assert.Equal(t, fp(dirBar, "custom1/conf/app.ini"), CustomConf) + }) + + t.Run("CustomPath(env,arg)", func(t *testing.T) { + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{"GITEA_CUSTOM": fp(dirBar, "custom1")}.Getenv, ArgWorkPathAndCustomConf{CustomPath: "custom2"}) + assert.Equal(t, dirFoo, AppWorkPath) + assert.Equal(t, fp(dirFoo, "custom2"), CustomPath) + assert.Equal(t, fp(dirFoo, "custom2/conf/app.ini"), CustomConf) + }) + + t.Run("CustomConf", func(t *testing.T) { + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: "app1.ini"}) + assert.Equal(t, dirFoo, AppWorkPath) + cwd, _ := os.Getwd() + assert.Equal(t, fp(cwd, "app1.ini"), CustomConf) + + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: fp(dirBar, "app1.ini")}) + assert.Equal(t, dirFoo, AppWorkPath) + assert.Equal(t, fp(dirBar, "app1.ini"), CustomConf) + }) + + t.Run("CustomConfOverrideWorkPath", func(t *testing.T) { + iniWorkPath := fp(tmpDir, "app-workpath.ini") + _ = os.WriteFile(iniWorkPath, []byte("WORK_PATH="+dirXxx), 0o644) + + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: iniWorkPath}) + assert.Equal(t, dirXxx, AppWorkPath) + assert.Equal(t, fp(dirXxx, "custom"), CustomPath) + assert.Equal(t, iniWorkPath, CustomConf) + assert.False(t, AppWorkPathMismatch) + + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirBar}.Getenv, ArgWorkPathAndCustomConf{CustomConf: iniWorkPath}) + assert.Equal(t, dirXxx, AppWorkPath) + assert.Equal(t, fp(dirXxx, "custom"), CustomPath) + assert.Equal(t, iniWorkPath, CustomConf) + assert.True(t, AppWorkPathMismatch) + + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{WorkPath: dirBar, CustomConf: iniWorkPath}) + assert.Equal(t, dirXxx, AppWorkPath) + assert.Equal(t, fp(dirXxx, "custom"), CustomPath) + assert.Equal(t, iniWorkPath, CustomConf) + assert.True(t, AppWorkPathMismatch) + }) + + t.Run("Builtin", func(t *testing.T) { + testInit(dirFoo, dirBar, dirXxx) + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{}) + assert.Equal(t, dirFoo, AppWorkPath) + assert.Equal(t, dirBar, CustomPath) + assert.Equal(t, dirXxx, CustomConf) + + testInit(dirFoo, "custom1", "cfg.ini") + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{}) + assert.Equal(t, dirFoo, AppWorkPath) + assert.Equal(t, fp(dirFoo, "custom1"), CustomPath) + assert.Equal(t, fp(dirFoo, "custom1/cfg.ini"), CustomConf) + + testInit(dirFoo, "custom1", "cfg.ini") + InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirYyy}.Getenv, ArgWorkPathAndCustomConf{}) + assert.Equal(t, dirYyy, AppWorkPath) + assert.Equal(t, fp(dirYyy, "custom1"), CustomPath) + assert.Equal(t, fp(dirYyy, "custom1/cfg.ini"), CustomConf) + + testInit(dirFoo, "custom1", "cfg.ini") + InitWorkPathAndCommonConfig(envVars{"GITEA_CUSTOM": dirYyy}.Getenv, ArgWorkPathAndCustomConf{}) + assert.Equal(t, dirFoo, AppWorkPath) + assert.Equal(t, dirYyy, CustomPath) + assert.Equal(t, fp(dirYyy, "cfg.ini"), CustomConf) + + iniWorkPath := fp(tmpDir, "app-workpath.ini") + _ = os.WriteFile(iniWorkPath, []byte("WORK_PATH="+dirXxx), 0o644) + testInit(dirFoo, "custom1", "cfg.ini") + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: iniWorkPath}) + assert.Equal(t, dirXxx, AppWorkPath) + assert.Equal(t, fp(dirXxx, "custom1"), CustomPath) + assert.Equal(t, iniWorkPath, CustomConf) + }) +} diff --git a/modules/setting/security.go b/modules/setting/security.go index ce2e7711f1e7e..c39eb7f3ebd6a 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -89,8 +89,13 @@ func generateSaveInternalToken(rootCfg ConfigProvider) { } InternalToken = token + saveCfg, err := rootCfg.PrepareSaving() + if err != nil { + log.Fatal("Error saving internal token: %v", err) + } rootCfg.Section("security").Key("INTERNAL_TOKEN").SetValue(token) - if err := rootCfg.Save(); err != nil { + saveCfg.Section("security").Key("INTERNAL_TOKEN").SetValue(token) + if err = saveCfg.Save(); err != nil { log.Fatal("Error saving internal token: %v", err) } } diff --git a/modules/setting/server.go b/modules/setting/server.go index d937faca1012b..7c033bcc6ba79 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -61,6 +61,7 @@ var ( AssetVersion string // Server settings + Protocol Scheme UseProxyProtocol bool // `ini:"USE_PROXY_PROTOCOL"` ProxyProtocolTLSBridging bool //`ini:"PROXY_PROTOCOL_TLS_BRIDGING"` @@ -324,7 +325,6 @@ func loadServerFrom(rootCfg ConfigProvider) { StaticCacheTime = sec.Key("STATIC_CACHE_TIME").MustDuration(6 * time.Hour) AppDataPath = sec.Key("APP_DATA_PATH").MustString(path.Join(AppWorkPath, "data")) if !filepath.IsAbs(AppDataPath) { - log.Info("The provided APP_DATA_PATH: %s is not absolute - it will be made absolute against the work path: %s", AppDataPath, AppWorkPath) AppDataPath = filepath.ToSlash(filepath.Join(AppWorkPath, AppDataPath)) } diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 293333a95bc36..0d69847dbeab2 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -5,12 +5,8 @@ package setting import ( - "errors" "fmt" "os" - "os/exec" - "path" - "path/filepath" "runtime" "strings" "time" @@ -28,19 +24,9 @@ var ( // AppStartTime store time gitea has started AppStartTime time.Time - // AppPath represents the path to the gitea binary - AppPath string - // AppWorkPath is the "working directory" of Gitea. It maps to the environment variable GITEA_WORK_DIR. - // If that is not set it is the default set here by the linker or failing that the directory of AppPath. - // - // AppWorkPath is used as the base path for several other paths. - AppWorkPath string - // Other global setting objects CfgProvider ConfigProvider - CustomPath string // Custom directory path - CustomConf string RunMode string RunUser string IsProd bool @@ -51,62 +37,6 @@ var ( IsInTesting = false ) -func getAppPath() (string, error) { - var appPath string - var err error - if IsWindows && filepath.IsAbs(os.Args[0]) { - appPath = filepath.Clean(os.Args[0]) - } else { - appPath, err = exec.LookPath(os.Args[0]) - } - - if err != nil { - if !errors.Is(err, exec.ErrDot) { - return "", err - } - appPath, err = filepath.Abs(os.Args[0]) - } - if err != nil { - return "", err - } - appPath, err = filepath.Abs(appPath) - if err != nil { - return "", err - } - // Note: we don't use path.Dir here because it does not handle case - // which path starts with two "/" in Windows: "//psf/Home/..." - return strings.ReplaceAll(appPath, "\\", "/"), err -} - -func getWorkPath(appPath string) string { - workPath := AppWorkPath - - if giteaWorkPath, ok := os.LookupEnv("GITEA_WORK_DIR"); ok { - workPath = giteaWorkPath - } - if len(workPath) == 0 { - i := strings.LastIndex(appPath, "/") - if i == -1 { - workPath = appPath - } else { - workPath = appPath[:i] - } - } - workPath = strings.ReplaceAll(workPath, "\\", "/") - if !filepath.IsAbs(workPath) { - log.Info("Provided work path %s is not absolute - will be made absolute against the current working directory", workPath) - - absPath, err := filepath.Abs(workPath) - if err != nil { - log.Error("Unable to absolute %s against the current working directory %v. Will absolute against the AppPath %s", workPath, err, appPath) - workPath = filepath.Join(appPath, workPath) - } else { - workPath = absPath - } - } - return strings.ReplaceAll(workPath, "\\", "/") -} - func init() { IsWindows = runtime.GOOS == "windows" if AppVer == "" { @@ -116,12 +46,6 @@ func init() { // We can rely on log.CanColorStdout being set properly because modules/log/console_windows.go comes before modules/setting/setting.go lexicographically // By default set this logger at Info - we'll change it later, but we need to start with something. log.SetConsoleLogger(log.DEFAULT, "console", log.INFO) - - var err error - if AppPath, err = getAppPath(); err != nil { - log.Fatal("Failed to get app path: %v", err) - } - AppWorkPath = getWorkPath(AppPath) } // IsRunUserMatchCurrentUser returns false if configured run user does not match @@ -137,36 +61,6 @@ func IsRunUserMatchCurrentUser(runUser string) (string, bool) { return currentUser, runUser == currentUser } -// SetCustomPathAndConf will set CustomPath and CustomConf with reference to the -// GITEA_CUSTOM environment variable and with provided overrides before stepping -// back to the default -func SetCustomPathAndConf(providedCustom, providedConf, providedWorkPath string) { - if len(providedWorkPath) != 0 { - AppWorkPath = filepath.ToSlash(providedWorkPath) - } - if giteaCustom, ok := os.LookupEnv("GITEA_CUSTOM"); ok { - CustomPath = giteaCustom - } - if len(providedCustom) != 0 { - CustomPath = providedCustom - } - if len(CustomPath) == 0 { - CustomPath = path.Join(AppWorkPath, "custom") - } else if !filepath.IsAbs(CustomPath) { - CustomPath = path.Join(AppWorkPath, CustomPath) - } - - if len(providedConf) != 0 { - CustomConf = providedConf - } - if len(CustomConf) == 0 { - CustomConf = path.Join(CustomPath, "conf/app.ini") - } else if !filepath.IsAbs(CustomConf) { - CustomConf = path.Join(CustomPath, CustomConf) - log.Warn("Using 'custom' directory as relative origin for configuration file: '%s'", CustomConf) - } -} - // PrepareAppDataPath creates app data directory if necessary func PrepareAppDataPath() error { // FIXME: There are too many calls to MkdirAll in old code. It is incorrect. @@ -196,25 +90,29 @@ func PrepareAppDataPath() error { return nil } -func Init(opts *Options) { - if opts.CustomConf == "" { - opts.CustomConf = CustomConf - } +func InitCfgProvider(file string, extraConfigs ...string) { var err error - CfgProvider, err = NewConfigProviderFromFile(opts) - if err != nil { - log.Fatal("newConfigProviderFromFile[%v]: %v", opts, err) + if CfgProvider, err = NewConfigProviderFromFile(file, extraConfigs...); err != nil { + log.Fatal("Unable to init config provider from %q: %v", file, err) } - if !opts.DisableLoadCommonSettings { - if err := loadCommonSettingsFrom(CfgProvider); err != nil { - log.Fatal("loadCommonSettingsFrom[%v]: %v", opts, err) - } + CfgProvider.DisableSaving() // do not allow saving the CfgProvider into file, it will be polluted by the "MustXxx" calls +} + +func MustInstalled() { + if !InstallLock { + log.Fatal(`Unable to load config file for a installed Gitea instance, you should either use "--config" to set your config file (app.ini), or run "gitea web" command to install Gitea.`) + } +} + +func LoadCommonSettings() { + if err := loadCommonSettingsFrom(CfgProvider); err != nil { + log.Fatal("Unable to load settings from config: %v", err) } } // loadCommonSettingsFrom loads common configurations from a configuration provider. func loadCommonSettingsFrom(cfg ConfigProvider) error { - // WARNNING: don't change the sequence except you know what you are doing. + // WARNING: don't change the sequence except you know what you are doing. loadRunModeFrom(cfg) loadLogGlobalFrom(cfg) loadServerFrom(cfg) @@ -262,7 +160,7 @@ func loadRunModeFrom(rootCfg ConfigProvider) { RunUser = rootSec.Key("RUN_USER").MustString(user.CurrentUsername()) // The following is a purposefully undocumented option. Please do not run Gitea as root. It will only cause future headaches. // Please don't use root as a bandaid to "fix" something that is broken, instead the broken thing should instead be fixed properly. - unsafeAllowRunAsRoot := rootSec.Key("I_AM_BEING_UNSAFE_RUNNING_AS_ROOT").MustBool(false) + unsafeAllowRunAsRoot := ConfigSectionKeyBool(rootSec, "I_AM_BEING_UNSAFE_RUNNING_AS_ROOT") RunMode = os.Getenv("GITEA_RUN_MODE") if RunMode == "" { RunMode = rootSec.Key("RUN_MODE").MustString("prod") diff --git a/modules/ssh/ssh.go b/modules/ssh/ssh.go index 4bf57eafb721f..923fa51d228a7 100644 --- a/modules/ssh/ssh.go +++ b/modules/ssh/ssh.go @@ -68,7 +68,7 @@ func sessionHandler(session ssh.Session) { log.Trace("SSH: Payload: %v", command) - args := []string{"serv", "key-" + keyID, "--config=" + setting.CustomConf} + args := []string{"--config=" + setting.CustomConf, "serv", "key-" + keyID} log.Trace("SSH: Arguments: %v", args) ctx, cancel := context.WithCancel(session.Context()) diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index a26c0531f81d1..d23103ce1bc56 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -81,16 +81,16 @@ func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefa // RenderCommitBody extracts the body of a commit message without its title. func RenderCommitBody(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML { - msgLine := strings.TrimRightFunc(msg, unicode.IsSpace) + msgLine := strings.TrimSpace(msg) lineEnd := strings.IndexByte(msgLine, '\n') if lineEnd > 0 { msgLine = msgLine[lineEnd+1:] } else { - return template.HTML("") + return "" } msgLine = strings.TrimLeftFunc(msgLine, unicode.IsSpace) if len(msgLine) == 0 { - return template.HTML("") + return "" } renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go new file mode 100644 index 0000000000000..29d3ed3a56c4e --- /dev/null +++ b/modules/templates/util_render_test.go @@ -0,0 +1,56 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package templates + +import ( + "context" + "html/template" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRenderCommitBody(t *testing.T) { + type args struct { + ctx context.Context + msg string + urlPrefix string + metas map[string]string + } + tests := []struct { + name string + args args + want template.HTML + }{ + { + name: "multiple lines", + args: args{ + ctx: context.Background(), + msg: "first line\nsecond line", + }, + want: "second line", + }, + { + name: "multiple lines with leading newlines", + args: args{ + ctx: context.Background(), + msg: "\n\n\n\nfirst line\nsecond line", + }, + want: "second line", + }, + { + name: "multiple lines with trailing newlines", + args: args{ + ctx: context.Background(), + msg: "first line\nsecond line\n\n\n", + }, + want: "second line", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, RenderCommitBody(tt.args.ctx, tt.args.msg, tt.args.urlPrefix, tt.args.metas), "RenderCommitBody(%v, %v, %v, %v)", tt.args.ctx, tt.args.msg, tt.args.urlPrefix, tt.args.metas) + }) + } +} diff --git a/modules/test/context_tests.go b/modules/test/context_tests.go index 349c7e3e8045f..cf8af32fc69ef 100644 --- a/modules/test/context_tests.go +++ b/modules/test/context_tests.go @@ -9,6 +9,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "strings" "testing" access_model "code.gitea.io/gitea/models/perm/access" @@ -25,19 +26,26 @@ import ( "github.com/stretchr/testify/assert" ) -// MockContext mock context for unit tests -// TODO: move this function to other packages, because it depends on "models" package -func MockContext(t *testing.T, path string) *context.Context { - resp := httptest.NewRecorder() +func mockRequest(t *testing.T, reqPath string) *http.Request { + method, path, found := strings.Cut(reqPath, " ") + if !found { + method = "GET" + path = reqPath + } requestURL, err := url.Parse(path) assert.NoError(t, err) - req := &http.Request{ - URL: requestURL, - Form: url.Values{}, - } + req := &http.Request{Method: method, URL: requestURL, Form: url.Values{}} + req = req.WithContext(middleware.WithContextData(req.Context())) + 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.Data = middleware.ContextData{} + base.Data = middleware.GetContextData(req.Context()) base.Locale = &translation.MockLocale{} ctx := &context.Context{ Base: base, @@ -48,29 +56,23 @@ func MockContext(t *testing.T, path string) *context.Context { chiCtx := chi.NewRouteContext() ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx) - return ctx + return ctx, resp } // MockAPIContext mock context for unit tests // 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() - requestURL, err := url.Parse(path) - assert.NoError(t, err) - req := &http.Request{ - URL: requestURL, - Form: url.Values{}, - } - + req := mockRequest(t, reqPath) base, baseCleanUp := context.NewBaseContext(resp, req) - base.Data = middleware.ContextData{} + base.Data = middleware.GetContextData(req.Context()) base.Locale = &translation.MockLocale{} ctx := &context.APIContext{Base: base} _ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later chiCtx := chi.NewRouteContext() ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx) - return ctx + return ctx, resp } // LoadRepo load a repo into a test context. diff --git a/modules/testlogger/testlogger.go b/modules/testlogger/testlogger.go index b4275e6005706..6a0cee4a2998b 100644 --- a/modules/testlogger/testlogger.go +++ b/modules/testlogger/testlogger.go @@ -90,10 +90,11 @@ func (w *testLoggerWriterCloser) Reset() { // PrintCurrentTest prints the current test to os.Stdout func PrintCurrentTest(t testing.TB, skip ...int) func() { + t.Helper() start := time.Now() actualSkip := 1 if len(skip) > 0 { - actualSkip = skip[0] + actualSkip = skip[0] + 1 } _, filename, line, _ := runtime.Caller(actualSkip) diff --git a/modules/util/path.go b/modules/util/path.go index 1a68bc748802d..58258560dded3 100644 --- a/modules/util/path.go +++ b/modules/util/path.go @@ -222,6 +222,8 @@ func isOSWindows() bool { return runtime.GOOS == "windows" } +var driveLetterRegexp = regexp.MustCompile("/[A-Za-z]:/") + // FileURLToPath extracts the path information from a file://... url. func FileURLToPath(u *url.URL) (string, error) { if u.Scheme != "file" { @@ -235,8 +237,7 @@ func FileURLToPath(u *url.URL) (string, error) { } // If it looks like there's a Windows drive letter at the beginning, strip off the leading slash. - re := regexp.MustCompile("/[A-Za-z]:/") - if re.MatchString(path) { + if driveLetterRegexp.MatchString(path) { return path[1:], nil } return path, nil diff --git a/modules/web/handler.go b/modules/web/handler.go index c8aebd90518df..26b7428016c41 100644 --- a/modules/web/handler.go +++ b/modules/web/handler.go @@ -9,25 +9,15 @@ import ( "net/http" "reflect" - "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "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 -type ResponseStatusProvider interface { - Written() bool -} - -// TODO: decouple this from the context package, let the context package register these providers -var argTypeProvider = map[reflect.Type]func(req *http.Request) ResponseStatusProvider{ - 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) }, -} +var responseStatusProviders = map[reflect.Type]func(req *http.Request) types.ResponseStatusProvider{} -func RegisterHandleTypeProvider[T any](fn func(req *http.Request) ResponseStatusProvider) { - argTypeProvider[reflect.TypeOf((*T)(nil)).Elem()] = fn +func RegisterResponseStatusProvider[T any](fn func(req *http.Request) types.ResponseStatusProvider) { + responseStatusProviders[reflect.TypeOf((*T)(nil)).Elem()] = fn } // responseWriter is a wrapper of http.ResponseWriter, to check whether the response has been written @@ -36,10 +26,10 @@ type responseWriter struct { status int } -var _ ResponseStatusProvider = (*responseWriter)(nil) +var _ types.ResponseStatusProvider = (*responseWriter)(nil) -func (r *responseWriter) Written() bool { - return r.status > 0 +func (r *responseWriter) WrittenStatus() int { + return r.status } func (r *responseWriter) Header() http.Header { @@ -68,7 +58,7 @@ var ( func preCheckHandler(fn reflect.Value, argsIn []reflect.Value) { hasStatusProvider := false for _, argIn := range argsIn { - if _, hasStatusProvider = argIn.Interface().(ResponseStatusProvider); hasStatusProvider { + if _, hasStatusProvider = argIn.Interface().(types.ResponseStatusProvider); hasStatusProvider { break } } @@ -101,7 +91,7 @@ func prepareHandleArgsIn(resp http.ResponseWriter, req *http.Request, fn reflect case httpReqType: argsIn[i] = reflect.ValueOf(req) default: - if argFn, ok := argTypeProvider[argTyp]; ok { + if argFn, ok := responseStatusProviders[argTyp]; ok { if isPreCheck { argsIn[i] = reflect.ValueOf(&responseWriter{}) } else { @@ -129,8 +119,8 @@ func handleResponse(fn reflect.Value, ret []reflect.Value) goctx.CancelFunc { func hasResponseBeenWritten(argsIn []reflect.Value) bool { for _, argIn := range argsIn { - if statusProvider, ok := argIn.Interface().(ResponseStatusProvider); ok { - if statusProvider.Written() { + if statusProvider, ok := argIn.Interface().(types.ResponseStatusProvider); ok { + if statusProvider.WrittenStatus() != 0 { 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) { // wrap the response writer to check whether the response has been written resp := respOrig - if _, ok := resp.(ResponseStatusProvider); !ok { + if _, ok := resp.(types.ResponseStatusProvider); !ok { resp = &responseWriter{respWriter: resp} } diff --git a/modules/web/middleware/data.go b/modules/web/middleware/data.go index c1f0516d7d2f7..9497dc02e23d3 100644 --- a/modules/web/middleware/data.go +++ b/modules/web/middleware/data.go @@ -17,7 +17,7 @@ type ContextDataStore interface { type ContextData map[string]any -func (ds ContextData) GetData() map[string]any { +func (ds ContextData) GetData() ContextData { return ds } diff --git a/modules/web/route.go b/modules/web/route.go index d801f1025c945..db511aeb185e9 100644 --- a/modules/web/route.go +++ b/modules/web/route.go @@ -7,31 +7,31 @@ import ( "net/http" "strings" - "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/web/middleware" "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 -func Bind[T any](_ T) any { - return func(ctx *context.Context) { +// Bind binding an obj to a handler's context data +func Bind[T any](_ T) http.HandlerFunc { + return func(resp http.ResponseWriter, req *http.Request) { theObj := new(T) // create a new form obj for every request but not use obj directly - binding.Bind(ctx.Req, theObj) - SetForm(ctx, theObj) - middleware.AssignForm(theObj, ctx.Data) + data := middleware.GetContextData(req.Context()) + binding.Bind(req, theObj) + SetForm(data, theObj) + middleware.AssignForm(theObj, data) } } // SetForm set the form object -func SetForm(data middleware.ContextDataStore, obj interface{}) { - data.GetData()["__form"] = obj +func SetForm(dataStore middleware.ContextDataStore, obj interface{}) { + dataStore.GetData()["__form"] = obj } // GetForm returns the validate form information -func GetForm(data middleware.ContextDataStore) interface{} { - return data.GetData()["__form"] +func GetForm(dataStore middleware.ContextDataStore) interface{} { + return dataStore.GetData()["__form"] } // Route defines a route based on chi's router diff --git a/modules/web/routing/logger.go b/modules/web/routing/logger.go index b58065aa7365b..6a4dae66f7932 100644 --- a/modules/web/routing/logger.go +++ b/modules/web/routing/logger.go @@ -8,8 +8,8 @@ import ( "strings" "time" - "code.gitea.io/gitea/modules/context" "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 @@ -86,8 +86,8 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) { } var status int - if v, ok := record.responseWriter.(context.ResponseWriter); ok { - status = v.Status() + if v, ok := record.responseWriter.(types.ResponseStatusProvider); ok { + status = v.WrittenStatus() } logf := log.Info if strings.HasPrefix(req.RequestURI, "/assets/") { diff --git a/modules/web/types/response.go b/modules/web/types/response.go new file mode 100644 index 0000000000000..834f4912f4d16 --- /dev/null +++ b/modules/web/types/response.go @@ -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 +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 25456d0493426..70802fc4ca2ff 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -130,6 +130,11 @@ show_timestamps = Show timestamps show_log_seconds = Show seconds show_full_screen = Show full screen +confirm_delete_selected = Confirm to delete all selected items? + +name = Name +value = Value + [aria] navbar = Navigation Bar footer = Footer @@ -192,11 +197,9 @@ host = Host user = Username password = Password db_name = Database Name -db_helper = Note to MySQL users: please use the InnoDB storage engine and if you use "utf8mb4", your InnoDB version must be greater than 5.6 . db_schema = Schema db_schema_helper = Leave blank for database default ("public"). ssl_mode = SSL -charset = Charset path = Path sqlite_helper = File path for the SQLite3 database.
Enter an absolute path if you run Gitea as a service. reinstall_error = You are trying to install into an existing Gitea database @@ -1608,6 +1611,9 @@ issues.review.pending.tooltip = This comment is not currently visible to other u issues.review.review = Review issues.review.reviewers = Reviewers issues.review.outdated = Outdated +issues.review.outdated_description = Content has changed since this comment was made +issues.review.option.show_outdated_comments = Show outdated comments +issues.review.option.hide_outdated_comments = Hide outdated comments issues.review.show_outdated = Show outdated issues.review.hide_outdated = Hide outdated issues.review.show_resolved = Show resolved @@ -3389,8 +3395,6 @@ owner.settings.chef.keypair.description = Generate a key pair used to authentica secrets = Secrets description = Secrets will be passed to certain actions and cannot be read otherwise. none = There are no secrets yet. -value = Value -name = Name creation = Add Secret creation.name_placeholder = case-insensitive, alphanumeric characters or underscores only, cannot start with GITEA_ or GITHUB_ creation.value_placeholder = Input any content. Whitespace at the start and end will be omitted. @@ -3457,9 +3461,31 @@ runs.commit = Commit runs.pushed_by = Pushed by runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s runs.no_matching_runner_helper = No matching runner: %s +runs.actor = Actor +runs.status = Status +runs.actors_no_select = All actors +runs.status_no_select = All status +runs.no_results = No results matched. +runs.no_runs = The workflow has no runs yet. need_approval_desc = Need approval to run workflows for fork pull request. +variables = Variables +variables.management = Variables Management +variables.creation = Add Variable +variables.none = There are no variables yet. +variables.deletion = Remove variable +variables.deletion.description = Removing a variable is permanent and cannot be undone. Continue? +variables.description = Variables will be passed to certain actions and cannot be read otherwise. +variables.id_not_exist = Variable with id %d not exists. +variables.edit = Edit Variable +variables.deletion.failed = Failed to remove variable. +variables.deletion.success = The variable has been removed. +variables.creation.failed = Failed to add variable. +variables.creation.success = The variable "%s" has been added. +variables.update.failed = Failed to edit variable. +variables.update.success = The variable has been edited. + [projects] type-1.display_name = Individual Project type-2.display_name = Repository Project diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 12f3b41362f6e..1818343f92fa6 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -79,6 +79,8 @@ milestones=マイルストーン ok=OK cancel=キャンセル +rerun=再実行 +rerun_all=すべてのジョブを再実行 save=保存 add=追加 add_all=すべて追加 @@ -113,11 +115,19 @@ unknown=不明 rss_feed=RSSフィード +pin=ピン留め +unpin=ピン留め解除 +artifacts=成果物 +concept_system_global=グローバル +concept_user_individual=個人 concept_code_repository=リポジトリ concept_user_organization=組織 +show_timestamps=タイムスタンプを表示 +show_log_seconds=秒数を表示 +show_full_screen=フルスクリーン表示 [aria] navbar=ナビゲーションバー @@ -314,6 +324,7 @@ repos=リポジトリ users=ユーザー organizations=組織 search=検索 +go_to=開く code=コード search.type.tooltip=検索タイプ search.fuzzy=あいまい @@ -517,6 +528,7 @@ lang_select_error=言語をリストから選択してください。 username_been_taken=ユーザー名が既に使用されています。 username_change_not_local_user=非ローカルユーザーのユーザー名は変更できません。 +username_has_not_been_changed=ユーザー名は変更されていません repo_name_been_taken=リポジトリ名が既に使用されています。 repository_force_private=強制プライベートが有効です。プライベートリポジトリはパブリックにできません。 repository_files_already_exist=このリポジトリのファイルはすでに存在します。システム管理者に問い合わせてください。 @@ -565,6 +577,7 @@ target_branch_not_exist=ターゲットのブランチが存在していませ [user] change_avatar=アバターを変更… +joined_on=%sに登録 repositories=リポジトリ activity=公開アクティビティ followers=フォロワー @@ -760,6 +773,8 @@ ssh_principal_deletion_desc=SSH証明書プリンシパルを削除して、ア ssh_key_deletion_success=SSHキーを削除しました。 gpg_key_deletion_success=GPGキーを削除しました。 ssh_principal_deletion_success=プリンシパルを削除しました。 +added_on=%sに追加 +valid_until_date=%sまで有効 valid_forever=永久に有効 last_used=最終使用日 no_activity=使用されていません @@ -791,6 +806,13 @@ access_token_deletion_cancel_action=キャンセル access_token_deletion_confirm_action=削除 access_token_deletion_desc=トークンを削除すると、それを使用しているアプリケーションは、アカウントへのアクセスができなくなります。これは元に戻せません。続行しますか? delete_token_success=トークンを削除しました。 削除したトークンを使用しているアプリケーションは、今後あなたのアカウントにアクセスできません。 +repo_and_org_access=リポジトリと組織へのアクセス +permissions_public_only=公開のみ +permissions_access_all=すべて (公開、プライベート、限定) +select_permissions=許可の選択 +scoped_token_desc=認証は、選択したトークンスコープに対応するAPIルートのみに制限されます。 詳細はドキュメントを参照してください。 +at_least_one_permission=トークンを作成するには、少なくともひとつの許可を選択する必要があります +permissions_list=許可: manage_oauth2_applications=OAuth2アプリケーションの管理 edit_oauth2_application=OAuth2アプリケーションの編集 @@ -804,6 +826,7 @@ create_oauth2_application_success=新しいOAuth2アプリケーションを作 update_oauth2_application_success=OAuth2アプリケーションを更新しました。 oauth2_application_name=アプリケーション名 oauth2_confidential_client=コンフィデンシャルクライアント。 ウェブアプリのように秘密情報を機密にできるアプリの場合に選択します。 デスクトップアプリやモバイルアプリなどのネイティブアプリには選択しないでください。 +oauth2_redirect_uris=リダイレクトURI (複数可)。 URIごとに改行してください。 save_application=保存 oauth2_client_id=クライアントID oauth2_client_secret=クライアント シークレット @@ -949,6 +972,7 @@ mirror_password_blank_placeholder=(未設定) mirror_password_help=ユーザー名を変更すると保存されているパスワードは消去されます。 watchers=ウォッチャー stargazers=スターゲイザー +stars_remove_warning=これを指定すると、このリポジトリのスターはすべて削除されます。 forks=フォーク reactions_more=さらに %d 件 unit_disabled=サイト管理者がこのリポジトリセクションを無効にしています。 @@ -992,6 +1016,7 @@ template.one_item=最低一つはテンプレート項目を選択する必要 template.invalid=テンプレートリポジトリを選択する必要があります archive.title=このリポジトリはアーカイブされています。 ファイルの閲覧とクローンは可能ですが、プッシュや、イシュー・プルリクエストのオープンはできません。 +archive.title_date=このリポジトリは%sにアーカイブされています。 ファイルの閲覧やリポジトリのクローンは可能ですが、プッシュしたり、イシューやプルリクエストを作成することはできません。 archive.issue.nocomment=このリポジトリはアーカイブされています。 イシューにコメントを追加することはできません。 archive.pull.nocomment=このリポジトリはアーカイブされています。 プルリクエストにコメントを追加することはできません。 @@ -1033,6 +1058,7 @@ migrated_from_fake=%[1]sから移行 migrate.migrate=%s からの移行 migrate.migrating=%s から移行しています ... migrate.migrating_failed=%s からの移行が失敗しました。 +migrate.migrating_failed.error=移行に失敗しました: %s migrate.migrating_failed_no_addr=移行に失敗しました。 migrate.github.description=github.com やその他の GitHub インスタンスからデータを移行します。 migrate.git.description=Git サービスからリポジトリのみを移行します。 @@ -1049,6 +1075,8 @@ migrate.migrating_labels=ラベル移行中 migrate.migrating_releases=リリース移行中 migrate.migrating_issues=イシュー移行中 migrate.migrating_pulls=プルリクエスト移行中 +migrate.cancel_migrating_title=移行のキャンセル +migrate.cancel_migrating_confirm=この移行をキャンセルしますか? mirror_from=ミラー元 forked_from=フォーク元 @@ -1178,6 +1206,8 @@ editor.filename_is_invalid=`ファイル名が不正です: "%s"` editor.branch_does_not_exist=このリポジトリにブランチ "%s" は存在しません。 editor.branch_already_exists=ブランチ "%s" は、このリポジトリに既に存在します。 editor.directory_is_a_file=ディレクトリ名 "%s" はすでにリポジトリ内のファイルで使用されています。 +editor.file_is_a_symlink=`"%s" はシンボリックリンクです。 シンボリックリンクはWebエディターで編集できません。` +editor.filename_is_a_directory=ファイル名 "%s" は、このリポジトリ上でディレクトリ名としてすでに使用されています。 editor.file_editing_no_longer_exists=編集中のファイル "%s" が、もうリポジトリ内にありません。 editor.file_deleting_no_longer_exists=削除しようとしたファイル "%s" が、すでにリポジトリ内にありません。 editor.file_changed_while_editing=あなたが編集を開始したあと、ファイルの内容が変更されました。 ここをクリックして何が変更されたか確認するか、もう一度"変更をコミット"をクリックして上書きします。 @@ -1343,6 +1373,10 @@ issues.filter_label_exclude=`ラベルで除外するには alt + < issues.filter_label_no_select=すべてのラベル issues.filter_label_select_no_label=ラベルなし issues.filter_milestone=マイルストーン +issues.filter_milestone_all=すべてのマイルストーン +issues.filter_milestone_none=マイルストーンなし +issues.filter_milestone_open=オープン中のマイルストーン +issues.filter_milestone_closed=クローズ済みマイルストーン issues.filter_project=プロジェクト issues.filter_project_all=すべてのプロジェクト issues.filter_project_none=プロジェクトなし @@ -1402,6 +1436,8 @@ issues.context.edit=編集 issues.context.delete=削除 issues.no_content=まだ内容がありません issues.close=イシューをクローズ +issues.comment_pull_merged_at=がコミット %[1]s を %[2]s にマージ %[3]s +issues.comment_manually_pull_merged_at=がコミット %[1]s を %[2]s に手動マージ %[3]s issues.close_comment_issue=コメントしてクローズ issues.reopen_issue=再オープンする issues.reopen_comment_issue=コメントして再オープン @@ -1452,6 +1488,10 @@ issues.attachment.open_tab=`クリックして新しいタブで "%s" を見る` issues.attachment.download=`クリックして "%s" をダウンロード` issues.subscribe=購読する issues.unsubscribe=購読を解除 +issues.unpin_issue=イシューのピン留めを解除 +issues.max_pinned=これ以上イシューをピン留めできません +issues.pin_comment=がピン留め %s +issues.unpin_comment=がピン留めを解除 %s issues.lock=会話をロック issues.unlock=会話をアンロック issues.lock.unknown_reason=未定義の理由ではイシューをロックできません。 @@ -1531,6 +1571,7 @@ issues.dependency.issue_closing_blockedby=このイシューのクローズは issues.dependency.issue_close_blocks=このイシューは、これらのイシューのクローズをブロックしています issues.dependency.pr_close_blocks=このプルリクエストは、これらのイシューのクローズをブロックしています issues.dependency.issue_close_blocked=このイシューをクローズするには、ブロックしているイシューをすべてクローズする必要があります。 +issues.dependency.issue_batch_close_blocked=選択したイシューの一括クローズはできません。 イシュー #%d に、まだオープン中の依存関係があります。 issues.dependency.pr_close_blocked=このプルリクエストを操作するには、ブロックしているイシューをすべてクローズする必要があります。 issues.dependency.blocks_short=ブロック対象 issues.dependency.blocked_by_short=依存先 @@ -1876,6 +1917,16 @@ settings.hooks=Webhook settings.githooks=Gitフック settings.basic_settings=基本設定 settings.mirror_settings=ミラー設定 +settings.mirror_settings.docs=コミット、タグ、ブランチを他のリポジトリと自動的に同期するように、このリポジトリを設定します。 +settings.mirror_settings.docs.disabled_pull_mirror.instructions=コミット、タグ、ブランチを他のリポジトリに自動的にプッシュするように、このプロジェクトを設定します。 プル方式のミラーはサイト管理者により無効化されています。 +settings.mirror_settings.docs.disabled_push_mirror.instructions=コミット、タグ、ブランチを他のリポジトリから自動的にプルするように、このプロジェクトを設定します。 +settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning=現在は「新しい移行」メニューでのみ対応しています。 詳細は次を参照: +settings.mirror_settings.docs.disabled_push_mirror.info=プッシュ方式のミラーはサイト管理者により無効化されています。 +settings.mirror_settings.docs.no_new_mirrors=このリポジトリは他のリポジトリと変更をミラーリングしています。 現時点では新しいミラーを作成することはできないことに留意してください。 +settings.mirror_settings.docs.can_still_use=既存ミラーを変更したりミラーを新規に作成することはできませんが、既存ミラーを利用することは可能です。 +settings.mirror_settings.docs.pull_mirror_instructions=プル方式のミラーを設定するには、次を参照: +settings.mirror_settings.docs.doc_link_title=リポジトリをミラーリングするには? +settings.mirror_settings.docs.pulling_remote_title=リモートリポジトリからのプル settings.mirror_settings.mirrored_repository=同期するリポジトリ settings.mirror_settings.direction=方向 settings.mirror_settings.direction.pull=プル @@ -1888,6 +1939,8 @@ settings.sync_mirror=今すぐ同期 settings.mirror_sync_in_progress=ミラー同期を実行しています。 しばらくあとでまた確認してください。 settings.site=Webサイト settings.update_settings=設定を更新 +settings.update_mirror_settings=ミラーリング設定を更新 +settings.branches.switch_default_branch=デフォルトブランチを切り替え settings.branches.update_default_branch=デフォルトブランチを更新 settings.branches.add_new_rule=新しいルールを追加 settings.advanced_settings=拡張設定 @@ -1986,6 +2039,7 @@ settings.delete_notices_2=- この操作は、リポジトリ %s%s' の保護ルール settings.protect_this_branch=ブランチの保護を有効にする settings.protect_this_branch_desc=ブランチの削除を防ぎ、ブランチへのプッシュやマージを制限します。 settings.protect_disable_push=プッシュ無効 settings.protect_disable_push_desc=このブランチへのプッシュは許可されません。 settings.protect_enable_push=プッシュ有効 settings.protect_enable_push_desc=誰でも書き込み権限があれば、このブランチへのプッシュが許可されます。(強制プッシュ以外) +settings.protect_enable_merge=マージ有効 +settings.protect_enable_merge_desc=誰でも書き込み権限があれば、このブランチへのプルリクエストのマージが許可されます。 settings.protect_whitelist_committers=ホワイトリストでプッシュを制限 settings.protect_whitelist_committers_desc=ホワイトリストに登録したユーザーまたはチームにのみ、このブランチへのプッシュが許可されます。(強制プッシュ以外) settings.protect_whitelist_deploy_keys=プッシュ可能な書き込み権限を持つデプロイキーをホワイトリストに含める。 @@ -2156,7 +2217,13 @@ settings.protect_merge_whitelist_committers_desc=ホワイトリストに登録 settings.protect_merge_whitelist_users=マージ・ホワイトリストに含むユーザー: settings.protect_merge_whitelist_teams=マージ・ホワイトリストに含むチーム: settings.protect_check_status_contexts=ステータスチェックを有効にする +settings.protect_status_check_patterns=ステータスチェック パターン: +settings.protect_status_check_patterns_desc=このルールの対象ブランチがマージ可能になる前に、どのステータスチェックがパスしなければならないかを、パターンで入力します。 各行にパターンを指定します。 この設定は空にできません。 +settings.protect_check_status_contexts_desc=マージの前にステータスチェックがパスしていることを必須にします。 有効にした場合、まず他のブランチにコミットをプッシュしておき、このルールの対象ブランチのステータスチェックがパスしたあとに、マージまたは直接プッシュする必要があります。 マッチするコンテキストが無い場合は、コンテキストに関係なく最後のコミットが成功している必要があります。 settings.protect_check_status_contexts_list=この1週間に、このリポジトリに対して行われたステータスチェック +settings.protect_status_check_matched=マッチ +settings.protect_invalid_status_check_pattern=`不正なステータスチェックパターン: "%s"` +settings.protect_no_valid_status_check_patterns=有効なステータスチェックパターンがありません。 settings.protect_required_approvals=必要な承認数: settings.protect_required_approvals_desc=肯定的なレビューの数を満たしたプルリクエストしかマージできないようにします。 settings.protect_approvals_whitelist_enabled=ホワイトリストに登録したユーザーやチームに承認を制限 @@ -2168,6 +2235,7 @@ settings.dismiss_stale_approvals_desc=プルリクエストの内容を変える settings.require_signed_commits=コミット署名必須 settings.require_signed_commits_desc=署名されていない場合、または署名が検証できなかった場合は、このブランチへのプッシュを拒否します。 settings.protect_branch_name_pattern=保護ブランチ名のパターン +settings.protect_patterns=パターン settings.protect_protected_file_patterns=保護されるファイルのパターン (セミコロン';'で区切る): settings.protect_protected_file_patterns_desc=保護されたファイルは、このブランチにファイルを追加・編集・削除する権限を持つユーザーであっても、直接変更することができなくなります。 セミコロン(';')で区切って複数のパターンを指定できます。 パターンの文法については github.com/gobwas/glob を参照してください。 例: .drone.yml, /docs/**/*.txt settings.protect_unprotected_file_patterns=保護しないファイルのパターン (セミコロン';'で区切る): @@ -2382,10 +2450,13 @@ branch.protected_deletion_failed=ブランチ "%s" は保護されています branch.default_deletion_failed=ブランチ "%s" はデフォルトブランチです。 削除できません。 branch.restore=ブランチ "%s" の復元 branch.download=ブランチ "%s" をダウンロード +branch.rename=ブランチ名 "%s" を変更 branch.included_desc=このブランチはデフォルトブランチに含まれています branch.included=埋没 branch.create_new_branch=このブランチをもとに作成します: branch.confirm_create_branch=ブランチを作成 +branch.warning_rename_default_branch=デフォルトブランチの名前を変更しようとしています。 +branch.rename_branch_to=`"%s" を変更:` branch.confirm_rename_branch=ブランチ名を変更 branch.create_branch_operation=ブランチを作成 branch.new_branch=新しいブランチの作成 @@ -2944,6 +3015,7 @@ config.mailer_sendmail_timeout=Sendmail のタイムアウト config.mailer_use_dummy=Dummy config.test_email_placeholder=Email (例 test@example.com) config.send_test_mail=テストメールを送信 +config.send_test_mail_submit=送信 config.test_mail_failed=`"%s" へのテストメール送信に失敗しました: %v` config.test_mail_sent=テストメールを "%s" へ送信しました。 @@ -2983,13 +3055,16 @@ config.git_pull_timeout=プル操作のタイムアウト config.git_gc_timeout=GC操作のタイムアウト config.log_config=ログ設定 +config.logger_name_fmt=ロガー: %s config.disabled_logger=無効 config.access_log_mode=アクセスログのモード +config.access_log_template=アクセスログ テンプレート config.xorm_log_sql=SQLのログ出力 config.get_setting_failed=%s の取得に失敗しました config.set_setting_failed=%s の設定に失敗しました +monitor.stats=統計 monitor.cron=Cronタスク monitor.name=名称 @@ -2999,6 +3074,8 @@ monitor.previous=前回 monitor.execute_times=実行回数 monitor.process=実行中のプロセス monitor.stacktrace=スタックトレース +monitor.processes_count=%d プロセス +monitor.download_diagnosis_report=診断レポートをダウンロード monitor.desc=説明 monitor.start=開始日時 monitor.execute_time=実行時間 @@ -3019,11 +3096,14 @@ monitor.queue.numberinqueue=キュー内の数 monitor.queue.review=設定確認 monitor.queue.review_add=確認/ワーカー追加 monitor.queue.settings.title=プール設定 +monitor.queue.settings.desc=プールはワーカーキューの待機状態に応じて動的に大きくなります。 monitor.queue.settings.maxnumberworkers=ワーカー数上限 monitor.queue.settings.maxnumberworkers.placeholder=現在の設定 %[1]d monitor.queue.settings.maxnumberworkers.error=ワーカー数上限は数値にしてください monitor.queue.settings.submit=設定を更新 monitor.queue.settings.changed=設定を更新しました +monitor.queue.settings.remove_all_items=すべて削除 +monitor.queue.settings.remove_all_items_done=キュー内のすべての項目を削除しました。 notices.system_notice_list=システム通知 notices.view_detail_header=通知の詳細を表示 @@ -3158,9 +3238,15 @@ versions=バージョン versions.view_all=すべて表示 dependency.id=ID dependency.version=バージョン +alpine.registry=あなたの /etc/apk/repositories ファイルにURLを追加して、このレジストリをセットアップします: +alpine.registry.key=インデックス署名の検証のため、レジストリのRSA公開鍵を /etc/apk/keys/ フォルダにダウンロードします: +alpine.registry.info=$branch と $repository は下にあるリストから選んでください。 alpine.install=パッケージをインストールするには、次のコマンドを実行します: -alpine.repository.branches=ブランチ -alpine.repository.repositories=リポジトリ +alpine.documentation=Alpine レジストリの詳細については、ドキュメント を参照してください。 +alpine.repository=リポジトリ情報 +alpine.repository.branches=Branches +alpine.repository.repositories=Repositories +alpine.repository.architectures=Architectures cargo.registry=Cargo 設定ファイルでこのレジストリをセットアップします。(例 ~/.cargo/config.toml): cargo.install=Cargo を使用してパッケージをインストールするには、次のコマンドを実行します: cargo.documentation=Cargoレジストリの詳細については、 ドキュメント を参照してください。 @@ -3193,11 +3279,21 @@ container.layers=イメージレイヤー container.labels=ラベル container.labels.key=キー container.labels.value=値 +cran.registry=あなたの Rprofile.site ファイルに、このレジストリをセットアップします: cran.install=パッケージをインストールするには、次のコマンドを実行します: +cran.documentation=CRAN レジストリの詳細については、ドキュメント を参照してください。 debian.registry=このレジストリをコマンドラインからセットアップします: +debian.registry.info=$distribution と $component は下にあるリストから選んでください。 debian.install=パッケージをインストールするには、次のコマンドを実行します: +debian.documentation=Debian レジストリの詳細については、ドキュメント を参照してください。 +debian.repository=リポジトリ情報 +debian.repository.distributions=Distributions +debian.repository.components=Components +debian.repository.architectures=Architectures generic.download=コマンドラインでパッケージをダウンロードします: generic.documentation=汎用 レジストリの詳細については、ドキュメント を参照してください。 +go.install=コマンドラインでパッケージをインストール: +go.documentation=Go レジストリの詳細については、ドキュメント を参照してください。 helm.registry=このレジストリをコマンドラインからセットアップします: helm.install=パッケージをインストールするには、次のコマンドを実行します: helm.documentation=Helm レジストリの詳細については、 ドキュメント を参照してください。 @@ -3226,6 +3322,7 @@ pypi.install=pip を使用してパッケージをインストールするには pypi.documentation=PyPI レジストリの詳細については、ドキュメント を参照してください。 rpm.registry=このレジストリをコマンドラインからセットアップします: rpm.install=パッケージをインストールするには、次のコマンドを実行します: +rpm.documentation=RPM レジストリの詳細については、ドキュメント を参照してください。 rubygems.install=gem を使用してパッケージをインストールするには、次のコマンドを実行します: rubygems.install2=または Gemfile に追加します: rubygems.dependencies.runtime=実行用依存関係 @@ -3298,6 +3395,7 @@ deletion=シークレットの削除 deletion.description=シークレットの削除は恒久的で元に戻すことはできません。 続行しますか? deletion.success=シークレットを削除しました。 deletion.failed=シークレットの削除に失敗しました。 +management=シークレット管理 [actions] actions=Actions diff --git a/package-lock.json b/package-lock.json index 76beea934e33a..e808ccf2a8ffd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,8 +4,6 @@ "requires": true, "packages": { "": { - "name": "gitea", - "license": "MIT", "dependencies": { "@citation-js/core": "0.6.8", "@citation-js/plugin-bibtex": "0.6.8", @@ -14,9 +12,9 @@ "@claviska/jquery-minicolors": "2.3.6", "@github/markdown-toolbar-element": "2.1.1", "@github/relative-time-element": "4.3.0", - "@github/text-expander-element": "2.3.0", + "@github/text-expander-element": "2.5.0", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", - "@primer/octicons": "19.1.0", + "@primer/octicons": "19.3.0", "@webcomponents/custom-elements": "1.6.0", "add-asset-webpack-plugin": "2.0.1", "ansi-to-html": "0.7.2", @@ -32,7 +30,7 @@ "jquery.are-you-sure": "1.9.0", "katex": "0.16.7", "license-checker-webpack-plugin": "0.2.1", - "mermaid": "10.2.2", + "mermaid": "10.2.3", "mini-css-extract-plugin": "2.7.6", "minimatch": "9.0.1", "monaco-editor": "0.39.0", @@ -40,7 +38,7 @@ "pdfobject": "2.2.12", "pretty-ms": "8.0.0", "sortablejs": "1.15.0", - "swagger-ui-dist": "4.19.0", + "swagger-ui-dist": "5.0.0", "throttle-debounce": "5.0.0", "tippy.js": "6.3.7", "tributejs": "5.1.3", @@ -49,17 +47,17 @@ "vue-bar-graph": "2.0.0", "vue-loader": "17.2.2", "vue3-calendar-heatmap": "2.0.5", - "webpack": "5.86.0", + "webpack": "5.87.0", "webpack-cli": "5.1.4", "wrap-ansi": "8.1.0" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "3.2.1", - "@playwright/test": "1.34.3", + "@playwright/test": "1.35.1", "@rollup/pluginutils": "5.0.2", "@stoplight/spectral-cli": "6.8.0", "@vitejs/plugin-vue": "4.2.3", - "eslint": "8.42.0", + "eslint": "8.43.0", "eslint-plugin-array-func": "3.1.8", "eslint-plugin-custom-elements": "0.0.8", "eslint-plugin-import": "2.27.5", @@ -72,13 +70,15 @@ "eslint-plugin-vue": "9.14.1", "eslint-plugin-wc": "1.5.0", "jsdom": "22.1.0", - "markdownlint-cli": "0.34.0", + "markdownlint-cli": "0.35.0", "postcss-html": "1.5.0", - "stylelint": "15.7.0", + "stylelint": "15.8.0", + "stylelint-declaration-block-no-ignored-properties": "2.7.0", "stylelint-declaration-strict-value": "1.9.2", + "stylelint-stylistic": "0.4.2", "svgo": "3.0.2", - "updates": "14.1.1", - "vitest": "0.32.0" + "updates": "14.2.4", + "vitest": "0.32.2" }, "engines": { "node": ">= 16.0.0" @@ -94,33 +94,33 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", "dev": true, "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", + "@babel/helper-validator-identifier": "^7.22.5", "chalk": "^2.0.0", "js-tokens": "^4.0.0" }, @@ -197,9 +197,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.4.tgz", - "integrity": "sha512-VLLsx06XkEYqBtE5YGPwfSGwfrjnyPP5oiGty3S8pQLFDFLaS8VwWSIxkTXpcvr5zeYLE6+MBNl2npl/YnfofA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", + "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==", "bin": { "parser": "bin/babel-parser.js" }, @@ -208,9 +208,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.22.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.3.tgz", - "integrity": "sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", + "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", "dependencies": { "regenerator-runtime": "^0.13.11" }, @@ -886,9 +886,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.42.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.42.0.tgz", - "integrity": "sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.43.0.tgz", + "integrity": "sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -910,9 +910,9 @@ "integrity": "sha512-+tFjX9//HRS1HnBa5cNgfEtE52arwiutYg1TOF+Trk40SPxst9Q8Rtc3BKD6aKsvfbtub68vfhipgchGjj9o7g==" }, "node_modules/@github/text-expander-element": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@github/text-expander-element/-/text-expander-element-2.3.0.tgz", - "integrity": "sha512-E1KCxTOA/7Y4dP5g7vXbfRDFU6/SjU0TuJxx6JMwvxzI/NlBrXyXsx/fjFskD2T/un6i6CNKnXu1ZwExDLjcqw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@github/text-expander-element/-/text-expander-element-2.5.0.tgz", + "integrity": "sha512-BjCxTshkUCgNXo/8iXUSK1sJ7kMJqhVsw6LAzIFtgaYrm4q2068WnPKjngfR+/QPhxN1nSvgd7CozblEIYjUZA==", "dependencies": { "@github/combobox-nav": "^2.0.2" } @@ -1207,19 +1207,19 @@ } }, "node_modules/@playwright/test": { - "version": "1.34.3", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.34.3.tgz", - "integrity": "sha512-zPLef6w9P6T/iT6XDYG3mvGOqOyb6eHaV9XtkunYs0+OzxBtrPAAaHotc0X+PJ00WPPnLfFBTl7mf45Mn8DBmw==", + "version": "1.35.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz", + "integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==", "dev": true, "dependencies": { "@types/node": "*", - "playwright-core": "1.34.3" + "playwright-core": "1.35.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=14" + "node": ">=16" }, "optionalDependencies": { "fsevents": "2.3.2" @@ -1235,9 +1235,9 @@ } }, "node_modules/@primer/octicons": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/@primer/octicons/-/octicons-19.1.0.tgz", - "integrity": "sha512-5o90F89gNPnAk1qfzl3hb/TcsUjk5g0WFI+fBRHLkBKzB3uc9EvxTpgzjXhhjyriOkrBOjFo58D0sjtwttaQww==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@primer/octicons/-/octicons-19.3.0.tgz", + "integrity": "sha512-hyIo54VPC3VI7ZyAgosiJcbhxq1gZLbBspZwN9cg1uImRd2E8T9JST3kGeezezJYPjG367FuF7p1L+gmLmeESw==", "dependencies": { "object-assign": "^4.1.1" } @@ -1521,14 +1521,14 @@ } }, "node_modules/@stoplight/spectral-formatters": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-formatters/-/spectral-formatters-1.0.0.tgz", - "integrity": "sha512-stdapKMnzEkpnefLUa7FnB++YxFM+Xlo17jxpcRpL+Ld6mHLs41AWoUzEfXDRzW5rHejVFTU/9Ld/ymzlyse8A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-formatters/-/spectral-formatters-1.1.0.tgz", + "integrity": "sha512-KUOKOF0Wz9iFS4pKHO8mSx0OZc9J5ziFJfbxFOL8xDGlxYTfBIQsaOjwi6GItcar7wK8S2ksAIUS2ijzAygZ5A==", "dev": true, "dependencies": { "@stoplight/path": "^1.3.2", - "@stoplight/spectral-core": "^1.18.0", - "@stoplight/spectral-runtime": "^1.1.2", + "@stoplight/spectral-core": "^1.15.1", + "@stoplight/spectral-runtime": "^1.1.0", "@stoplight/types": "^13.15.0", "chalk": "4.1.2", "cliui": "7.0.4", @@ -1800,9 +1800,9 @@ } }, "node_modules/@types/eslint": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.40.0.tgz", - "integrity": "sha512-nbq2mvc/tBrK9zQQuItvjJl++GTN5j06DaPtp3hZCpngmG6Q3xoyEmd0TwZI0gAy/G1X0zhGBbr2imsGFdFV0g==", + "version": "8.40.2", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.40.2.tgz", + "integrity": "sha512-PRVjQ4Eh9z9pmmtaq8nTjZjQwKFk7YIHIud3lRoKRBgUQjgjRmoGxxGEPXQkF+lH7QkHJRNr5F4aBgYCW0lqpQ==", "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -1858,9 +1858,9 @@ "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, "node_modules/@types/node": { - "version": "20.2.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.5.tgz", - "integrity": "sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==" + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz", + "integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==" }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", @@ -1901,13 +1901,13 @@ } }, "node_modules/@vitest/expect": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.32.0.tgz", - "integrity": "sha512-VxVHhIxKw9Lux+O9bwLEEk2gzOUe93xuFHy9SzYWnnoYZFYg1NfBtnfnYWiJN7yooJ7KNElCK5YtA7DTZvtXtg==", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.32.2.tgz", + "integrity": "sha512-6q5yzweLnyEv5Zz1fqK5u5E83LU+gOMVBDuxBl2d2Jfx1BAp5M+rZgc5mlyqdnxquyoiOXpXmFNkcGcfFnFH3Q==", "dev": true, "dependencies": { - "@vitest/spy": "0.32.0", - "@vitest/utils": "0.32.0", + "@vitest/spy": "0.32.2", + "@vitest/utils": "0.32.2", "chai": "^4.3.7" }, "funding": { @@ -1915,12 +1915,12 @@ } }, "node_modules/@vitest/runner": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.32.0.tgz", - "integrity": "sha512-QpCmRxftHkr72xt5A08xTEs9I4iWEXIOCHWhQQguWOKE4QH7DXSKZSOFibuwEIMAD7G0ERvtUyQn7iPWIqSwmw==", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.32.2.tgz", + "integrity": "sha512-06vEL0C1pomOEktGoLjzZw+1Fb+7RBRhmw/06WkDrd1akkT9i12su0ku+R/0QM69dfkIL/rAIDTG+CSuQVDcKw==", "dev": true, "dependencies": { - "@vitest/utils": "0.32.0", + "@vitest/utils": "0.32.2", "concordance": "^5.0.4", "p-limit": "^4.0.0", "pathe": "^1.1.0" @@ -1957,9 +1957,9 @@ } }, "node_modules/@vitest/snapshot": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.32.0.tgz", - "integrity": "sha512-yCKorPWjEnzpUxQpGlxulujTcSPgkblwGzAUEL+z01FTUg/YuCDZ8dxr9sHA08oO2EwxzHXNLjQKWJ2zc2a19Q==", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.32.2.tgz", + "integrity": "sha512-JwhpeH/PPc7GJX38vEfCy9LtRzf9F4er7i4OsAJyV7sjPwjj+AIR8cUgpMTWK4S3TiamzopcTyLsZDMuldoi5A==", "dev": true, "dependencies": { "magic-string": "^0.30.0", @@ -1983,9 +1983,9 @@ } }, "node_modules/@vitest/spy": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.32.0.tgz", - "integrity": "sha512-MruAPlM0uyiq3d53BkwTeShXY0rYEfhNGQzVO5GHBmmX3clsxcWp79mMnkOVcV244sNTeDcHbcPFWIjOI4tZvw==", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.32.2.tgz", + "integrity": "sha512-Q/ZNILJ4ca/VzQbRM8ur3Si5Sardsh1HofatG9wsJY1RfEaw0XKP8IVax2lI1qnrk9YPuG9LA2LkZ0EI/3d4ug==", "dev": true, "dependencies": { "tinyspy": "^2.1.0" @@ -1995,12 +1995,12 @@ } }, "node_modules/@vitest/utils": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.32.0.tgz", - "integrity": "sha512-53yXunzx47MmbuvcOPpLaVljHaeSu1G2dHdmy7+9ngMnQIkBQcvwOcoclWFnxDMxFbnq8exAfh3aKSZaK71J5A==", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.32.2.tgz", + "integrity": "sha512-lnJ0T5i03j0IJaeW73hxe2AuVnZ/y1BhhCOuIcl9LIzXnbpXJT9Lrt6brwKHXLOiA7MZ6N5hSJjt0xE1dGNCzQ==", "dev": true, "dependencies": { - "concordance": "^5.0.4", + "diff-sequences": "^29.4.3", "loupe": "^2.3.6", "pretty-format": "^27.5.1" }, @@ -2338,9 +2338,9 @@ } }, "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", + "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", "bin": { "acorn": "bin/acorn" }, @@ -2739,9 +2739,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.7.tgz", - "integrity": "sha512-BauCXrQ7I2ftSqd2mvKHGo85XR0u7Ru3C/Hxsy/0TkfCtjrmAbPdzLGasmoiBxplpDXlPvdjX9u7srIMfgasNA==", + "version": "4.21.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", + "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", "funding": [ { "type": "opencollective", @@ -2757,8 +2757,8 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001489", - "electron-to-chromium": "^1.4.411", + "caniuse-lite": "^1.0.30001503", + "electron-to-chromium": "^1.4.431", "node-releases": "^2.0.12", "update-browserslist-db": "^1.0.11" }, @@ -2882,9 +2882,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001495", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001495.tgz", - "integrity": "sha512-F6x5IEuigtUfU5ZMQK2jsy5JqUUlEFRVZq8bO2a+ysq5K7jD6PPc9YXZj78xDNS3uNchesp1Jw47YXEqr+Viyg==", + "version": "1.0.30001504", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001504.tgz", + "integrity": "sha512-5uo7eoOp2mKbWyfMXnGO9rJWOGU8duvzEiYITW+wivukL7yHH4gX9yuRaobu6El4jPxo6jKZfG+N6fB621GD/Q==", "funding": [ { "type": "opencollective", @@ -4023,6 +4023,15 @@ "node": ">=0.3.1" } }, + "node_modules/diff-sequences": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", + "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -4158,9 +4167,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.425", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.425.tgz", - "integrity": "sha512-wv1NufHxu11zfDbY4fglYQApMswleE9FL/DSeyOyauVXDZ+Kco96JK/tPfBUaDqfRarYp2WH2hJ/5UnVywp9Jg==" + "version": "1.4.433", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.433.tgz", + "integrity": "sha512-MGO1k0w1RgrfdbLVwmXcDhHHuxCn2qRgR7dYsJvWFKDttvYPx6FNzCGG0c/fBBvzK2LDh3UV7Tt9awnHnvAAUQ==" }, "node_modules/elkjs": { "version": "0.8.2", @@ -4190,9 +4199,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.14.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.14.1.tgz", - "integrity": "sha512-Vklwq2vDKtl0y/vtwjSesgJ5MYS7Etuk5txS8VdKL4AOS1aUlD96zqIfsOSLQsdv3xgMRbtkWM8eG9XDfKUPow==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -4299,9 +4308,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz", - "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz", + "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==" }, "node_modules/es-set-tostringtag": { "version": "2.0.1", @@ -4507,15 +4516,15 @@ } }, "node_modules/eslint": { - "version": "8.42.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.42.0.tgz", - "integrity": "sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.43.0.tgz", + "integrity": "sha512-aaCpf2JqqKesMFGgmRPessmVKjcGXqdlAYLLC3THM8t5nBRZRQ+st5WM/hoJXkdioEXLLbXgclUpM0TXo5HX5Q==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.42.0", + "@eslint/js": "8.43.0", "@humanwhocodes/config-array": "^0.11.10", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -6899,48 +6908,48 @@ } }, "node_modules/markdownlint": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.28.2.tgz", - "integrity": "sha512-yYaQXoKKPV1zgrFsyAuZPEQoe+JrY9GDag9ObKpk09twx4OCU5lut+0/kZPrQ3W7w82SmgKhd7D8m34aG1unVw==", + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.29.0.tgz", + "integrity": "sha512-ASAzqpODstu/Qsk0xW5BPgWnK/qjpBQ4e7IpsSvvFXcfYIjanLTdwFRJK1SIEEh0fGSMKXcJf/qhaZYHyME0wA==", "dev": true, "dependencies": { "markdown-it": "13.0.1", - "markdownlint-micromark": "0.1.2" + "markdownlint-micromark": "0.1.5" }, "engines": { - "node": ">=14.18.0" + "node": ">=16" } }, "node_modules/markdownlint-cli": { - "version": "0.34.0", - "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.34.0.tgz", - "integrity": "sha512-4G9I++VBTZkaye6Yfc/7dU6HQHcyldZEVB+bYyQJLcpJOHKk/q5ZpGqK80oKMIdlxzsA3aWOJLZ4DkoaoUWXbQ==", + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.35.0.tgz", + "integrity": "sha512-lVIIIV1MrUtjoocgDqXLxUCxlRbn7Ve8rsWppfwciUNwLlNS28AhNiyQ3PU7jjj4Qvj+rWTTvwkqg7AcdG988g==", "dev": true, "dependencies": { - "commander": "~10.0.1", + "commander": "~11.0.0", "get-stdin": "~9.0.0", - "glob": "~10.2.2", + "glob": "~10.2.7", "ignore": "~5.2.4", "js-yaml": "^4.1.0", "jsonc-parser": "~3.2.0", - "markdownlint": "~0.28.2", - "minimatch": "~9.0.0", + "markdownlint": "~0.29.0", + "minimatch": "~9.0.1", "run-con": "~1.2.11" }, "bin": { "markdownlint": "markdownlint.js" }, "engines": { - "node": ">=14" + "node": ">=16" } }, "node_modules/markdownlint-cli/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", + "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", "dev": true, "engines": { - "node": ">=14" + "node": ">=16" } }, "node_modules/markdownlint-cli/node_modules/glob": { @@ -6972,12 +6981,12 @@ "dev": true }, "node_modules/markdownlint-micromark": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/markdownlint-micromark/-/markdownlint-micromark-0.1.2.tgz", - "integrity": "sha512-jRxlQg8KpOfM2IbCL9RXM8ZiYWz2rv6DlZAnGv8ASJQpUh6byTBnEsbuMZ6T2/uIgntyf7SKg/mEaEBo1164fQ==", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/markdownlint-micromark/-/markdownlint-micromark-0.1.5.tgz", + "integrity": "sha512-HvofNU4QCvfUCWnocQP1IAWaqop5wpWrB0mKB6SSh0fcpV0PdmQNS6tdUuFew1utpYlUvYYzz84oDkrD76GB9A==", "dev": true, "engines": { - "node": ">=14.18.0" + "node": ">=16" } }, "node_modules/marked": { @@ -7157,9 +7166,9 @@ } }, "node_modules/mermaid": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.2.2.tgz", - "integrity": "sha512-ifYKlCcZKYq48hxC1poJXnvk/PbCdgqqbg5B4qsybb8nIItPM1ATKqVEDkyde6BBJxVFhVJr9hoUjipzniQJZg==", + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.2.3.tgz", + "integrity": "sha512-cMVE5s9PlQvOwfORkyVpr5beMsLdInrycAosdr+tpZ0WFjG4RJ/bUHST7aTgHNJbujHkdBRAm+N50P3puQOfPw==", "dependencies": { "@braintree/sanitize-url": "^6.0.2", "cytoscape": "^3.23.0", @@ -8345,15 +8354,15 @@ "dev": true }, "node_modules/playwright-core": { - "version": "1.34.3", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.34.3.tgz", - "integrity": "sha512-2pWd6G7OHKemc5x1r1rp8aQcpvDh7goMBZlJv6Co5vCNLVcQJdhxRL09SGaY6HcyHH9aT4tiynZabMofVasBYw==", + "version": "1.35.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz", + "integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==", "dev": true, "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=14" + "node": ">=16" } }, "node_modules/pluralize": { @@ -9185,9 +9194,9 @@ } }, "node_modules/schema-utils": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.1.0.tgz", - "integrity": "sha512-Jw+GZVbP5IggB2WAn6UHI02LBwGmsIeYN/lNbSMZyDziQ7jmtAUrqKqDja+W89YHVs+KL/3IkIMltAklqB1vAw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -9214,9 +9223,9 @@ } }, "node_modules/semver": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -9717,9 +9726,9 @@ "dev": true }, "node_modules/stylelint": { - "version": "15.7.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.7.0.tgz", - "integrity": "sha512-fQRwHwWuZsDn4ENyE9AsKkOkV9WlD2CmYiVDbdZPdS3iZh0ceypOn1EuwTNuZ8xTrHF+jVeIEzLtFFSlD/nJHg==", + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.8.0.tgz", + "integrity": "sha512-x9qBk84F3MEjMEUNCE7MtWmfj9G9y5XzJ0cpQeJdy2l/IoqjC8Ih0N0ytmOTnXE4Yv0J7I1cmVRQUVNSPCxTsA==", "dev": true, "dependencies": { "@csstools/css-parser-algorithms": "^2.2.0", @@ -9762,7 +9771,6 @@ "supports-hyperlinks": "^3.0.0", "svg-tags": "^1.0.0", "table": "^6.8.1", - "v8-compile-cache": "^2.3.0", "write-file-atomic": "^5.0.1" }, "bin": { @@ -9776,6 +9784,18 @@ "url": "https://opencollective.com/stylelint" } }, + "node_modules/stylelint-declaration-block-no-ignored-properties": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stylelint-declaration-block-no-ignored-properties/-/stylelint-declaration-block-no-ignored-properties-2.7.0.tgz", + "integrity": "sha512-44SpI9+9Oc1ICuwwRfwS/3npQ2jPobDSTnwWdNgZGryGqQCp17CgEIWjCv1BgUOSzND3RqywNCNLKvO1AOxbfg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "stylelint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0" + } + }, "node_modules/stylelint-declaration-strict-value": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/stylelint-declaration-strict-value/-/stylelint-declaration-strict-value-1.9.2.tgz", @@ -9789,6 +9809,21 @@ "stylelint": ">=7 <=15" } }, + "node_modules/stylelint-stylistic": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/stylelint-stylistic/-/stylelint-stylistic-0.4.2.tgz", + "integrity": "sha512-WF/fLvotTklG8LZ+fO0nxhu5swVENHhSat2l5ckrmtXpij1P9ybYa56XivOg/E02CH+Ygui5pd2hQrlli3NCDQ==", + "dev": true, + "dependencies": { + "postcss": "^8.4.21", + "postcss-media-query-parser": "^0.2.3", + "postcss-value-parser": "^4.2.0", + "style-search": "^0.1.0" + }, + "peerDependencies": { + "stylelint": "^15.0.0" + } + }, "node_modules/stylelint/node_modules/balanced-match": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", @@ -9889,9 +9924,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.19.0.tgz", - "integrity": "sha512-9C9fJGI18gK5AhaU5YRyPY1lXJH4lmWh8h9zFMrJBkYzdRjCbAzYl1ayWPYgwFvag/Luqi3Co599OK/39IS2QQ==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.0.0.tgz", + "integrity": "sha512-bwl6og9I9CAHKGSnYLKydjhBuH7d3oU6RX6uKN8oDCkLusTHXOW3sZMyBWjRtjGFnCMmN085oZoaR/4Wm9nIaQ==" }, "node_modules/symbol-tree": { "version": "3.2.4", @@ -9900,9 +9935,9 @@ "dev": true }, "node_modules/sync-fetch": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.4.2.tgz", - "integrity": "sha512-vilDD6yTGwyUjm7/W5WUUOCw1GH1aV591zC21XhbV6MJNZqfZcNMs9DVPHzy1UAmQ2GAg6S03F5TQ3paegKSdg==", + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.4.5.tgz", + "integrity": "sha512-esiWJ7ixSKGpd9DJPBTC4ckChqdOjIwJfYhVHkcQ2Gnm41323p1TRmEI+esTQ9ppD+b5opps2OTEGTCGX5kF+g==", "dependencies": { "buffer": "^5.7.1", "node-fetch": "^2.6.1" @@ -9936,9 +9971,9 @@ } }, "node_modules/terser": { - "version": "5.17.7", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.7.tgz", - "integrity": "sha512-/bi0Zm2C6VAexlGgLlVxA0P2lru/sdLyfCVaRMfKVo9nWxbmz7f/sD8VPybPeSUJaJcwmCJis9pBIhcVcG1QcQ==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.18.0.tgz", + "integrity": "sha512-pdL757Ig5a0I+owA42l6tIuEycRuM7FPY4n62h44mRLRfnOxJkkOHd6i89dOpwZlpF6JXBwaAHF6yWzFrt+QyA==", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -10014,9 +10049,9 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.2.0.tgz", - "integrity": "sha512-0zTyLGyDJYd/MBxG1AhJkKa6fpEBds4OQO2ut0w7OYG+ZGhGea09lijvzsqegYSik88zc7cUtIlnnO+/BvD6gQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -10342,9 +10377,9 @@ } }, "node_modules/updates": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/updates/-/updates-14.1.1.tgz", - "integrity": "sha512-FRaSNMgs3T24w92GrIyspY//2gaAXyoNg+nHaYZMv6MT5xJEFYT25RLnKQIJuz0CW2l1OOnMxooTn+LzN/2NIw==", + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/updates/-/updates-14.2.4.tgz", + "integrity": "sha512-r54h4Q12lUAmQ9dENy7BnY22AnTfW4YGEZw73gv6RvNEWgcZ3qS88jPLc1ckPAzt/8TPKWwLkSVpbEpgGwglJw==", "dev": true, "bin": { "updates": "bin/updates.js" @@ -10416,12 +10451,6 @@ "node": ">=8" } }, - "node_modules/v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -10490,9 +10519,9 @@ } }, "node_modules/vite-node": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.32.0.tgz", - "integrity": "sha512-220P/y8YacYAU+daOAqiGEFXx2A8AwjadDzQqos6wSukjvvTWNqleJSwoUn0ckyNdjHIKoxn93Nh1vWBqEKr3Q==", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.32.2.tgz", + "integrity": "sha512-dTQ1DCLwl2aEseov7cfQ+kDMNJpM1ebpyMMMwWzBvLbis8Nla/6c9WQcqpPssTwS6Rp/+U6KwlIj8Eapw4bLdA==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -10513,9 +10542,9 @@ } }, "node_modules/vite/node_modules/rollup": { - "version": "3.24.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.24.0.tgz", - "integrity": "sha512-OgraHOIg2YpHQTjl0/ymWfFNBEyPucB7lmhXrQUh38qNOegxLapSPFs9sNr0qKR75awW41D93XafoR2QfhBdUQ==", + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.25.1.tgz", + "integrity": "sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -10529,19 +10558,19 @@ } }, "node_modules/vitest": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.32.0.tgz", - "integrity": "sha512-SW83o629gCqnV3BqBnTxhB10DAwzwEx3z+rqYZESehUB+eWsJxwcBQx7CKy0otuGMJTYh7qCVuUX23HkftGl/Q==", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.32.2.tgz", + "integrity": "sha512-hU8GNNuQfwuQmqTLfiKcqEhZY72Zxb7nnN07koCUNmntNxbKQnVbeIS6sqUgR3eXSlbOpit8+/gr1KpqoMgWCQ==", "dev": true, "dependencies": { "@types/chai": "^4.3.5", "@types/chai-subset": "^1.3.3", "@types/node": "*", - "@vitest/expect": "0.32.0", - "@vitest/runner": "0.32.0", - "@vitest/snapshot": "0.32.0", - "@vitest/spy": "0.32.0", - "@vitest/utils": "0.32.0", + "@vitest/expect": "0.32.2", + "@vitest/runner": "0.32.2", + "@vitest/snapshot": "0.32.2", + "@vitest/spy": "0.32.2", + "@vitest/utils": "0.32.2", "acorn": "^8.8.2", "acorn-walk": "^8.2.0", "cac": "^6.7.14", @@ -10557,7 +10586,7 @@ "tinybench": "^2.5.0", "tinypool": "^0.5.0", "vite": "^3.0.0 || ^4.0.0", - "vite-node": "0.32.0", + "vite-node": "0.32.2", "why-is-node-running": "^2.2.2" }, "bin": { @@ -10656,9 +10685,9 @@ } }, "node_modules/vue-eslint-parser": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.0.tgz", - "integrity": "sha512-48IxT9d0+wArT1+3wNIy0tascRoywqSUe2E1YalIC1L8jsUGe5aJQItWfRok7DVFGz3UYvzEI7n5wiTXsCMAcQ==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.1.tgz", + "integrity": "sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==", "dev": true, "dependencies": { "debug": "^4.3.4", @@ -10751,9 +10780,9 @@ } }, "node_modules/webpack": { - "version": "5.86.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.86.0.tgz", - "integrity": "sha512-3BOvworZ8SO/D4GVP+GoRC3fVeg5MO4vzmq8TJJEkdmopxyazGDxN8ClqN12uzrZW9Tv8EED8v5VSb6Sqyi0pg==", + "version": "5.87.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.87.0.tgz", + "integrity": "sha512-GOu1tNbQ7p1bDEoFRs2YPcfyGs8xq52yyPBZ3m2VGnXGtV9MxjrkABHm4V9Ia280OefsSLzvbVoXcfLxjKY/Iw==", "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.0", @@ -10764,7 +10793,7 @@ "acorn-import-assertions": "^1.9.0", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.14.1", + "enhanced-resolve": "^5.15.0", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -10774,7 +10803,7 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.1.2", + "schema-utils": "^3.2.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.7", "watchpack": "^2.4.0", @@ -10918,9 +10947,9 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/webpack/node_modules/schema-utils": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.2.0.tgz", - "integrity": "sha512-0zTyLGyDJYd/MBxG1AhJkKa6fpEBds4OQO2ut0w7OYG+ZGhGea09lijvzsqegYSik88zc7cUtIlnnO+/BvD6gQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", diff --git a/package.json b/package.json index 41d834771aaff..0701862cc2a65 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,4 @@ { - "name": "gitea", - "license": "MIT", - "private": true, "type": "module", "engines": { "node": ">= 16.0.0" @@ -14,9 +11,9 @@ "@claviska/jquery-minicolors": "2.3.6", "@github/markdown-toolbar-element": "2.1.1", "@github/relative-time-element": "4.3.0", - "@github/text-expander-element": "2.3.0", + "@github/text-expander-element": "2.5.0", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", - "@primer/octicons": "19.1.0", + "@primer/octicons": "19.3.0", "@webcomponents/custom-elements": "1.6.0", "add-asset-webpack-plugin": "2.0.1", "ansi-to-html": "0.7.2", @@ -32,7 +29,7 @@ "jquery.are-you-sure": "1.9.0", "katex": "0.16.7", "license-checker-webpack-plugin": "0.2.1", - "mermaid": "10.2.2", + "mermaid": "10.2.3", "mini-css-extract-plugin": "2.7.6", "minimatch": "9.0.1", "monaco-editor": "0.39.0", @@ -40,7 +37,7 @@ "pdfobject": "2.2.12", "pretty-ms": "8.0.0", "sortablejs": "1.15.0", - "swagger-ui-dist": "4.19.0", + "swagger-ui-dist": "5.0.0", "throttle-debounce": "5.0.0", "tippy.js": "6.3.7", "tributejs": "5.1.3", @@ -49,17 +46,17 @@ "vue-bar-graph": "2.0.0", "vue-loader": "17.2.2", "vue3-calendar-heatmap": "2.0.5", - "webpack": "5.86.0", + "webpack": "5.87.0", "webpack-cli": "5.1.4", "wrap-ansi": "8.1.0" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "3.2.1", - "@playwright/test": "1.34.3", + "@playwright/test": "1.35.1", "@rollup/pluginutils": "5.0.2", "@stoplight/spectral-cli": "6.8.0", "@vitejs/plugin-vue": "4.2.3", - "eslint": "8.42.0", + "eslint": "8.43.0", "eslint-plugin-array-func": "3.1.8", "eslint-plugin-custom-elements": "0.0.8", "eslint-plugin-import": "2.27.5", @@ -72,13 +69,15 @@ "eslint-plugin-vue": "9.14.1", "eslint-plugin-wc": "1.5.0", "jsdom": "22.1.0", - "markdownlint-cli": "0.34.0", + "markdownlint-cli": "0.35.0", "postcss-html": "1.5.0", - "stylelint": "15.7.0", + "stylelint": "15.8.0", + "stylelint-declaration-block-no-ignored-properties": "2.7.0", "stylelint-declaration-strict-value": "1.9.2", + "stylelint-stylistic": "0.4.2", "svgo": "3.0.2", - "updates": "14.1.1", - "vitest": "0.32.0" + "updates": "14.2.4", + "vitest": "0.32.2" }, "browserslist": [ "defaults", diff --git a/poetry.toml b/poetry.toml index 57e0ba893fc4c..0299355b5ddad 100644 --- a/poetry.toml +++ b/poetry.toml @@ -1,4 +1,4 @@ [virtualenvs] in-project = true -no-pip = true -no-setuptools = true +options.no-pip = true +options.no-setuptools = true diff --git a/public/img/svg/octicon-copilot.svg b/public/img/svg/octicon-copilot.svg index 64eddfa1d5d11..b2a143c5c63ca 100644 --- a/public/img/svg/octicon-copilot.svg +++ b/public/img/svg/octicon-copilot.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/img/svg/octicon-pivot-column.svg b/public/img/svg/octicon-pivot-column.svg new file mode 100644 index 0000000000000..34370c2060072 --- /dev/null +++ b/public/img/svg/octicon-pivot-column.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/svg/octicon-redo.svg b/public/img/svg/octicon-redo.svg new file mode 100644 index 0000000000000..a4fbeabeaa221 --- /dev/null +++ b/public/img/svg/octicon-redo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/svg/octicon-tracked-by-closed-completed.svg b/public/img/svg/octicon-tracked-by-closed-completed.svg new file mode 100644 index 0000000000000..7a3af6dee2b54 --- /dev/null +++ b/public/img/svg/octicon-tracked-by-closed-completed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/svg/octicon-tracked-by-closed-not-planned.svg b/public/img/svg/octicon-tracked-by-closed-not-planned.svg new file mode 100644 index 0000000000000..bbeb817b822d6 --- /dev/null +++ b/public/img/svg/octicon-tracked-by-closed-not-planned.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/svg/octicon-undo.svg b/public/img/svg/octicon-undo.svg new file mode 100644 index 0000000000000..53ac6464147d1 --- /dev/null +++ b/public/img/svg/octicon-undo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index d5151bd480915..ce5f475b272fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,3 @@ djlint = "1.31.0" [tool.djlint] profile="golang" ignore="H005,H006,H008,H013,H014,H016,H020,H021,H023,H026,H030,H031,T027" - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" diff --git a/routers/api/actions/actions.go b/routers/api/actions/actions.go index bdcac412068ab..a418b3a1c4538 100644 --- a/routers/api/actions/actions.go +++ b/routers/api/actions/actions.go @@ -4,7 +4,6 @@ package actions import ( - "context" "net/http" "code.gitea.io/gitea/modules/web" @@ -12,7 +11,7 @@ import ( "code.gitea.io/gitea/routers/api/actions/runner" ) -func Routes(_ context.Context, prefix string) *web.Route { +func Routes(prefix string) *web.Route { m := web.NewRoute() path, handler := ping.NewPingServiceHandler() diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go index 4b10cd7ad1108..060f7bc3d4bae 100644 --- a/routers/api/actions/artifacts.go +++ b/routers/api/actions/artifacts.go @@ -82,6 +82,7 @@ import ( "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + web_types "code.gitea.io/gitea/modules/web/types" ) const ( @@ -102,7 +103,7 @@ type ArtifactContext struct { } func init() { - web.RegisterHandleTypeProvider[*ArtifactContext](func(req *http.Request) web.ResponseStatusProvider { + web.RegisterResponseStatusProvider[*ArtifactContext](func(req *http.Request) web_types.ResponseStatusProvider { return req.Context().Value(artifactContextKey).(*ArtifactContext) }) } diff --git a/routers/api/actions/runner/utils.go b/routers/api/actions/runner/utils.go index e1d54134c6133..cc9c06ab455cc 100644 --- a/routers/api/actions/runner/utils.go +++ b/routers/api/actions/runner/utils.go @@ -36,6 +36,7 @@ func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv WorkflowPayload: t.Job.WorkflowPayload, Context: generateTaskContext(t), Secrets: getSecretsOfTask(ctx, t), + Vars: getVariablesOfTask(ctx, t), } if needs, err := findTaskNeeds(ctx, t); err != nil { @@ -88,6 +89,29 @@ func getSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) map[s return secrets } +func getVariablesOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string { + variables := map[string]string{} + + // Org / User level + ownerVariables, err := actions_model.FindVariables(ctx, actions_model.FindVariablesOpts{OwnerID: task.Job.Run.Repo.OwnerID}) + if err != nil { + log.Error("find variables of org: %d, error: %v", task.Job.Run.Repo.OwnerID, err) + } + + // Repo level + repoVariables, err := actions_model.FindVariables(ctx, actions_model.FindVariablesOpts{RepoID: task.Job.Run.RepoID}) + if err != nil { + log.Error("find variables of repo: %d, error: %v", task.Job.Run.RepoID, err) + } + + // Level precedence: Repo > Org / User + for _, v := range append(ownerVariables, repoVariables...) { + variables[v.Name] = v.Data + } + + return variables +} + func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct { event := map[string]interface{}{} _ = json.Unmarshal([]byte(t.Job.Run.EventPayload), &event) @@ -119,7 +143,7 @@ func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct { "head_ref": headRef, // string, The head_ref or source branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either pull_request or pull_request_target. "job": fmt.Sprint(t.JobID), // string, The job_id of the current job. "ref": t.Job.Run.Ref, // string, The fully-formed ref of the branch or tag that triggered the workflow run. For workflows triggered by push, this is the branch or tag ref that was pushed. For workflows triggered by pull_request, this is the pull request merge branch. For workflows triggered by release, this is the release tag created. For other triggers, this is the branch or tag ref that triggered the workflow run. This is only set if a branch or tag is available for the event type. The ref given is fully-formed, meaning that for branches the format is refs/heads/, for pull requests it is refs/pull//merge, and for tags it is refs/tags/. For example, refs/heads/feature-branch-1. - "ref_name": refName.String(), // string, The short ref name of the branch or tag that triggered the workflow run. This value matches the branch or tag name shown on GitHub. For example, feature-branch-1. + "ref_name": refName.ShortName(), // string, The short ref name of the branch or tag that triggered the workflow run. This value matches the branch or tag name shown on GitHub. For example, feature-branch-1. "ref_protected": false, // boolean, true if branch protections are configured for the ref that triggered the workflow run. "ref_type": refName.RefType(), // string, The type of ref that triggered the workflow run. Valid values are branch or tag. "path": "", // string, Path on the runner to the file that sets system PATH variables from workflow commands. This file is unique to the current step and is a different file for each step in a job. For more information, see "Workflow commands for GitHub Actions." diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 4f0f637fa57d9..fa7f66f3ab38d 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -4,7 +4,6 @@ package packages import ( - gocontext "context" "net/http" "regexp" "strings" @@ -96,7 +95,7 @@ func verifyAuth(r *web.Route, authMethods []auth.Method) { // CommonRoutes provide endpoints for most package managers (except containers - see below) // These are mounted on `/api/packages` (not `/api/v1/packages`) -func CommonRoutes(ctx gocontext.Context) *web.Route { +func CommonRoutes() *web.Route { r := web.NewRoute() r.Use(context.PackageContexter()) @@ -590,7 +589,7 @@ func CommonRoutes(ctx gocontext.Context) *web.Route { // ContainerRoutes provides endpoints that implement the OCI API to serve containers // These have to be mounted on `/v2/...` to comply with the OCI spec: // https://github.com/opencontainers/distribution-spec/blob/main/spec.md -func ContainerRoutes(ctx gocontext.Context) *web.Route { +func ContainerRoutes() *web.Route { r := web.NewRoute() r.Use(context.PackageContexter()) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 37361a8b963bd..be66cc5240812 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -64,7 +64,6 @@ package v1 import ( - gocontext "context" "fmt" "net/http" "strings" @@ -705,7 +704,7 @@ func buildAuthGroup() *auth.Group { } // Routes registers all v1 APIs routes to web application. -func Routes(ctx gocontext.Context) *web.Route { +func Routes() *web.Route { m := web.NewRoute() m.Use(securityHeaders()) @@ -722,13 +721,8 @@ func Routes(ctx gocontext.Context) *web.Route { } m.Use(context.APIContexter()) - group := buildAuthGroup() - if err := group.Init(ctx); err != nil { - log.Error("Could not initialize '%s' auth method, error: %s", group.Name(), err) - } - // Get user from session if logged in. - m.Use(auth.APIAuth(group)) + m.Use(auth.APIAuth(buildAuthGroup())) m.Use(auth.VerifyAuthWithOptionsAPI(&auth.VerifyOptions{ SignInRequired: setting.Service.RequireSignInView, diff --git a/routers/api/v1/misc/markup_test.go b/routers/api/v1/misc/markup_test.go index fdf540fd65b76..bab06b3e6626a 100644 --- a/routers/api/v1/misc/markup_test.go +++ b/routers/api/v1/misc/markup_test.go @@ -7,18 +7,14 @@ import ( go_context "context" "io" "net/http" - "net/http/httptest" - "net/url" "strings" "testing" - "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/modules/web/middleware" "github.com/stretchr/testify/assert" ) @@ -29,34 +25,16 @@ const ( AppSubURL = AppURL + Repo + "/" ) -func createAPIContext(req *http.Request) (*context.APIContext, *httptest.ResponseRecorder) { - resp := httptest.NewRecorder() - base, baseCleanUp := context.NewBaseContext(resp, req) - base.Data = middleware.ContextData{} - c := &context.APIContext{Base: base} - _ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later - - return c, resp -} - func testRenderMarkup(t *testing.T, mode, filePath, text, responseBody string, responseCode int) { setting.AppURL = AppURL - options := api.MarkupOption{ Mode: mode, - Text: "", + Text: text, Context: Repo, Wiki: true, FilePath: filePath, } - requrl, _ := url.Parse(util.URLJoin(AppURL, "api", "v1", "markup")) - req := &http.Request{ - Method: "POST", - URL: requrl, - } - ctx, resp := createAPIContext(req) - - options.Text = text + ctx, resp := test.MockAPIContext(t, "POST /api/v1/markup") web.SetForm(ctx, &options) Markup(ctx) assert.Equal(t, responseBody, resp.Body.String()) @@ -66,21 +44,13 @@ func testRenderMarkup(t *testing.T, mode, filePath, text, responseBody string, r func testRenderMarkdown(t *testing.T, mode, text, responseBody string, responseCode int) { setting.AppURL = AppURL - options := api.MarkdownOption{ Mode: mode, - Text: "", + Text: text, Context: Repo, Wiki: true, } - requrl, _ := url.Parse(util.URLJoin(AppURL, "api", "v1", "markdown")) - req := &http.Request{ - Method: "POST", - URL: requrl, - } - ctx, resp := createAPIContext(req) - - options.Text = text + ctx, resp := test.MockAPIContext(t, "POST /api/v1/markdown") web.SetForm(ctx, &options) Markdown(ctx) assert.Equal(t, responseBody, resp.Body.String()) @@ -187,19 +157,12 @@ var simpleCases = []string{ func TestAPI_RenderSimple(t *testing.T) { setting.AppURL = AppURL - options := api.MarkdownOption{ Mode: "markdown", Text: "", Context: Repo, } - requrl, _ := url.Parse(util.URLJoin(AppURL, "api", "v1", "markdown")) - req := &http.Request{ - Method: "POST", - URL: requrl, - } - ctx, resp := createAPIContext(req) - + ctx, resp := test.MockAPIContext(t, "POST /api/v1/markdown") for i := 0; i < len(simpleCases); i += 2 { options.Text = simpleCases[i] web.SetForm(ctx, &options) @@ -211,14 +174,7 @@ func TestAPI_RenderSimple(t *testing.T) { func TestAPI_RenderRaw(t *testing.T) { setting.AppURL = AppURL - - requrl, _ := url.Parse(util.URLJoin(AppURL, "api", "v1", "markdown")) - req := &http.Request{ - Method: "POST", - URL: requrl, - } - ctx, resp := createAPIContext(req) - + ctx, resp := test.MockAPIContext(t, "POST /api/v1/markdown") for i := 0; i < len(simpleCases); i += 2 { ctx.Req.Body = io.NopCloser(strings.NewReader(simpleCases[i])) MarkdownRaw(ctx) diff --git a/routers/api/v1/repo/hook_test.go b/routers/api/v1/repo/hook_test.go index 56658b45d59b8..b43a22cd55f85 100644 --- a/routers/api/v1/repo/hook_test.go +++ b/routers/api/v1/repo/hook_test.go @@ -17,7 +17,7 @@ import ( func TestTestHook(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockAPIContext(t, "user2/repo1/wiki/_pages") + ctx, _ := test.MockAPIContext(t, "user2/repo1/wiki/_pages") ctx.SetParams(":id", "1") test.LoadRepo(t, ctx, 1) test.LoadRepoCommit(t, ctx) diff --git a/routers/api/v1/repo/repo_test.go b/routers/api/v1/repo/repo_test.go index e1bdea5c82bd8..7593a87c2c0b2 100644 --- a/routers/api/v1/repo/repo_test.go +++ b/routers/api/v1/repo/repo_test.go @@ -19,7 +19,7 @@ import ( func TestRepoEdit(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockAPIContext(t, "user2/repo1") + ctx, _ := test.MockAPIContext(t, "user2/repo1") test.LoadRepo(t, ctx, 1) test.LoadUser(t, ctx, 2) ctx.Repo.Owner = ctx.Doer @@ -65,7 +65,7 @@ func TestRepoEdit(t *testing.T) { func TestRepoEditNameChange(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockAPIContext(t, "user2/repo1") + ctx, _ := test.MockAPIContext(t, "user2/repo1") test.LoadRepo(t, ctx, 1) test.LoadUser(t, ctx, 2) ctx.Repo.Owner = ctx.Doer diff --git a/routers/init.go b/routers/init.go index 725e5c52ba4fc..ddbabcc397447 100644 --- a/routers/init.go +++ b/routers/init.go @@ -28,7 +28,6 @@ import ( "code.gitea.io/gitea/modules/system" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/translation" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" actions_router "code.gitea.io/gitea/routers/api/actions" packages_router "code.gitea.io/gitea/routers/api/packages" @@ -101,21 +100,16 @@ func syncAppConfForGit(ctx context.Context) error { return nil } -// GlobalInitInstalled is for global installed configuration. -func GlobalInitInstalled(ctx context.Context) { - if !setting.InstallLock { - log.Fatal("Gitea is not installed") - } +func InitWebInstallPage(ctx context.Context) { + translation.InitLocales(ctx) + setting.LoadSettingsForInstall() + mustInit(svg.Init) +} +// InitWebInstalled is for global installed configuration. +func InitWebInstalled(ctx context.Context) { mustInitCtx(ctx, git.InitFull) - log.Info("Gitea Version: %s%s", setting.AppVer, setting.AppBuiltWith) - log.Info("Git Version: %s (home: %s)", git.VersionInfo(), git.HomeDir()) - log.Info("AppPath: %s", setting.AppPath) - log.Info("AppWorkPath: %s", setting.AppWorkPath) - log.Info("Custom path: %s", setting.CustomPath) - log.Info("Log path: %s", setting.Log.RootPath) - log.Info("Configuration file: %s", setting.CustomConf) - log.Info("Run Mode: %s", util.ToTitleCase(setting.RunMode)) + log.Info("Git version: %s (home: %s)", git.VersionInfo(), git.HomeDir()) // Setup i18n translation.InitLocales(ctx) @@ -174,27 +168,27 @@ func GlobalInitInstalled(ctx context.Context) { } // NormalRoutes represents non install routes -func NormalRoutes(ctx context.Context) *web.Route { +func NormalRoutes() *web.Route { _ = templates.HTMLRenderer() r := web.NewRoute() r.Use(common.ProtocolMiddlewares()...) - r.Mount("/", web_routers.Routes(ctx)) - r.Mount("/api/v1", apiv1.Routes(ctx)) + r.Mount("/", web_routers.Routes()) + r.Mount("/api/v1", apiv1.Routes()) r.Mount("/api/internal", private.Routes()) r.Post("/-/fetch-redirect", common.FetchRedirectDelegate) if setting.Packages.Enabled { // This implements package support for most package managers - r.Mount("/api/packages", packages_router.CommonRoutes(ctx)) + r.Mount("/api/packages", packages_router.CommonRoutes()) // This implements the OCI API (Note this is not preceded by /api but is instead /v2) - r.Mount("/v2", packages_router.ContainerRoutes(ctx)) + r.Mount("/v2", packages_router.ContainerRoutes()) } if setting.Actions.Enabled { prefix := "/api/actions" - r.Mount(prefix, actions_router.Routes(ctx, prefix)) + r.Mount(prefix, actions_router.Routes(prefix)) // TODO: Pipeline api used for runner internal communication with gitea server. but only artifact is used for now. // In Github, it uses ACTIONS_RUNTIME_URL=https://pipelines.actions.githubusercontent.com/fLgcSHkPGySXeIFrg8W8OBSfeg3b5Fls1A1CwX566g8PayEGlg/ diff --git a/routers/install/install.go b/routers/install/install.go index 4dba64df01d6f..f121f313769d1 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -32,6 +32,7 @@ import ( "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web/middleware" + "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/forms" "gitea.com/go-chi/session" @@ -98,7 +99,6 @@ func Install(ctx *context.Context) { form.DbName = setting.Database.Name form.DbPath = setting.Database.Path form.DbSchema = setting.Database.Schema - form.Charset = setting.Database.Charset curDBType := setting.Database.Type.String() var isCurDBTypeSupported bool @@ -268,7 +268,6 @@ func SubmitInstall(ctx *context.Context) { setting.Database.Name = form.DbName setting.Database.Schema = form.DbSchema setting.Database.SSLMode = form.SSLMode - setting.Database.Charset = form.Charset setting.Database.Path = form.DbPath setting.Database.LogSQL = !setting.IsProd @@ -370,11 +369,16 @@ func SubmitInstall(ctx *context.Context) { } // Save settings. - cfg, err := setting.NewConfigProviderFromFile(&setting.Options{CustomConf: setting.CustomConf, AllowEmpty: true}) + cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) if err != nil { log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err) } + cfg.Section("").Key("APP_NAME").SetValue(form.AppName) + cfg.Section("").Key("RUN_USER").SetValue(form.RunUser) + cfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath) + cfg.Section("").Key("RUN_MODE").SetValue("prod") + cfg.Section("database").Key("DB_TYPE").SetValue(setting.Database.Type.String()) cfg.Section("database").Key("HOST").SetValue(setting.Database.Host) cfg.Section("database").Key("NAME").SetValue(setting.Database.Name) @@ -382,17 +386,15 @@ func SubmitInstall(ctx *context.Context) { cfg.Section("database").Key("PASSWD").SetValue(setting.Database.Passwd) cfg.Section("database").Key("SCHEMA").SetValue(setting.Database.Schema) cfg.Section("database").Key("SSL_MODE").SetValue(setting.Database.SSLMode) - cfg.Section("database").Key("CHARSET").SetValue(setting.Database.Charset) cfg.Section("database").Key("PATH").SetValue(setting.Database.Path) cfg.Section("database").Key("LOG_SQL").SetValue("false") // LOG_SQL is rarely helpful - cfg.Section("").Key("APP_NAME").SetValue(form.AppName) cfg.Section("repository").Key("ROOT").SetValue(form.RepoRootPath) - cfg.Section("").Key("RUN_USER").SetValue(form.RunUser) cfg.Section("server").Key("SSH_DOMAIN").SetValue(form.Domain) cfg.Section("server").Key("DOMAIN").SetValue(form.Domain) cfg.Section("server").Key("HTTP_PORT").SetValue(form.HTTPPort) cfg.Section("server").Key("ROOT_URL").SetValue(form.AppURL) + cfg.Section("server").Key("APP_DATA_PATH").SetValue(setting.AppDataPath) if form.SSHPort == 0 { cfg.Section("server").Key("DISABLE_SSH").SetValue("true") @@ -449,8 +451,6 @@ func SubmitInstall(ctx *context.Context) { cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(fmt.Sprint(form.NoReplyAddress)) cfg.Section("cron.update_checker").Key("ENABLED").SetValue(fmt.Sprint(form.EnableUpdateChecker)) - cfg.Section("").Key("RUN_MODE").SetValue("prod") - cfg.Section("session").Key("PROVIDER").SetValue("file") cfg.Section("log").Key("MODE").MustString("console") @@ -513,7 +513,13 @@ func SubmitInstall(ctx *context.Context) { // ---- All checks are passed // Reload settings (and re-initialize database connection) - reloadSettings(ctx) + setting.InitCfgProvider(setting.CustomConf) + setting.LoadCommonSettings() + setting.MustInstalled() + setting.LoadDBSetting() + if err := common.InitDBEngine(ctx); err != nil { + log.Fatal("ORM engine initialization failed: %v", err) + } // Create admin account if len(form.AdminName) > 0 { diff --git a/routers/install/setting.go b/routers/install/setting.go deleted file mode 100644 index c14843d8ee4d3..0000000000000 --- a/routers/install/setting.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package install - -import ( - "context" - - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/svg" - "code.gitea.io/gitea/modules/translation" - "code.gitea.io/gitea/routers/common" -) - -// PreloadSettings preloads the configuration to check if we need to run install -func PreloadSettings(ctx context.Context) bool { - setting.Init(&setting.Options{ - AllowEmpty: true, - }) - if !setting.InstallLock { - log.Info("AppPath: %s", setting.AppPath) - log.Info("AppWorkPath: %s", setting.AppWorkPath) - log.Info("Custom path: %s", setting.CustomPath) - log.Info("Log path: %s", setting.Log.RootPath) - log.Info("Configuration file: %s", setting.CustomConf) - log.Info("Prepare to run install page") - translation.InitLocales(ctx) - if setting.EnableSQLite3 { - log.Info("SQLite3 is supported") - } - - setting.LoadSettingsForInstall() - _ = svg.Init() - } - - return !setting.InstallLock -} - -// reloadSettings reloads the existing settings and starts up the database -func reloadSettings(ctx context.Context) { - setting.Init(&setting.Options{}) - setting.LoadDBSetting() - if setting.InstallLock { - if err := common.InitDBEngine(ctx); err == nil { - log.Info("ORM engine initialization successful!") - } else { - log.Fatal("ORM engine initialization failed: %v", err) - } - } -} diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go index be662c22efdcc..2c6989a71dbb3 100644 --- a/routers/web/admin/config.go +++ b/routers/web/admin/config.go @@ -8,7 +8,6 @@ import ( "fmt" "net/http" "net/url" - "os" "strconv" "strings" @@ -167,20 +166,6 @@ func Config(ctx *context.Context) { ctx.Data["SessionConfig"] = sessionCfg ctx.Data["Git"] = setting.Git - - type envVar struct { - Name, Value string - } - - envVars := map[string]*envVar{} - if len(os.Getenv("GITEA_WORK_DIR")) > 0 { - envVars["GITEA_WORK_DIR"] = &envVar{"GITEA_WORK_DIR", os.Getenv("GITEA_WORK_DIR")} - } - if len(os.Getenv("GITEA_CUSTOM")) > 0 { - envVars["GITEA_CUSTOM"] = &envVar{"GITEA_CUSTOM", os.Getenv("GITEA_CUSTOM")} - } - - ctx.Data["EnvVars"] = envVars ctx.Data["AccessLogTemplate"] = setting.Log.AccessLogTemplate ctx.Data["LogSQL"] = setting.Database.LogSQL diff --git a/routers/web/admin/users_test.go b/routers/web/admin/users_test.go index ed58a54eef939..19d6d7294da6f 100644 --- a/routers/web/admin/users_test.go +++ b/routers/web/admin/users_test.go @@ -19,7 +19,7 @@ import ( func TestNewUserPost_MustChangePassword(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "admin/users/new") + ctx, _ := test.MockContext(t, "admin/users/new") u := unittest.AssertExistsAndLoadBean(t, &user_model.User{ IsAdmin: true, @@ -56,7 +56,7 @@ func TestNewUserPost_MustChangePassword(t *testing.T) { func TestNewUserPost_MustChangePasswordFalse(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "admin/users/new") + ctx, _ := test.MockContext(t, "admin/users/new") u := unittest.AssertExistsAndLoadBean(t, &user_model.User{ IsAdmin: true, @@ -93,7 +93,7 @@ func TestNewUserPost_MustChangePasswordFalse(t *testing.T) { func TestNewUserPost_InvalidEmail(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "admin/users/new") + ctx, _ := test.MockContext(t, "admin/users/new") u := unittest.AssertExistsAndLoadBean(t, &user_model.User{ IsAdmin: true, @@ -123,7 +123,7 @@ func TestNewUserPost_InvalidEmail(t *testing.T) { func TestNewUserPost_VisibilityDefaultPublic(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "admin/users/new") + ctx, _ := test.MockContext(t, "admin/users/new") u := unittest.AssertExistsAndLoadBean(t, &user_model.User{ IsAdmin: true, @@ -161,7 +161,7 @@ func TestNewUserPost_VisibilityDefaultPublic(t *testing.T) { func TestNewUserPost_VisibilityPrivate(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "admin/users/new") + ctx, _ := test.MockContext(t, "admin/users/new") u := unittest.AssertExistsAndLoadBean(t, &user_model.User{ IsAdmin: true, diff --git a/routers/web/org/projects_test.go b/routers/web/org/projects_test.go index 3450fa8e72db4..08a97b7d2deec 100644 --- a/routers/web/org/projects_test.go +++ b/routers/web/org/projects_test.go @@ -15,7 +15,7 @@ import ( func TestCheckProjectBoardChangePermissions(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/-/projects/4/4") + ctx, _ := test.MockContext(t, "user2/-/projects/4/4") test.LoadUser(t, ctx, 2) ctx.ContextUser = ctx.Doer // user2 ctx.SetParams(":id", "4") diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index 10acb468542ea..e1e07b5a72966 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -5,6 +5,7 @@ package actions import ( "bytes" + "fmt" "net/http" actions_model "code.gitea.io/gitea/models/actions" @@ -16,6 +17,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/routers/web/repo" "code.gitea.io/gitea/services/convert" "github.com/nektos/act/pkg/model" @@ -125,7 +127,16 @@ func List(ctx *context.Context) { } workflow := ctx.FormString("workflow") + actorID := ctx.FormInt64("actor") + status := ctx.FormInt("status") ctx.Data["CurWorkflow"] = workflow + // if status or actor query param is not given to frontend href, (href="//actions") + // they will be 0 by default, which indicates get all status or actors + ctx.Data["CurActor"] = actorID + ctx.Data["CurStatus"] = status + if actorID > 0 || status > int(actions_model.StatusUnknown) { + ctx.Data["IsFiltered"] = true + } opts := actions_model.FindRunOptions{ ListOptions: db.ListOptions{ @@ -134,6 +145,8 @@ func List(ctx *context.Context) { }, RepoID: ctx.Repo.Repository.ID, WorkflowFileName: workflow, + TriggerUserID: actorID, + Status: actions_model.Status(status), } runs, total, err := actions_model.FindRuns(ctx, opts) @@ -153,9 +166,20 @@ func List(ctx *context.Context) { ctx.Data["Runs"] = runs + actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + ctx.Data["Actors"] = repo.MakeSelfOnTop(ctx, actors) + + ctx.Data["StatusInfoList"] = actions_model.GetStatusInfoList(ctx) + pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5) pager.SetDefaultParams(ctx) pager.AddParamString("workflow", workflow) + pager.AddParamString("actor", fmt.Sprint(actorID)) + pager.AddParamString("status", fmt.Sprint(status)) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplListActions) diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 7433a0a56b16c..2fea8a9532b90 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -685,7 +685,11 @@ func UploadFilePost(ctx *context.Context) { message := strings.TrimSpace(form.CommitSummary) if len(message) == 0 { - message = ctx.Tr("repo.editor.upload_files_to_dir", form.TreePath) + dir := form.TreePath + if dir == "" { + dir = "/" + } + message = ctx.Tr("repo.editor.upload_files_to_dir", dir) } form.CommitMessage = strings.TrimSpace(form.CommitMessage) diff --git a/routers/web/repo/editor_test.go b/routers/web/repo/editor_test.go index 1e53aac9b0938..52dded68b793e 100644 --- a/routers/web/repo/editor_test.go +++ b/routers/web/repo/editor_test.go @@ -41,7 +41,7 @@ func TestCleanUploadName(t *testing.T) { func TestGetUniquePatchBranchName(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1") + ctx, _ := test.MockContext(t, "user2/repo1") ctx.SetParams(":id", "1") test.LoadRepo(t, ctx, 1) test.LoadRepoCommit(t, ctx) @@ -56,7 +56,7 @@ func TestGetUniquePatchBranchName(t *testing.T) { func TestGetClosestParentWithFiles(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1") + ctx, _ := test.MockContext(t, "user2/repo1") ctx.SetParams(":id", "1") test.LoadRepo(t, ctx, 1) test.LoadRepoCommit(t, ctx) diff --git a/routers/web/repo/helper.go b/routers/web/repo/helper.go index 6f9ca4874b002..fb5ada1bdb476 100644 --- a/routers/web/repo/helper.go +++ b/routers/web/repo/helper.go @@ -10,7 +10,7 @@ import ( "code.gitea.io/gitea/modules/context" ) -func makeSelfOnTop(ctx *context.Context, users []*user.User) []*user.User { +func MakeSelfOnTop(ctx *context.Context, users []*user.User) []*user.User { if ctx.Doer != nil { sort.Slice(users, func(i, j int) bool { if users[i].ID == users[j].ID { diff --git a/routers/web/repo/helper_test.go b/routers/web/repo/helper_test.go index e9ab44fe69f95..226e2e81f4dde 100644 --- a/routers/web/repo/helper_test.go +++ b/routers/web/repo/helper_test.go @@ -13,15 +13,15 @@ import ( ) func TestMakeSelfOnTop(t *testing.T) { - users := makeSelfOnTop(&context.Context{}, []*user.User{{ID: 2}, {ID: 1}}) + users := MakeSelfOnTop(&context.Context{}, []*user.User{{ID: 2}, {ID: 1}}) assert.Len(t, users, 2) assert.EqualValues(t, 2, users[0].ID) - users = makeSelfOnTop(&context.Context{Doer: &user.User{ID: 1}}, []*user.User{{ID: 2}, {ID: 1}}) + users = MakeSelfOnTop(&context.Context{Doer: &user.User{ID: 1}}, []*user.User{{ID: 2}, {ID: 1}}) assert.Len(t, users, 2) assert.EqualValues(t, 1, users[0].ID) - users = makeSelfOnTop(&context.Context{Doer: &user.User{ID: 2}}, []*user.User{{ID: 2}, {ID: 1}}) + users = MakeSelfOnTop(&context.Context{Doer: &user.User{ID: 2}}, []*user.User{{ID: 2}, {ID: 1}}) assert.Len(t, users, 2) assert.EqualValues(t, 2, users[0].ID) } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 9f087edc72d5f..a9ce1cc1e752d 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -312,7 +312,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti ctx.ServerError("GetRepoAssignees", err) return } - ctx.Data["Assignees"] = makeSelfOnTop(ctx, assigneeUsers) + ctx.Data["Assignees"] = MakeSelfOnTop(ctx, assigneeUsers) handleTeamMentions(ctx) if ctx.Written() { @@ -508,7 +508,7 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R ctx.ServerError("GetRepoAssignees", err) return } - ctx.Data["Assignees"] = makeSelfOnTop(ctx, assigneeUsers) + ctx.Data["Assignees"] = MakeSelfOnTop(ctx, assigneeUsers) handleTeamMentions(ctx) } @@ -2705,6 +2705,20 @@ func ListIssues(ctx *context.Context) { ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues)) } +func BatchDeleteIssues(ctx *context.Context) { + issues := getActionIssues(ctx) + if ctx.Written() { + return + } + for _, issue := range issues { + if err := issue_service.DeleteIssue(ctx, ctx.Doer, ctx.Repo.GitRepo, issue); err != nil { + ctx.ServerError("DeleteIssue", err) + return + } + } + ctx.JSONOK() +} + // UpdateIssueStatus change issue's status func UpdateIssueStatus(ctx *context.Context) { issues := getActionIssues(ctx) @@ -2740,9 +2754,7 @@ func UpdateIssueStatus(ctx *context.Context) { } } } - ctx.JSON(http.StatusOK, map[string]interface{}{ - "ok": true, - }) + ctx.JSONOK() } // NewComment create a comment for issue @@ -3475,7 +3487,7 @@ func IssuePosters(ctx *context.Context) { } } - posters = makeSelfOnTop(ctx, posters) + posters = MakeSelfOnTop(ctx, posters) resp := &userSearchResponse{} resp.Results = make([]*userSearchInfo, len(posters)) diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go index c24fe898b6bca..4c9a359438015 100644 --- a/routers/web/repo/issue_label_test.go +++ b/routers/web/repo/issue_label_test.go @@ -32,7 +32,7 @@ func int64SliceToCommaSeparated(a []int64) string { func TestInitializeLabels(t *testing.T) { unittest.PrepareTestEnv(t) assert.NoError(t, repository.LoadRepoConfig()) - ctx := test.MockContext(t, "user2/repo1/labels/initialize") + ctx, _ := test.MockContext(t, "user2/repo1/labels/initialize") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 2) web.SetForm(ctx, &forms.InitializeLabelsForm{TemplateName: "Default"}) @@ -57,7 +57,7 @@ func TestRetrieveLabels(t *testing.T) { {1, "leastissues", []int64{2, 1}}, {2, "", []int64{}}, } { - ctx := test.MockContext(t, "user/repo/issues") + ctx, _ := test.MockContext(t, "user/repo/issues") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, testCase.RepoID) ctx.Req.Form.Set("sort", testCase.Sort) @@ -75,7 +75,7 @@ func TestRetrieveLabels(t *testing.T) { func TestNewLabel(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/labels/edit") + ctx, _ := test.MockContext(t, "user2/repo1/labels/edit") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) web.SetForm(ctx, &forms.CreateLabelForm{ @@ -93,7 +93,7 @@ func TestNewLabel(t *testing.T) { func TestUpdateLabel(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/labels/edit") + ctx, _ := test.MockContext(t, "user2/repo1/labels/edit") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) web.SetForm(ctx, &forms.CreateLabelForm{ @@ -113,7 +113,7 @@ func TestUpdateLabel(t *testing.T) { func TestDeleteLabel(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/labels/delete") + ctx, _ := test.MockContext(t, "user2/repo1/labels/delete") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) ctx.Req.Form.Set("id", "2") @@ -126,7 +126,7 @@ func TestDeleteLabel(t *testing.T) { func TestUpdateIssueLabel_Clear(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/issues/labels") + ctx, _ := test.MockContext(t, "user2/repo1/issues/labels") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) ctx.Req.Form.Set("issue_ids", "1,3") @@ -151,7 +151,7 @@ func TestUpdateIssueLabel_Toggle(t *testing.T) { {"toggle", []int64{1, 2}, 2, true}, } { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/issues/labels") + ctx, _ := test.MockContext(t, "user2/repo1/issues/labels") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) ctx.Req.Form.Set("issue_ids", int64SliceToCommaSeparated(testCase.IssueIDs)) diff --git a/routers/web/repo/middlewares.go b/routers/web/repo/middlewares.go index 5c38b31154fe3..216550ca996c6 100644 --- a/routers/web/repo/middlewares.go +++ b/routers/web/repo/middlewares.go @@ -5,6 +5,7 @@ package repo import ( "fmt" + "strconv" system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" @@ -88,3 +89,27 @@ func SetWhitespaceBehavior(ctx *context.Context) { ctx.Data["WhitespaceBehavior"] = whitespaceBehavior } } + +// SetShowOutdatedComments set the show outdated comments option as context variable +func SetShowOutdatedComments(ctx *context.Context) { + showOutdatedCommentsValue := ctx.FormString("show-outdated") + // var showOutdatedCommentsValue string + + if showOutdatedCommentsValue != "true" && showOutdatedCommentsValue != "false" { + // invalid or no value for this form string -> use default or stored user setting + if ctx.IsSigned { + showOutdatedCommentsValue, _ = user_model.GetUserSetting(ctx.Doer.ID, user_model.SettingsKeyShowOutdatedComments, "false") + } else { + // not logged in user -> use the default value + showOutdatedCommentsValue = "false" + } + } else { + // valid value -> update user setting if user is logged in + if ctx.IsSigned { + _ = user_model.SetUserSetting(ctx.Doer.ID, user_model.SettingsKeyShowOutdatedComments, showOutdatedCommentsValue) + } + } + + showOutdatedComments, _ := strconv.ParseBool(showOutdatedCommentsValue) + ctx.Data["ShowOutdatedComments"] = showOutdatedComments +} diff --git a/routers/web/repo/projects_test.go b/routers/web/repo/projects_test.go index c712902ea9f1a..e2797772a8f6e 100644 --- a/routers/web/repo/projects_test.go +++ b/routers/web/repo/projects_test.go @@ -14,7 +14,7 @@ import ( func TestCheckProjectBoardChangePermissions(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/projects/1/2") + ctx, _ := test.MockContext(t, "user2/repo1/projects/1/2") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) ctx.SetParams(":id", "1") diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 09dbc23eac2cc..f2a58a35a78a8 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -37,7 +37,6 @@ import ( "code.gitea.io/gitea/modules/upload" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/routers/utils" asymkey_service "code.gitea.io/gitea/services/asymkey" "code.gitea.io/gitea/services/automerge" @@ -762,7 +761,7 @@ func ViewPullFiles(ctx *context.Context) { "numberOfViewedFiles": diff.NumViewedFiles, } - if err = diff.LoadComments(ctx, issue, ctx.Doer); err != nil { + if err = diff.LoadComments(ctx, issue, ctx.Doer, ctx.Data["ShowOutdatedComments"].(bool)); err != nil { ctx.ServerError("LoadComments", err) return } @@ -810,7 +809,7 @@ func ViewPullFiles(ctx *context.Context) { ctx.ServerError("GetRepoAssignees", err) return } - ctx.Data["Assignees"] = makeSelfOnTop(ctx, assigneeUsers) + ctx.Data["Assignees"] = MakeSelfOnTop(ctx, assigneeUsers) handleTeamMentions(ctx) if ctx.Written() { @@ -1206,36 +1205,12 @@ func CompareAndPullRequestPost(ctx *context.Context) { } if ctx.HasError() { - middleware.AssignForm(form, ctx.Data) - - // This stage is already stop creating new pull request, so it does not matter if it has - // something to compare or not. - PrepareCompareDiff(ctx, ci, - gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string))) - if ctx.Written() { - return - } - - if len(form.Title) > 255 { - var trailer string - form.Title, trailer = util.SplitStringAtByteN(form.Title, 255) - - form.Content = trailer + "\n\n" + form.Content - } - middleware.AssignForm(form, ctx.Data) - - ctx.HTML(http.StatusOK, tplCompareDiff) + ctx.JSONError(ctx.GetErrMsg()) return } if util.IsEmptyString(form.Title) { - PrepareCompareDiff(ctx, ci, - gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string))) - if ctx.Written() { - return - } - - ctx.RenderWithErr(ctx.Tr("repo.issues.new.title_empty"), tplCompareDiff, form) + ctx.JSONError(ctx.Tr("repo.issues.new.title_empty")) return } @@ -1278,20 +1253,20 @@ func CompareAndPullRequestPost(ctx *context.Context) { pushrejErr := err.(*git.ErrPushRejected) message := pushrejErr.Message if len(message) == 0 { - ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected_no_message")) - } else { - flashError, err := ctx.RenderToString(tplAlertDetails, map[string]interface{}{ - "Message": ctx.Tr("repo.pulls.push_rejected"), - "Summary": ctx.Tr("repo.pulls.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(pushrejErr.Message), - }) - if err != nil { - ctx.ServerError("CompareAndPullRequest.HTMLString", err) - return - } - ctx.Flash.Error(flashError) + ctx.JSONError(ctx.Tr("repo.pulls.push_rejected_no_message")) + return } - ctx.Redirect(pullIssue.Link()) + flashError, err := ctx.RenderToString(tplAlertDetails, map[string]interface{}{ + "Message": ctx.Tr("repo.pulls.push_rejected"), + "Summary": ctx.Tr("repo.pulls.push_rejected_summary"), + "Details": utils.SanitizeFlashErrorString(pushrejErr.Message), + }) + if err != nil { + ctx.ServerError("CompareAndPullRequest.HTMLString", err) + return + } + ctx.Flash.Error(flashError) + ctx.JSONRedirect(pullIssue.Link()) // FIXME: it's unfriendly, and will make the content lost return } ctx.ServerError("NewPullRequest", err) @@ -1299,7 +1274,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { } log.Trace("Pull request created: %d/%d", repo.ID, pullIssue.ID) - ctx.Redirect(pullIssue.Link()) + ctx.JSONRedirect(pullIssue.Link()) } // CleanUpPullRequest responses for delete merged branch when PR has been merged diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go index 69d36ff4a47ef..5aa5811367056 100644 --- a/routers/web/repo/pull_review.go +++ b/routers/web/repo/pull_review.go @@ -159,7 +159,7 @@ func UpdateResolveConversation(ctx *context.Context) { } func renderConversation(ctx *context.Context, comment *issues_model.Comment) { - comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line) + comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line, ctx.Data["ShowOutdatedComments"].(bool)) if err != nil { ctx.ServerError("FetchCodeCommentsByLine", err) return diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index afba1f18bfe69..5fddddb344080 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -349,7 +349,7 @@ func NewRelease(ctx *context.Context) { ctx.ServerError("GetRepoAssignees", err) return } - ctx.Data["Assignees"] = makeSelfOnTop(ctx, assigneeUsers) + ctx.Data["Assignees"] = MakeSelfOnTop(ctx, assigneeUsers) upload.AddUploadContext(ctx, "release") ctx.HTML(http.StatusOK, tplReleaseNew) @@ -517,7 +517,7 @@ func EditRelease(ctx *context.Context) { ctx.ServerError("GetRepoAssignees", err) return } - ctx.Data["Assignees"] = makeSelfOnTop(ctx, assigneeUsers) + ctx.Data["Assignees"] = MakeSelfOnTop(ctx, assigneeUsers) ctx.HTML(http.StatusOK, tplReleaseNew) } diff --git a/routers/web/repo/release_test.go b/routers/web/repo/release_test.go index 9ec1b4d3492c5..07e349811e399 100644 --- a/routers/web/repo/release_test.go +++ b/routers/web/repo/release_test.go @@ -47,7 +47,7 @@ func TestNewReleasePost(t *testing.T) { } { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/releases/new") + ctx, _ := test.MockContext(t, "user2/repo1/releases/new") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) test.LoadGitRepo(t, ctx) @@ -67,7 +67,7 @@ func TestNewReleasePost(t *testing.T) { func TestNewReleasesList(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo-release/releases") + ctx, _ := test.MockContext(t, "user2/repo-release/releases") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 57) test.LoadGitRepo(t, ctx) diff --git a/routers/web/repo/setting/secrets.go b/routers/web/repo/setting/secrets.go index 444f16f86c195..3d7a0576027e9 100644 --- a/routers/web/repo/setting/secrets.go +++ b/routers/web/repo/setting/secrets.go @@ -92,6 +92,12 @@ func SecretsPost(ctx *context.Context) { ctx.ServerError("getSecretsCtx", err) return } + + if ctx.HasError() { + ctx.JSONError(ctx.GetErrMsg()) + return + } + shared.PerformSecretsPost( ctx, sCtx.OwnerID, diff --git a/routers/web/repo/setting/variables.go b/routers/web/repo/setting/variables.go new file mode 100644 index 0000000000000..1005d1d9c6106 --- /dev/null +++ b/routers/web/repo/setting/variables.go @@ -0,0 +1,119 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "errors" + "net/http" + + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" + shared "code.gitea.io/gitea/routers/web/shared/actions" +) + +const ( + tplRepoVariables base.TplName = "repo/settings/actions" + tplOrgVariables base.TplName = "org/settings/actions" + tplUserVariables base.TplName = "user/settings/actions" +) + +type variablesCtx struct { + OwnerID int64 + RepoID int64 + IsRepo bool + IsOrg bool + IsUser bool + VariablesTemplate base.TplName + RedirectLink string +} + +func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) { + if ctx.Data["PageIsRepoSettings"] == true { + return &variablesCtx{ + RepoID: ctx.Repo.Repository.ID, + IsRepo: true, + VariablesTemplate: tplRepoVariables, + RedirectLink: ctx.Repo.RepoLink + "/settings/actions/variables", + }, nil + } + + if ctx.Data["PageIsOrgSettings"] == true { + return &variablesCtx{ + OwnerID: ctx.ContextUser.ID, + IsOrg: true, + VariablesTemplate: tplOrgVariables, + RedirectLink: ctx.Org.OrgLink + "/settings/actions/variables", + }, nil + } + + if ctx.Data["PageIsUserSettings"] == true { + return &variablesCtx{ + OwnerID: ctx.Doer.ID, + IsUser: true, + VariablesTemplate: tplUserVariables, + RedirectLink: setting.AppSubURL + "/user/settings/actions/variables", + }, nil + } + + return nil, errors.New("unable to set Variables context") +} + +func Variables(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("actions.variables") + ctx.Data["PageType"] = "variables" + ctx.Data["PageIsSharedSettingsVariables"] = true + + vCtx, err := getVariablesCtx(ctx) + if err != nil { + ctx.ServerError("getVariablesCtx", err) + return + } + + shared.SetVariablesContext(ctx, vCtx.OwnerID, vCtx.RepoID) + if ctx.Written() { + return + } + + ctx.HTML(http.StatusOK, vCtx.VariablesTemplate) +} + +func VariableCreate(ctx *context.Context) { + vCtx, err := getVariablesCtx(ctx) + if err != nil { + ctx.ServerError("getVariablesCtx", err) + return + } + + if ctx.HasError() { // form binding validation error + ctx.JSONError(ctx.GetErrMsg()) + return + } + + shared.CreateVariable(ctx, vCtx.OwnerID, vCtx.RepoID, vCtx.RedirectLink) +} + +func VariableUpdate(ctx *context.Context) { + vCtx, err := getVariablesCtx(ctx) + if err != nil { + ctx.ServerError("getVariablesCtx", err) + return + } + + if ctx.HasError() { // form binding validation error + ctx.JSONError(ctx.GetErrMsg()) + return + } + + shared.UpdateVariable(ctx, vCtx.RedirectLink) +} + +func VariableDelete(ctx *context.Context) { + vCtx, err := getVariablesCtx(ctx) + if err != nil { + ctx.ServerError("getVariablesCtx", err) + return + } + shared.DeleteVariable(ctx, vCtx.RedirectLink) +} diff --git a/routers/web/repo/settings_test.go b/routers/web/repo/settings_test.go index 3bb202505c47d..a33e92c82113c 100644 --- a/routers/web/repo/settings_test.go +++ b/routers/web/repo/settings_test.go @@ -42,7 +42,7 @@ func TestAddReadOnlyDeployKey(t *testing.T) { } unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/settings/keys") + ctx, _ := test.MockContext(t, "user2/repo1/settings/keys") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 2) @@ -71,7 +71,7 @@ func TestAddReadWriteOnlyDeployKey(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/settings/keys") + ctx, _ := test.MockContext(t, "user2/repo1/settings/keys") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 2) @@ -94,7 +94,7 @@ func TestAddReadWriteOnlyDeployKey(t *testing.T) { func TestCollaborationPost(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/issues/labels") + ctx, _ := test.MockContext(t, "user2/repo1/issues/labels") test.LoadUser(t, ctx, 2) test.LoadUser(t, ctx, 4) test.LoadRepo(t, ctx, 1) @@ -129,7 +129,7 @@ func TestCollaborationPost(t *testing.T) { func TestCollaborationPost_InactiveUser(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/issues/labels") + ctx, _ := test.MockContext(t, "user2/repo1/issues/labels") test.LoadUser(t, ctx, 2) test.LoadUser(t, ctx, 9) test.LoadRepo(t, ctx, 1) @@ -152,7 +152,7 @@ func TestCollaborationPost_InactiveUser(t *testing.T) { func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/issues/labels") + ctx, _ := test.MockContext(t, "user2/repo1/issues/labels") test.LoadUser(t, ctx, 2) test.LoadUser(t, ctx, 4) test.LoadRepo(t, ctx, 1) @@ -193,7 +193,7 @@ func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) { func TestCollaborationPost_NonExistentUser(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/issues/labels") + ctx, _ := test.MockContext(t, "user2/repo1/issues/labels") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) @@ -215,7 +215,7 @@ func TestCollaborationPost_NonExistentUser(t *testing.T) { func TestAddTeamPost(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "org26/repo43") + ctx, _ := test.MockContext(t, "org26/repo43") ctx.Req.Form.Set("team", "team11") @@ -255,7 +255,7 @@ func TestAddTeamPost(t *testing.T) { func TestAddTeamPost_NotAllowed(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "org26/repo43") + ctx, _ := test.MockContext(t, "org26/repo43") ctx.Req.Form.Set("team", "team11") @@ -295,7 +295,7 @@ func TestAddTeamPost_NotAllowed(t *testing.T) { func TestAddTeamPost_AddTeamTwice(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "org26/repo43") + ctx, _ := test.MockContext(t, "org26/repo43") ctx.Req.Form.Set("team", "team11") @@ -336,7 +336,7 @@ func TestAddTeamPost_AddTeamTwice(t *testing.T) { func TestAddTeamPost_NonExistentTeam(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "org26/repo43") + ctx, _ := test.MockContext(t, "org26/repo43") ctx.Req.Form.Set("team", "team-non-existent") @@ -369,7 +369,7 @@ func TestAddTeamPost_NonExistentTeam(t *testing.T) { func TestDeleteTeam(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "org3/team1/repo3") + ctx, _ := test.MockContext(t, "org3/team1/repo3") ctx.Req.Form.Set("id", "2") diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go index e51820a520ee2..d85879d1e5cf8 100644 --- a/routers/web/repo/wiki_test.go +++ b/routers/web/repo/wiki_test.go @@ -78,7 +78,7 @@ func assertPagesMetas(t *testing.T, expectedNames []string, metas interface{}) { func TestWiki(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/wiki/?action=_pages") + ctx, _ := test.MockContext(t, "user2/repo1/wiki/?action=_pages") ctx.SetParams("*", "Home") test.LoadRepo(t, ctx, 1) Wiki(ctx) @@ -90,7 +90,7 @@ func TestWiki(t *testing.T) { func TestWikiPages(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/wiki/?action=_pages") + ctx, _ := test.MockContext(t, "user2/repo1/wiki/?action=_pages") test.LoadRepo(t, ctx, 1) WikiPages(ctx) assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) @@ -100,7 +100,7 @@ func TestWikiPages(t *testing.T) { func TestNewWiki(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/wiki/?action=_new") + ctx, _ := test.MockContext(t, "user2/repo1/wiki/?action=_new") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) NewWiki(ctx) @@ -115,7 +115,7 @@ func TestNewWikiPost(t *testing.T) { } { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/wiki/?action=_new") + ctx, _ := test.MockContext(t, "user2/repo1/wiki/?action=_new") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) web.SetForm(ctx, &forms.NewWikiForm{ @@ -133,7 +133,7 @@ func TestNewWikiPost(t *testing.T) { func TestNewWikiPost_ReservedName(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/wiki/?action=_new") + ctx, _ := test.MockContext(t, "user2/repo1/wiki/?action=_new") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) web.SetForm(ctx, &forms.NewWikiForm{ @@ -150,7 +150,7 @@ func TestNewWikiPost_ReservedName(t *testing.T) { func TestEditWiki(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/wiki/Home?action=_edit") + ctx, _ := test.MockContext(t, "user2/repo1/wiki/Home?action=_edit") ctx.SetParams("*", "Home") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) @@ -166,7 +166,7 @@ func TestEditWikiPost(t *testing.T) { "New/", } { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/wiki/Home?action=_new") + ctx, _ := test.MockContext(t, "user2/repo1/wiki/Home?action=_new") ctx.SetParams("*", "Home") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) @@ -188,7 +188,7 @@ func TestEditWikiPost(t *testing.T) { func TestDeleteWikiPagePost(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/wiki/Home?action=_delete") + ctx, _ := test.MockContext(t, "user2/repo1/wiki/Home?action=_delete") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) DeleteWikiPagePost(ctx) @@ -207,7 +207,7 @@ func TestWikiRaw(t *testing.T) { } { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/wiki/raw/"+url.PathEscape(filepath)) + ctx, _ := test.MockContext(t, "user2/repo1/wiki/raw/"+url.PathEscape(filepath)) ctx.SetParams("*", filepath) test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go new file mode 100644 index 0000000000000..8d1516c91ceb3 --- /dev/null +++ b/routers/web/shared/actions/variables.go @@ -0,0 +1,128 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "errors" + "regexp" + "strings" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/forms" +) + +func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) { + variables, err := actions_model.FindVariables(ctx, actions_model.FindVariablesOpts{ + OwnerID: ownerID, + RepoID: repoID, + }) + if err != nil { + ctx.ServerError("FindVariables", err) + return + } + ctx.Data["Variables"] = variables +} + +// some regular expression of `variables` and `secrets` +// reference to: +// https://docs.github.com/en/actions/learn-github-actions/variables#naming-conventions-for-configuration-variables +// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets +var ( + nameRx = regexp.MustCompile("(?i)^[A-Z_][A-Z0-9_]*$") + forbiddenPrefixRx = regexp.MustCompile("(?i)^GIT(EA|HUB)_") + + forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI") +) + +func NameRegexMatch(name string) error { + if !nameRx.MatchString(name) || forbiddenPrefixRx.MatchString(name) { + log.Error("Name %s, regex match error", name) + return errors.New("name has invalid character") + } + return nil +} + +func envNameCIRegexMatch(name string) error { + if forbiddenEnvNameCIRx.MatchString(name) { + log.Error("Env Name cannot be ci") + return errors.New("env name cannot be ci") + } + return nil +} + +func CreateVariable(ctx *context.Context, ownerID, repoID int64, redirectURL string) { + form := web.GetForm(ctx).(*forms.EditVariableForm) + + if err := NameRegexMatch(form.Name); err != nil { + ctx.JSONError(err.Error()) + return + } + + if err := envNameCIRegexMatch(form.Name); err != nil { + ctx.JSONError(err.Error()) + return + } + + v, err := actions_model.InsertVariable(ctx, ownerID, repoID, form.Name, ReserveLineBreakForTextarea(form.Data)) + if err != nil { + log.Error("InsertVariable error: %v", err) + ctx.JSONError(ctx.Tr("actions.variables.creation.failed")) + return + } + ctx.Flash.Success(ctx.Tr("actions.variables.creation.success", v.Name)) + ctx.JSONRedirect(redirectURL) +} + +func UpdateVariable(ctx *context.Context, redirectURL string) { + id := ctx.ParamsInt64(":variable_id") + form := web.GetForm(ctx).(*forms.EditVariableForm) + + if err := NameRegexMatch(form.Name); err != nil { + ctx.JSONError(err.Error()) + return + } + + if err := envNameCIRegexMatch(form.Name); err != nil { + ctx.JSONError(err.Error()) + return + } + + ok, err := actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{ + ID: id, + Name: strings.ToUpper(form.Name), + Data: ReserveLineBreakForTextarea(form.Data), + }) + if err != nil || !ok { + log.Error("UpdateVariable error: %v", err) + ctx.JSONError(ctx.Tr("actions.variables.update.failed")) + return + } + ctx.Flash.Success(ctx.Tr("actions.variables.update.success")) + ctx.JSONRedirect(redirectURL) +} + +func DeleteVariable(ctx *context.Context, redirectURL string) { + id := ctx.ParamsInt64(":variable_id") + + if _, err := db.DeleteByBean(ctx, &actions_model.ActionVariable{ID: id}); err != nil { + log.Error("Delete variable [%d] failed: %v", id, err) + ctx.JSONError(ctx.Tr("actions.variables.deletion.failed")) + return + } + ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success")) + ctx.JSONRedirect(redirectURL) +} + +func ReserveLineBreakForTextarea(input string) string { + // Since the content is from a form which is a textarea, the line endings are \r\n. + // It's a standard behavior of HTML. + // But we want to store them as \n like what GitHub does. + // And users are unlikely to really need to keep the \r. + // Other than this, we should respect the original content, even leading or trailing spaces. + return strings.ReplaceAll(input, "\r\n", "\n") +} diff --git a/routers/web/shared/secrets/secrets.go b/routers/web/shared/secrets/secrets.go index a0d648f908fd4..c09ce51499a40 100644 --- a/routers/web/shared/secrets/secrets.go +++ b/routers/web/shared/secrets/secrets.go @@ -4,14 +4,12 @@ package secrets import ( - "net/http" - "strings" - "code.gitea.io/gitea/models/db" secret_model "code.gitea.io/gitea/models/secret" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/web/shared/actions" "code.gitea.io/gitea/services/forms" ) @@ -28,23 +26,20 @@ func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) { func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) { form := web.GetForm(ctx).(*forms.AddSecretForm) - content := form.Content - // Since the content is from a form which is a textarea, the line endings are \r\n. - // It's a standard behavior of HTML. - // But we want to store them as \n like what GitHub does. - // And users are unlikely to really need to keep the \r. - // Other than this, we should respect the original content, even leading or trailing spaces. - content = strings.ReplaceAll(content, "\r\n", "\n") + if err := actions.NameRegexMatch(form.Name); err != nil { + ctx.JSONError(ctx.Tr("secrets.creation.failed")) + return + } - s, err := secret_model.InsertEncryptedSecret(ctx, ownerID, repoID, form.Title, content) + s, err := secret_model.InsertEncryptedSecret(ctx, ownerID, repoID, form.Name, actions.ReserveLineBreakForTextarea(form.Data)) if err != nil { log.Error("InsertEncryptedSecret: %v", err) - ctx.Flash.Error(ctx.Tr("secrets.creation.failed")) - } else { - ctx.Flash.Success(ctx.Tr("secrets.creation.success", s.Name)) + ctx.JSONError(ctx.Tr("secrets.creation.failed")) + return } - ctx.Redirect(redirectURL) + ctx.Flash.Success(ctx.Tr("secrets.creation.success", s.Name)) + ctx.JSONRedirect(redirectURL) } func PerformSecretsDelete(ctx *context.Context, ownerID, repoID int64, redirectURL string) { @@ -52,12 +47,9 @@ func PerformSecretsDelete(ctx *context.Context, ownerID, repoID int64, redirectU if _, err := db.DeleteByBean(ctx, &secret_model.Secret{ID: id, OwnerID: ownerID, RepoID: repoID}); err != nil { log.Error("Delete secret %d failed: %v", id, err) - ctx.Flash.Error(ctx.Tr("secrets.deletion.failed")) - } else { - ctx.Flash.Success(ctx.Tr("secrets.deletion.success")) + ctx.JSONError(ctx.Tr("secrets.deletion.failed")) + return } - - ctx.JSON(http.StatusOK, map[string]interface{}{ - "redirect": redirectURL, - }) + ctx.Flash.Success(ctx.Tr("secrets.deletion.success")) + ctx.JSONRedirect(redirectURL) } diff --git a/routers/web/user/home_test.go b/routers/web/user/home_test.go index 534b0b26207b6..3a06a38c2450d 100644 --- a/routers/web/user/home_test.go +++ b/routers/web/user/home_test.go @@ -20,7 +20,7 @@ func TestArchivedIssues(t *testing.T) { setting.UI.IssuePagingNum = 1 assert.NoError(t, unittest.LoadFixtures()) - ctx := test.MockContext(t, "issues") + ctx, _ := test.MockContext(t, "issues") test.LoadUser(t, ctx, 30) ctx.Req.Form.Set("state", "open") @@ -53,7 +53,7 @@ func TestIssues(t *testing.T) { setting.UI.IssuePagingNum = 1 assert.NoError(t, unittest.LoadFixtures()) - ctx := test.MockContext(t, "issues") + ctx, _ := test.MockContext(t, "issues") test.LoadUser(t, ctx, 2) ctx.Req.Form.Set("state", "closed") Issues(ctx) @@ -69,7 +69,7 @@ func TestPulls(t *testing.T) { setting.UI.IssuePagingNum = 20 assert.NoError(t, unittest.LoadFixtures()) - ctx := test.MockContext(t, "pulls") + ctx, _ := test.MockContext(t, "pulls") test.LoadUser(t, ctx, 2) ctx.Req.Form.Set("state", "open") Pulls(ctx) @@ -82,7 +82,7 @@ func TestMilestones(t *testing.T) { setting.UI.IssuePagingNum = 1 assert.NoError(t, unittest.LoadFixtures()) - ctx := test.MockContext(t, "milestones") + ctx, _ := test.MockContext(t, "milestones") test.LoadUser(t, ctx, 2) ctx.SetParams("sort", "issues") ctx.Req.Form.Set("state", "closed") @@ -101,7 +101,7 @@ func TestMilestonesForSpecificRepo(t *testing.T) { setting.UI.IssuePagingNum = 1 assert.NoError(t, unittest.LoadFixtures()) - ctx := test.MockContext(t, "milestones") + ctx, _ := test.MockContext(t, "milestones") test.LoadUser(t, ctx, 2) ctx.SetParams("sort", "issues") ctx.SetParams("repo", "1") diff --git a/routers/web/user/setting/account_test.go b/routers/web/user/setting/account_test.go index 569d597722ee3..ba840db28894a 100644 --- a/routers/web/user/setting/account_test.go +++ b/routers/web/user/setting/account_test.go @@ -83,7 +83,7 @@ func TestChangePassword(t *testing.T) { t.Run(req.OldPassword+"__"+req.NewPassword, func(t *testing.T) { unittest.PrepareTestEnv(t) setting.PasswordComplexity = req.PasswordComplexity - ctx := test.MockContext(t, "user/settings/security") + ctx, _ := test.MockContext(t, "user/settings/security") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) diff --git a/routers/web/web.go b/routers/web/web.go index 8683ef221dd97..26ad2d54c3a32 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -104,7 +104,7 @@ func ctxDataSet(args ...any) func(ctx *context.Context) { } // Routes returns all web routes -func Routes(ctx gocontext.Context) *web.Route { +func Routes() *web.Route { routes := web.NewRoute() routes.Head("/", misc.DummyOK) // for health check - doesn't need to be passed through gzip handler @@ -146,13 +146,8 @@ func Routes(ctx gocontext.Context) *web.Route { mid = append(mid, common.Sessioner(), context.Contexter()) - group := buildAuthGroup() - if err := group.Init(ctx); err != nil { - log.Error("Could not initialize '%s' auth method, error: %s", group.Name(), err) - } - // Get user from session if logged in. - mid = append(mid, auth_service.Auth(group)) + mid = append(mid, auth_service.Auth(buildAuthGroup())) // GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route mid = append(mid, middleware.GetHead) @@ -312,6 +307,15 @@ func registerRoutes(m *web.Route) { m.Post("/packagist/{id}", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost) } + addSettingVariablesRoutes := func() { + m.Group("/variables", func() { + m.Get("", repo_setting.Variables) + m.Post("/new", web.Bind(forms.EditVariableForm{}), repo_setting.VariableCreate) + m.Post("/{variable_id}/edit", web.Bind(forms.EditVariableForm{}), repo_setting.VariableUpdate) + m.Post("/{variable_id}/delete", repo_setting.VariableDelete) + }) + } + addSettingsSecretsRoutes := func() { m.Group("/secrets", func() { m.Get("", repo_setting.Secrets) @@ -499,6 +503,7 @@ func registerRoutes(m *web.Route) { m.Get("", user_setting.RedirectToDefaultSetting) addSettingsRunnersRoutes() addSettingsSecretsRoutes() + addSettingVariablesRoutes() }, actions.MustEnableActions) m.Get("/organization", user_setting.Organization) @@ -765,6 +770,7 @@ func registerRoutes(m *web.Route) { m.Get("", org_setting.RedirectToDefaultSetting) addSettingsRunnersRoutes() addSettingsSecretsRoutes() + addSettingVariablesRoutes() }, actions.MustEnableActions) m.RouteMethods("/delete", "GET,POST", org.SettingsDelete) @@ -946,6 +952,7 @@ func registerRoutes(m *web.Route) { m.Get("", repo_setting.RedirectToDefaultSetting) addSettingsRunnersRoutes() addSettingsSecretsRoutes() + addSettingVariablesRoutes() }, actions.MustEnableActions) m.Post("/migrate/cancel", repo.MigrateCancelPost) // this handler must be under "settings", otherwise this incomplete repo can't be accessed }, ctxDataSet("PageIsRepoSettings", true, "LFSStartServer", setting.LFS.StartServer)) @@ -1029,7 +1036,8 @@ func registerRoutes(m *web.Route) { m.Post("/request_review", reqRepoIssuesOrPullsReader, repo.UpdatePullReviewRequest) m.Post("/dismiss_review", reqRepoAdmin, web.Bind(forms.DismissReviewForm{}), repo.DismissReview) m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus) - m.Post("/resolve_conversation", reqRepoIssuesOrPullsReader, repo.UpdateResolveConversation) + m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues) + m.Post("/resolve_conversation", reqRepoIssuesOrPullsReader, repo.SetShowOutdatedComments, repo.UpdateResolveConversation) m.Post("/attachments", repo.UploadIssueAttachment) m.Post("/attachments/remove", repo.DeleteAttachment) m.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin) @@ -1277,10 +1285,10 @@ func registerRoutes(m *web.Route) { m.Post("/set_allow_maintainer_edit", web.Bind(forms.UpdateAllowEditsForm{}), repo.SetAllowEdits) m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest) m.Group("/files", func() { - m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.ViewPullFiles) + m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFiles) m.Group("/reviews", func() { m.Get("/new_comment", repo.RenderNewCodeCommentForm) - m.Post("/comments", web.Bind(forms.CodeCommentForm{}), repo.CreateCodeComment) + m.Post("/comments", web.Bind(forms.CodeCommentForm{}), repo.SetShowOutdatedComments, repo.CreateCodeComment) m.Post("/submit", web.Bind(forms.SubmitReviewForm{}), repo.SubmitReview) }, context.RepoMustNotBeArchived()) }) diff --git a/services/auth/group.go b/services/auth/group.go index 0a0330b3aa95e..a1ff65f20385e 100644 --- a/services/auth/group.go +++ b/services/auth/group.go @@ -4,7 +4,6 @@ package auth import ( - "context" "net/http" "reflect" "strings" @@ -14,9 +13,7 @@ import ( // Ensure the struct implements the interface. var ( - _ Method = &Group{} - _ Initializable = &Group{} - _ Freeable = &Group{} + _ Method = &Group{} ) // Group implements the Auth interface with serval Auth. @@ -49,35 +46,6 @@ func (b *Group) Name() string { return strings.Join(names, ",") } -// Init does nothing as the Basic implementation does not need to allocate any resources -func (b *Group) Init(ctx context.Context) error { - for _, method := range b.methods { - initializable, ok := method.(Initializable) - if !ok { - continue - } - - if err := initializable.Init(ctx); err != nil { - return err - } - } - return nil -} - -// Free does nothing as the Basic implementation does not have to release any resources -func (b *Group) Free() error { - for _, method := range b.methods { - freeable, ok := method.(Freeable) - if !ok { - continue - } - if err := freeable.Free(); err != nil { - return err - } - } - return nil -} - // Verify extracts and validates func (b *Group) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { // Try to sign in with each of the enabled plugins diff --git a/services/auth/interface.go b/services/auth/interface.go index c4a8a20d0103b..508291fa4311b 100644 --- a/services/auth/interface.go +++ b/services/auth/interface.go @@ -29,26 +29,11 @@ type Method interface { Verify(http *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) } -// Initializable represents a structure that requires initialization -// It usually should only be called once before anything else is called -type Initializable interface { - // Init should be called exactly once before using any of the other methods, - // in order to allow the plugin to allocate necessary resources - Init(ctx context.Context) error -} - // Named represents a named thing type Named interface { Name() string } -// Freeable represents a structure that is required to be freed -type Freeable interface { - // Free should be called exactly once before application closes, in order to - // give chance to the plugin to free any allocated resources - Free() error -} - // PasswordAuthenticator represents a source of authentication type PasswordAuthenticator interface { Authenticate(user *user_model.User, login, password string) (*user_model.User, error) diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go index 4571ff6540c3e..3e0f47a37e296 100644 --- a/services/auth/source/ldap/source_sync.go +++ b/services/auth/source/ldap/source_sync.go @@ -6,7 +6,6 @@ package ldap import ( "context" "fmt" - "sort" "strings" asymkey_model "code.gitea.io/gitea/models/asymkey" @@ -24,7 +23,6 @@ import ( func (source *Source) Sync(ctx context.Context, updateExisting bool) error { log.Trace("Doing: SyncExternalUsers[%s]", source.authSource.Name) - var existingUsers []int isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0 var sshKeysNeedUpdate bool @@ -41,9 +39,14 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { default: } - sort.Slice(users, func(i, j int) bool { - return users[i].LowerName < users[j].LowerName - }) + usernameUsers := make(map[string]*user_model.User, len(users)) + mailUsers := make(map[string]*user_model.User, len(users)) + keepActiveUsers := make(map[int64]struct{}) + + for _, u := range users { + usernameUsers[u.LowerName] = u + mailUsers[strings.ToLower(u.Email)] = u + } sr, err := source.SearchEntries() if err != nil { @@ -59,11 +62,6 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { log.Warn("LDAP search found no entries but did not report an error. All users will be deactivated as per settings") } - sort.Slice(sr, func(i, j int) bool { - return sr[i].LowerName < sr[j].LowerName - }) - - userPos := 0 orgCache := make(map[string]*organization.Organization) teamCache := make(map[string]*organization.Team) @@ -86,21 +84,27 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { return db.ErrCancelledf("During update of %s before completed update of users", source.authSource.Name) default: } - if len(su.Username) == 0 { + if len(su.Username) == 0 && len(su.Mail) == 0 { continue } - if len(su.Mail) == 0 { - su.Mail = fmt.Sprintf("%s@localhost", su.Username) + var usr *user_model.User + if len(su.Username) > 0 { + usr = usernameUsers[su.LowerName] + } + if usr == nil && len(su.Mail) > 0 { + usr = mailUsers[strings.ToLower(su.Mail)] } - var usr *user_model.User - for userPos < len(users) && users[userPos].LowerName < su.LowerName { - userPos++ + if usr != nil { + keepActiveUsers[usr.ID] = struct{}{} + } else if len(su.Username) == 0 { + // we cannot create the user if su.Username is empty + continue } - if userPos < len(users) && users[userPos].LowerName == su.LowerName { - usr = users[userPos] - existingUsers = append(existingUsers, userPos) + + if len(su.Mail) == 0 { + su.Mail = fmt.Sprintf("%s@localhost", su.Username) } fullName := composeFullName(su.Name, su.Surname, su.Username) @@ -203,19 +207,17 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { // Deactivate users not present in LDAP if updateExisting { - existPos := 0 - for i, usr := range users { - for existPos < len(existingUsers) && i > existingUsers[existPos] { - existPos++ + for _, usr := range users { + if _, ok := keepActiveUsers[usr.ID]; ok { + continue } - if usr.IsActive && (existPos >= len(existingUsers) || i < existingUsers[existPos]) { - log.Trace("SyncExternalUsers[%s]: Deactivating user %s", source.authSource.Name, usr.Name) - usr.IsActive = false - err = user_model.UpdateUserCols(ctx, usr, "is_active") - if err != nil { - log.Error("SyncExternalUsers[%s]: Error deactivating user %s: %v", source.authSource.Name, usr.Name, err) - } + log.Trace("SyncExternalUsers[%s]: Deactivating user %s", source.authSource.Name, usr.Name) + + usr.IsActive = false + err = user_model.UpdateUserCols(ctx, usr, "is_active") + if err != nil { + log.Error("SyncExternalUsers[%s]: Error deactivating user %s: %v", source.authSource.Name, usr.Name, err) } } } diff --git a/services/auth/sspi_windows.go b/services/auth/sspi_windows.go index d49497e19c958..c162810797c84 100644 --- a/services/auth/sspi_windows.go +++ b/services/auth/sspi_windows.go @@ -4,10 +4,10 @@ package auth import ( - "context" "errors" "net/http" "strings" + "sync" "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/avatars" @@ -32,13 +32,12 @@ var ( // sspiAuth is a global instance of the websspi authentication package, // which is used to avoid acquiring the server credential handle on // every request - sspiAuth *websspi.Authenticator + sspiAuth *websspi.Authenticator + sspiAuthOnce sync.Once // Ensure the struct implements the interface. - _ Method = &SSPI{} - _ Named = &SSPI{} - _ Initializable = &SSPI{} - _ Freeable = &SSPI{} + _ Method = &SSPI{} + _ Named = &SSPI{} ) // SSPI implements the SingleSignOn interface and authenticates requests @@ -47,32 +46,25 @@ var ( // Returns nil if authentication fails. type SSPI struct{} -// Init creates a new global websspi.Authenticator object -func (s *SSPI) Init(ctx context.Context) error { - config := websspi.NewConfig() - var err error - sspiAuth, err = websspi.New(config) - if err != nil { - return err - } - return nil -} - // Name represents the name of auth method func (s *SSPI) Name() string { return "sspi" } -// Free releases resources used by the global websspi.Authenticator object -func (s *SSPI) Free() error { - return sspiAuth.Free() -} - // Verify uses SSPI (Windows implementation of SPNEGO) to authenticate the request. // If authentication is successful, returns the corresponding user object. // If negotiation should continue or authentication fails, immediately returns a 401 HTTP // response code, as required by the SPNEGO protocol. func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { + var errInit error + sspiAuthOnce.Do(func() { + config := websspi.NewConfig() + sspiAuth, errInit = websspi.New(config) + }) + if errInit != nil { + return nil, errInit + } + if !s.shouldAuthenticate(req) { return nil, nil } diff --git a/services/cron/tasks_extended.go b/services/cron/tasks_extended.go index 3e0dbd132eb31..acf2d3373c8ea 100644 --- a/services/cron/tasks_extended.go +++ b/services/cron/tasks_extended.go @@ -150,7 +150,7 @@ func registerUpdateGiteaChecker() { RunAtStart: false, Schedule: "@every 168h", }, - HTTPEndpoint: "https://dl.gitea.io/gitea/version.json", + HTTPEndpoint: "https://dl.gitea.com/gitea/version.json", }, func(ctx context.Context, _ *user_model.User, config Config) error { updateCheckerConfig := config.(*UpdateCheckerConfig) return updatechecker.GiteaUpdateChecker(updateCheckerConfig.HTTPEndpoint) diff --git a/services/forms/user_form.go b/services/forms/user_form.go index 1315fb237b3bb..1f5abf94ee129 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -27,7 +27,6 @@ type InstallForm struct { DbPasswd string DbName string SSLMode string - Charset string `binding:"Required;In(utf8,utf8mb4)"` DbPath string DbSchema string @@ -367,8 +366,8 @@ func (f *AddKeyForm) Validate(req *http.Request, errs binding.Errors) binding.Er // AddSecretForm for adding secrets type AddSecretForm struct { - Title string `binding:"Required;MaxSize(50)"` - Content string `binding:"Required"` + Name string `binding:"Required;MaxSize(255)"` + Data string `binding:"Required;MaxSize(65535)"` } // Validate validates the fields @@ -377,6 +376,16 @@ func (f *AddSecretForm) Validate(req *http.Request, errs binding.Errors) binding return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } +type EditVariableForm struct { + Name string `binding:"Required;MaxSize(255)"` + Data string `binding:"Required;MaxSize(65535)"` +} + +func (f *EditVariableForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { + ctx := context.GetValidateContext(req) + return middleware.Validate(errs, ctx.Data, f, ctx.Locale) +} + // NewAccessTokenForm form for creating access token type NewAccessTokenForm struct { Name string `binding:"Required;MaxSize(255)"` diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index b6a75f60982f2..9adf3b940093e 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -450,8 +450,8 @@ type Diff struct { } // LoadComments loads comments into each line -func (diff *Diff) LoadComments(ctx context.Context, issue *issues_model.Issue, currentUser *user_model.User) error { - allComments, err := issues_model.FetchCodeComments(ctx, issue, currentUser) +func (diff *Diff) LoadComments(ctx context.Context, issue *issues_model.Issue, currentUser *user_model.User, showOutdatedComments bool) error { + allComments, err := issues_model.FetchCodeComments(ctx, issue, currentUser, showOutdatedComments) if err != nil { return err } diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go index 389f787dfc442..e270e46fd453f 100644 --- a/services/gitdiff/gitdiff_test.go +++ b/services/gitdiff/gitdiff_test.go @@ -594,16 +594,26 @@ func setupDefaultDiff() *Diff { } } -func TestDiff_LoadComments(t *testing.T) { +func TestDiff_LoadCommentsNoOutdated(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) diff := setupDefaultDiff() - assert.NoError(t, diff.LoadComments(db.DefaultContext, issue, user)) + assert.NoError(t, diff.LoadComments(db.DefaultContext, issue, user, false)) assert.Len(t, diff.Files[0].Sections[0].Lines[0].Comments, 2) } +func TestDiff_LoadCommentsWithOutdated(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + diff := setupDefaultDiff() + assert.NoError(t, diff.LoadComments(db.DefaultContext, issue, user, true)) + assert.Len(t, diff.Files[0].Sections[0].Lines[0].Comments, 3) +} + func TestDiffLine_CanComment(t *testing.T) { assert.False(t, (&DiffLine{Type: DiffLineSection}).CanComment()) assert.False(t, (&DiffLine{Type: DiffLineAdd, Comments: []*issues_model.Comment{{Content: "bla"}}}).CanComment()) diff --git a/services/lfs/server.go b/services/lfs/server.go index a18e752d4791b..b32f218785edc 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -77,6 +77,8 @@ func CheckAcceptMediaType(ctx *context.Context) { } } +var rangeHeaderRegexp = regexp.MustCompile(`bytes=(\d+)\-(\d*).*`) + // DownloadHandler gets the content from the content store func DownloadHandler(ctx *context.Context) { rc := getRequestContext(ctx) @@ -92,8 +94,7 @@ func DownloadHandler(ctx *context.Context) { toByte = meta.Size - 1 statusCode := http.StatusOK if rangeHdr := ctx.Req.Header.Get("Range"); rangeHdr != "" { - regex := regexp.MustCompile(`bytes=(\d+)\-(\d*).*`) - match := regex.FindStringSubmatch(rangeHdr) + match := rangeHeaderRegexp.FindStringSubmatch(rangeHdr) if len(match) > 1 { statusCode = http.StatusPartialContent fromByte, _ = strconv.ParseInt(match[1], 10, 32) diff --git a/services/repository/archiver/archiver_test.go b/services/repository/archiver/archiver_test.go index 3cd6e813512bc..4b6fb7446da22 100644 --- a/services/repository/archiver/archiver_test.go +++ b/services/repository/archiver/archiver_test.go @@ -24,7 +24,7 @@ func TestMain(m *testing.M) { func TestArchive_Basic(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - ctx := test.MockContext(t, "user27/repo49") + ctx, _ := test.MockContext(t, "user27/repo49") firstCommit, secondCommit := "51f84af23134", "aacbdfe9e1c4" test.LoadRepo(t, ctx, 49) diff --git a/services/repository/files/content_test.go b/services/repository/files/content_test.go index a43b71cf31e1a..8ff96822c9a90 100644 --- a/services/repository/files/content_test.go +++ b/services/repository/files/content_test.go @@ -54,7 +54,7 @@ func getExpectedReadmeContentsResponse() *api.ContentsResponse { func TestGetContents(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1") + ctx, _ := test.MockContext(t, "user2/repo1") ctx.SetParams(":id", "1") test.LoadRepo(t, ctx, 1) test.LoadRepoCommit(t, ctx) @@ -82,7 +82,7 @@ func TestGetContents(t *testing.T) { func TestGetContentsOrListForDir(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1") + ctx, _ := test.MockContext(t, "user2/repo1") ctx.SetParams(":id", "1") test.LoadRepo(t, ctx, 1) test.LoadRepoCommit(t, ctx) @@ -117,7 +117,7 @@ func TestGetContentsOrListForDir(t *testing.T) { func TestGetContentsOrListForFile(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1") + ctx, _ := test.MockContext(t, "user2/repo1") ctx.SetParams(":id", "1") test.LoadRepo(t, ctx, 1) test.LoadRepoCommit(t, ctx) @@ -145,7 +145,7 @@ func TestGetContentsOrListForFile(t *testing.T) { func TestGetContentsErrors(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1") + ctx, _ := test.MockContext(t, "user2/repo1") ctx.SetParams(":id", "1") test.LoadRepo(t, ctx, 1) test.LoadRepoCommit(t, ctx) @@ -176,7 +176,7 @@ func TestGetContentsErrors(t *testing.T) { func TestGetContentsOrListErrors(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1") + ctx, _ := test.MockContext(t, "user2/repo1") ctx.SetParams(":id", "1") test.LoadRepo(t, ctx, 1) test.LoadRepoCommit(t, ctx) @@ -207,7 +207,7 @@ func TestGetContentsOrListErrors(t *testing.T) { func TestGetContentsOrListOfEmptyRepos(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user30/empty") + ctx, _ := test.MockContext(t, "user30/empty") ctx.SetParams(":id", "52") test.LoadRepo(t, ctx, 52) test.LoadUser(t, ctx, 30) @@ -225,7 +225,7 @@ func TestGetContentsOrListOfEmptyRepos(t *testing.T) { func TestGetBlobBySHA(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1") + ctx, _ := test.MockContext(t, "user2/repo1") test.LoadRepo(t, ctx, 1) test.LoadRepoCommit(t, ctx) test.LoadUser(t, ctx, 2) diff --git a/services/repository/files/diff_test.go b/services/repository/files/diff_test.go index 621816e97d424..0346e0e9e9b75 100644 --- a/services/repository/files/diff_test.go +++ b/services/repository/files/diff_test.go @@ -17,7 +17,7 @@ import ( func TestGetDiffPreview(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1") + ctx, _ := test.MockContext(t, "user2/repo1") ctx.SetParams(":id", "1") test.LoadRepo(t, ctx, 1) test.LoadRepoCommit(t, ctx) @@ -139,7 +139,7 @@ func TestGetDiffPreview(t *testing.T) { func TestGetDiffPreviewErrors(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1") + ctx, _ := test.MockContext(t, "user2/repo1") ctx.SetParams(":id", "1") test.LoadRepo(t, ctx, 1) test.LoadRepoCommit(t, ctx) diff --git a/services/repository/files/file_test.go b/services/repository/files/file_test.go index e1c7d5d7fb409..d14a049438eb5 100644 --- a/services/repository/files/file_test.go +++ b/services/repository/files/file_test.go @@ -98,7 +98,7 @@ func getExpectedFileResponse() *api.FileResponse { func TestGetFileResponseFromCommit(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1") + ctx, _ := test.MockContext(t, "user2/repo1") ctx.SetParams(":id", "1") test.LoadRepo(t, ctx, 1) test.LoadRepoCommit(t, ctx) diff --git a/services/repository/files/tree_test.go b/services/repository/files/tree_test.go index a500dbdb22417..51a2190e8f44d 100644 --- a/services/repository/files/tree_test.go +++ b/services/repository/files/tree_test.go @@ -15,7 +15,7 @@ import ( func TestGetTreeBySHA(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1") + ctx, _ := test.MockContext(t, "user2/repo1") test.LoadRepo(t, ctx, 1) test.LoadRepoCommit(t, ctx) test.LoadUser(t, ctx, 2) diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 2850cc8d370dc..2ddc0c1ac6738 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -46,15 +46,6 @@
{{.ScriptType}}
{{.locale.Tr "admin.config.reverse_auth_user"}}
{{.ReverseProxyAuthUser}}
- - {{if .EnvVars}} -
- {{range .EnvVars}} -
{{.Name}}
-
{{.Value}}
- {{end}} - {{end}} - diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index e46e276ed02e3..3086e674fd621 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -3,62 +3,63 @@ {{$notificationUnreadCount = call .NotificationUnreadCount}} {{end}} -