diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 9e290fb6a51..a4a6ce8fcf6 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,6 +1,6 @@
{
"name": "Gitea DevContainer",
- "image": "mcr.microsoft.com/devcontainers/go:1.21-bullseye",
+ "image": "mcr.microsoft.com/devcontainers/go:1.22-bullseye",
"features": {
// installs nodejs into container
"ghcr.io/devcontainers/features/node:1": {
@@ -8,7 +8,9 @@
},
"ghcr.io/devcontainers/features/git-lfs:1.1.0": {},
"ghcr.io/devcontainers-contrib/features/poetry:2": {},
- "ghcr.io/devcontainers/features/python:1": {}
+ "ghcr.io/devcontainers/features/python:1": {
+ "version": "3.12"
+ }
},
"customizations": {
"vscode": {
diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index 4159f021889..b62b13cefea 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -12,6 +12,7 @@ plugins:
- "@eslint-community/eslint-plugin-eslint-comments"
- "@stylistic/eslint-plugin-js"
- eslint-plugin-array-func
+ - eslint-plugin-github
- eslint-plugin-i
- eslint-plugin-jquery
- eslint-plugin-no-jquery
@@ -128,7 +129,7 @@ rules:
"@stylistic/js/computed-property-spacing": [2, never]
"@stylistic/js/dot-location": [2, property]
"@stylistic/js/eol-last": [2]
- "@stylistic/js/func-call-spacing": [2, never]
+ "@stylistic/js/function-call-spacing": [2, never]
"@stylistic/js/function-call-argument-newline": [0]
"@stylistic/js/function-paren-newline": [0]
"@stylistic/js/generator-star-spacing": [0]
@@ -209,6 +210,29 @@ rules:
func-names: [0]
func-style: [0]
getter-return: [2]
+ github/a11y-aria-label-is-well-formatted: [0]
+ github/a11y-no-title-attribute: [0]
+ github/a11y-no-visually-hidden-interactive-element: [0]
+ github/a11y-role-supports-aria-props: [0]
+ github/a11y-svg-has-accessible-name: [0]
+ github/array-foreach: [0]
+ github/async-currenttarget: [2]
+ github/async-preventdefault: [2]
+ github/authenticity-token: [0]
+ github/get-attribute: [0]
+ github/js-class-name: [0]
+ github/no-blur: [0]
+ github/no-d-none: [0]
+ github/no-dataset: [2]
+ github/no-dynamic-script-tag: [2]
+ github/no-implicit-buggy-globals: [2]
+ github/no-inner-html: [0]
+ github/no-innerText: [2]
+ github/no-then: [2]
+ github/no-useless-passive: [2]
+ github/prefer-observers: [2]
+ github/require-passive-events: [2]
+ github/unescaped-html-literal: [0]
grouped-accessor-pairs: [2]
guard-for-in: [0]
id-blacklist: [0]
@@ -259,7 +283,7 @@ rules:
i/unambiguous: [0]
init-declarations: [0]
jquery/no-ajax-events: [2]
- jquery/no-ajax: [0]
+ jquery/no-ajax: [2]
jquery/no-animate: [2]
jquery/no-attr: [0]
jquery/no-bind: [2]
@@ -272,7 +296,7 @@ rules:
jquery/no-delegate: [2]
jquery/no-each: [0]
jquery/no-extend: [2]
- jquery/no-fade: [0]
+ jquery/no-fade: [2]
jquery/no-filter: [0]
jquery/no-find: [0]
jquery/no-global-eval: [2]
@@ -285,13 +309,13 @@ rules:
jquery/no-is-function: [2]
jquery/no-is: [0]
jquery/no-load: [2]
- jquery/no-map: [0]
+ jquery/no-map: [2]
jquery/no-merge: [2]
jquery/no-param: [2]
jquery/no-parent: [0]
jquery/no-parents: [0]
jquery/no-parse-html: [2]
- jquery/no-prop: [0]
+ jquery/no-prop: [2]
jquery/no-proxy: [2]
jquery/no-ready: [2]
jquery/no-serialize: [2]
@@ -372,11 +396,11 @@ rules:
no-irregular-whitespace: [2]
no-iterator: [2]
no-jquery/no-ajax-events: [2]
- no-jquery/no-ajax: [0]
+ no-jquery/no-ajax: [2]
no-jquery/no-and-self: [2]
no-jquery/no-animate-toggle: [2]
no-jquery/no-animate: [2]
- no-jquery/no-append-html: [0]
+ no-jquery/no-append-html: [2]
no-jquery/no-attr: [0]
no-jquery/no-bind: [2]
no-jquery/no-box-model: [2]
@@ -427,7 +451,7 @@ rules:
no-jquery/no-load: [2]
no-jquery/no-map-collection: [0]
no-jquery/no-map-util: [2]
- no-jquery/no-map: [0]
+ no-jquery/no-map: [2]
no-jquery/no-merge: [2]
no-jquery/no-node-name: [2]
no-jquery/no-noop: [2]
@@ -442,7 +466,7 @@ rules:
no-jquery/no-parse-html: [2]
no-jquery/no-parse-json: [2]
no-jquery/no-parse-xml: [2]
- no-jquery/no-prop: [0]
+ no-jquery/no-prop: [2]
no-jquery/no-proxy: [2]
no-jquery/no-ready-shorthand: [2]
no-jquery/no-ready: [2]
@@ -463,7 +487,7 @@ rules:
no-jquery/no-visibility: [2]
no-jquery/no-when: [2]
no-jquery/no-wrap: [2]
- no-jquery/variable-pattern: [0]
+ no-jquery/variable-pattern: [2]
no-label-var: [2]
no-labels: [0] # handled by no-restricted-syntax
no-lone-blocks: [2]
@@ -558,7 +582,6 @@ rules:
prefer-rest-params: [2]
prefer-spread: [2]
prefer-template: [2]
- quotes: [2, single, {avoidEscape: true, allowTemplateLiterals: true}]
radix: [2, as-needed]
regexp/confusing-quantifier: [2]
regexp/control-character-escape: [2]
@@ -723,6 +746,7 @@ rules:
unicorn/no-this-assignment: [2]
unicorn/no-typeof-undefined: [2]
unicorn/no-unnecessary-await: [2]
+ unicorn/no-unnecessary-polyfills: [2]
unicorn/no-unreadable-array-destructuring: [0]
unicorn/no-unreadable-iife: [2]
unicorn/no-unused-properties: [2]
@@ -810,7 +834,7 @@ rules:
wc/no-constructor-params: [2]
wc/no-constructor: [2]
wc/no-customized-built-in-elements: [2]
- wc/no-exports-with-element: [2]
+ wc/no-exports-with-element: [0]
wc/no-invalid-element-name: [2]
wc/no-invalid-extends: [2]
wc/no-method-prefixed-with-on: [2]
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 624a2d97db1..1447a6ea322 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,2 +1 @@
open_collective: gitea
-custom: https://www.bountysource.com/teams/gitea
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 8a5ab26975e..980b9c337cc 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -1,36 +1,84 @@
modifies/docs:
- - "**/*.md"
- - "docs/**"
+ - changed-files:
+ - any-glob-to-any-file:
+ - "**/*.md"
+ - "docs/**"
modifies/frontend:
- - "web_src/**/*"
+ - changed-files:
+ - any-glob-to-any-file:
+ - "web_src/**"
+ - "tailwind.config.js"
+ - "webpack.config.js"
modifies/templates:
- - all: ["templates/**", "!templates/swagger/v1_json.tmpl"]
+ - changed-files:
+ - all-globs-to-any-file:
+ - "templates/**"
+ - "!templates/swagger/v1_json.tmpl"
modifies/api:
- - "routers/api/**"
- - "templates/swagger/v1_json.tmpl"
+ - changed-files:
+ - any-glob-to-any-file:
+ - "routers/api/**"
+ - "templates/swagger/v1_json.tmpl"
modifies/cli:
- - "cmd/**"
+ - changed-files:
+ - any-glob-to-any-file:
+ - "cmd/**"
modifies/translation:
- - "options/locale/*.ini"
+ - changed-files:
+ - any-glob-to-any-file:
+ - "options/locale/*.ini"
modifies/migrations:
- - "models/migrations/**/*"
+ - changed-files:
+ - any-glob-to-any-file:
+ - "models/migrations/**"
modifies/internal:
- - "Makefile"
- - "Dockerfile"
- - "Dockerfile.rootless"
- - "docker/**"
- - "webpack.config.js"
- - ".eslintrc.yaml"
- - ".golangci.yml"
- - ".markdownlint.yaml"
- - ".spectral.yaml"
- - ".stylelintrc.yaml"
- - ".yamllint.yaml"
- - ".github/**"
+ - changed-files:
+ - any-glob-to-any-file:
+ - ".air.toml"
+ - "Makefile"
+ - "Dockerfile"
+ - "Dockerfile.rootless"
+ - ".dockerignore"
+ - "docker/**"
+ - ".editorconfig"
+ - ".eslintrc.yaml"
+ - ".golangci.yml"
+ - ".gitpod.yml"
+ - ".markdownlint.yaml"
+ - ".spectral.yaml"
+ - ".stylelintrc.yaml"
+ - ".yamllint.yaml"
+ - ".github/**"
+ - ".gitea/"
+ - ".devcontainer/**"
+ - "build.go"
+ - "build/**"
+ - "contrib/**"
+
+modifies/dependencies:
+ - changed-files:
+ - any-glob-to-any-file:
+ - "package.json"
+ - "package-lock.json"
+ - "pyproject.toml"
+ - "poetry.lock"
+ - "go.mod"
+ - "go.sum"
+
+modifies/go:
+ - changed-files:
+ - any-glob-to-any-file:
+ - "**/*.go"
+
+modifies/js:
+ - changed-files:
+ - any-glob-to-any-file:
+ - "**/*.js"
+ - "**/*.vue"
diff --git a/.github/stale.yml b/.github/stale.yml
deleted file mode 100644
index ebe95acf533..00000000000
--- a/.github/stale.yml
+++ /dev/null
@@ -1,54 +0,0 @@
-# Configuration for probot-stale - https://github.com/probot/stale
-
-# Number of days of inactivity before an Issue or Pull Request becomes stale
-daysUntilStale: 60
-
-# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
-# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
-daysUntilClose: 14
-
-# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
-exemptLabels:
- - status/blocked
- - kind/security
- - lgtm/done
- - reviewed/confirmed
- - priority/critical
- - kind/proposal
-
-# Set to true to ignore issues in a project (defaults to false)
-exemptProjects: false
-
-# Set to true to ignore issues in a milestone (defaults to false)
-exemptMilestones: false
-
-# Label to use when marking as stale
-staleLabel: stale
-
-# Comment to post when marking as stale. Set to `false` to disable
-markComment: >
- This issue has been automatically marked as stale because it has not had recent activity.
- I am here to help clear issues left open even if solved or waiting for more insight.
- This issue will be closed if no further activity occurs during the next 2 weeks.
- If the issue is still valid just add a comment to keep it alive.
- Thank you for your contributions.
-
-# Comment to post when closing a stale Issue or Pull Request.
-closeComment: >
- This issue has been automatically closed because of inactivity.
- You can re-open it if needed.
-
-# Limit the number of actions per hour, from 1-30. Default is 30
-limitPerRun: 1
-
-# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
-pulls:
- daysUntilStale: 60
- daysUntilClose: 60
- markComment: >
- This pull request has been automatically marked as stale because it has not had
- recent activity. It will be closed if no further activity occurs during the next 2 months. Thank you
- for your contributions.
- closeComment: >
- This pull request has been automatically closed because of inactivity.
- You can re-open it if needed.
diff --git a/.github/workflows/cron-licenses.yml b/.github/workflows/cron-licenses.yml
index 6ef5813a64f..cd8386ecc52 100644
--- a/.github/workflows/cron-licenses.yml
+++ b/.github/workflows/cron-licenses.yml
@@ -11,7 +11,7 @@ jobs:
if: github.repository == 'go-gitea/gitea'
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-go@v4
+ - uses: actions/setup-go@v5
with:
go-version-file: go.mod
check-latest: true
diff --git a/.github/workflows/cron-lock.yml b/.github/workflows/cron-lock.yml
index 935f926cce9..665313135b6 100644
--- a/.github/workflows/cron-lock.yml
+++ b/.github/workflows/cron-lock.yml
@@ -17,6 +17,7 @@ jobs:
runs-on: ubuntu-latest
if: github.repository == 'go-gitea/gitea'
steps:
- - uses: dessant/lock-threads@v4
+ - uses: dessant/lock-threads@v5
with:
- issue-inactive-days: 45
+ issue-inactive-days: 10
+ pr-inactive-days: 7
diff --git a/.github/workflows/disk-clean.yml b/.github/workflows/disk-clean.yml
index 78cd251354a..8abe8891c79 100644
--- a/.github/workflows/disk-clean.yml
+++ b/.github/workflows/disk-clean.yml
@@ -8,21 +8,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- # FIXME: https://github.com/jlumbroso/free-disk-space/issues/17
- - name: same as 'large-packages' but without 'google-cloud-sdk'
- shell: bash
- run: |
- sudo apt-get update
- sudo apt-get remove -y '^dotnet-.*' || true
- sudo apt-get remove -y '^llvm-.*' || true
- sudo apt-get remove -y 'php.*' || true
- sudo apt-get remove -y '^mongodb-.*' || true
- sudo apt-get remove -y '^mysql-.*' || true
- sudo apt-get remove -y azure-cli google-chrome-stable firefox powershell mono-devel libgl1-mesa-dri || true
- sudo apt-get autoremove -y
- sudo apt-get clean
- env:
- DEBIAN_FRONTEND: noninteractive
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml
index e7039053af9..f9b6b1ec495 100644
--- a/.github/workflows/files-changed.yml
+++ b/.github/workflows/files-changed.yml
@@ -35,7 +35,7 @@ jobs:
yaml: ${{ steps.changes.outputs.yaml }}
steps:
- uses: actions/checkout@v4
- - uses: dorny/paths-filter@v2
+ - uses: dorny/paths-filter@v3
id: changes
with:
filters: |
@@ -48,6 +48,7 @@ jobs:
- "Makefile"
- ".golangci.yml"
- ".editorconfig"
+ - "options/locale/locale_en-US.ini"
frontend:
- "**/*.js"
diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml
index 6977dc32b29..02a265b1ffd 100644
--- a/.github/workflows/pull-compliance.yml
+++ b/.github/workflows/pull-compliance.yml
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-go@v4
+ - uses: actions/setup-go@v5
with:
go-version-file: go.mod
check-latest: true
@@ -32,9 +32,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
- python-version: "3.11"
+ python-version: "3.12"
- run: pip install poetry
- run: make deps-py
- run: make lint-templates
@@ -45,9 +45,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
- python-version: "3.11"
+ python-version: "3.12"
- run: pip install poetry
- run: make deps-py
- run: make lint-yaml
@@ -64,13 +64,25 @@ jobs:
- run: make deps-frontend
- run: make lint-swagger
+ lint-spell:
+ if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.templates == 'true'
+ needs: files-changed
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+ check-latest: true
+ - run: make lint-spell
+
lint-go-windows:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-go@v4
+ - uses: actions/setup-go@v5
with:
go-version-file: go.mod
check-latest: true
@@ -87,7 +99,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-go@v4
+ - uses: actions/setup-go@v5
with:
go-version-file: go.mod
check-latest: true
@@ -102,7 +114,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-go@v4
+ - uses: actions/setup-go@v5
with:
go-version-file: go.mod
check-latest: true
@@ -130,7 +142,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-go@v4
+ - uses: actions/setup-go@v5
with:
go-version-file: go.mod
check-latest: true
@@ -175,7 +187,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-go@v4
+ - uses: actions/setup-go@v5
with:
go-version-file: go.mod
check-latest: true
diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml
index 97446e6cd3b..61c03915096 100644
--- a/.github/workflows/pull-db-tests.yml
+++ b/.github/workflows/pull-db-tests.yml
@@ -39,7 +39,7 @@ jobs:
- "9000:9000"
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-go@v4
+ - uses: actions/setup-go@v5
with:
go-version-file: go.mod
check-latest: true
@@ -49,7 +49,10 @@ jobs:
- run: make backend
env:
TAGS: bindata
- - run: make test-pgsql-migration test-pgsql
+ - name: run migration tests
+ run: make test-pgsql-migration
+ - name: run tests
+ run: make test-pgsql
timeout-minutes: 50
env:
TAGS: bindata gogit
@@ -64,7 +67,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-go@v4
+ - uses: actions/setup-go@v5
with:
go-version-file: go.mod
check-latest: true
@@ -72,7 +75,10 @@ jobs:
- run: make backend
env:
TAGS: bindata gogit sqlite sqlite_unlock_notify
- - run: make test-sqlite-migration test-sqlite
+ - name: run migration tests
+ run: make test-sqlite-migration
+ - name: run tests
+ run: make test-sqlite
timeout-minutes: 50
env:
TAGS: bindata gogit sqlite sqlite_unlock_notify
@@ -115,7 +121,7 @@ jobs:
- "9000:9000"
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-go@v4
+ - uses: actions/setup-go@v5
with:
go-version-file: go.mod
check-latest: true
@@ -165,7 +171,7 @@ jobs:
- "993:993"
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-go@v4
+ - uses: actions/setup-go@v5
with:
go-version-file: go.mod
check-latest: true
@@ -175,8 +181,10 @@ jobs:
- run: make backend
env:
TAGS: bindata
+ - name: run migration tests
+ run: make test-mysql-migration
- name: run tests
- run: make test-mysql-migration integration-test-coverage
+ run: make integration-test-coverage
env:
TAGS: bindata
RACE_ENABLED: true
@@ -198,7 +206,7 @@ jobs:
- "1433:1433"
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-go@v4
+ - uses: actions/setup-go@v5
with:
go-version-file: go.mod
check-latest: true
@@ -208,7 +216,9 @@ jobs:
- run: make backend
env:
TAGS: bindata
- - run: make test-mssql-migration test-mssql
+ - run: make test-mssql-migration
+ - name: run tests
+ run: make test-mssql
timeout-minutes: 50
env:
TAGS: bindata
diff --git a/.github/workflows/pull-e2e-tests.yml b/.github/workflows/pull-e2e-tests.yml
index 540263788d4..5a249db9f8d 100644
--- a/.github/workflows/pull-e2e-tests.yml
+++ b/.github/workflows/pull-e2e-tests.yml
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-go@v4
+ - uses: actions/setup-go@v5
with:
go-version-file: go.mod
check-latest: true
diff --git a/.github/workflows/pull-labeler.yml b/.github/workflows/pull-labeler.yml
index edd2f6d16e6..812819b5991 100644
--- a/.github/workflows/pull-labeler.yml
+++ b/.github/workflows/pull-labeler.yml
@@ -9,12 +9,12 @@ concurrency:
cancel-in-progress: true
jobs:
- label:
+ labeler:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- - uses: actions/labeler@v4
+ - uses: actions/labeler@v5
with:
- dot: true
+ sync-labels: true
diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml
index ef1e63df2ff..80e6683919f 100644
--- a/.github/workflows/release-nightly.yml
+++ b/.github/workflows/release-nightly.yml
@@ -18,7 +18,7 @@ jobs:
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- - uses: actions/setup-go@v4
+ - uses: actions/setup-go@v5
with:
go-version-file: go.mod
check-latest: true
@@ -64,7 +64,7 @@ jobs:
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- - uses: actions/setup-go@v4
+ - uses: actions/setup-go@v5
with:
go-version-file: go.mod
check-latest: true
@@ -101,7 +101,7 @@ jobs:
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- - uses: actions/setup-go@v4
+ - uses: actions/setup-go@v5
with:
go-version-file: go.mod
check-latest: true
diff --git a/.github/workflows/release-tag-rc.yml b/.github/workflows/release-tag-rc.yml
index 861179d9c86..12d1e1e4beb 100644
--- a/.github/workflows/release-tag-rc.yml
+++ b/.github/workflows/release-tag-rc.yml
@@ -17,7 +17,7 @@ jobs:
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- - uses: actions/setup-go@v4
+ - uses: actions/setup-go@v5
with:
go-version-file: go.mod
check-latest: true
@@ -44,7 +44,7 @@ jobs:
- name: Get cleaned branch name
id: clean_name
run: |
- REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
+ REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//')
echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
- name: configure aws
@@ -56,6 +56,10 @@ jobs:
- name: upload binaries to s3
run: |
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
+ - name: Install GH CLI
+ uses: dev-hanz-ops/install-gh-cli-action@v0.1.0
+ with:
+ gh-cli-version: 2.39.1
- name: create github release
run: |
gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --draft --notes-from-tag dist/release/*
@@ -74,6 +78,8 @@ jobs:
id: meta
with:
images: gitea/gitea
+ flavor: |
+ latest=false
# 1.2.3-rc0
tags: |
type=semver,pattern={{version}}
@@ -105,6 +111,7 @@ jobs:
images: gitea/gitea
# each tag below will have the suffix of -rootless
flavor: |
+ latest=false
suffix=-rootless
# 1.2.3-rc0
tags: |
diff --git a/.github/workflows/release-tag-version.yml b/.github/workflows/release-tag-version.yml
index c3fce7e2a7c..e0e93633e8a 100644
--- a/.github/workflows/release-tag-version.yml
+++ b/.github/workflows/release-tag-version.yml
@@ -19,7 +19,7 @@ jobs:
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- - uses: actions/setup-go@v4
+ - uses: actions/setup-go@v5
with:
go-version-file: go.mod
check-latest: true
@@ -46,7 +46,7 @@ jobs:
- name: Get cleaned branch name
id: clean_name
run: |
- REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
+ REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//')
echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
- name: configure aws
@@ -58,9 +58,13 @@ jobs:
- name: upload binaries to s3
run: |
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
+ - name: Install GH CLI
+ uses: dev-hanz-ops/install-gh-cli-action@v0.1.0
+ with:
+ gh-cli-version: 2.39.1
- name: create github release
run: |
- gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --draft --notes-from-tag dist/release/*
+ gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --notes-from-tag dist/release/*
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
docker-rootful:
@@ -82,7 +86,6 @@ jobs:
# 1.2
# 1.2.3
tags: |
- type=raw,value=latest
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{version}}
@@ -114,14 +117,13 @@ jobs:
images: gitea/gitea
# each tag below will have the suffix of -rootless
flavor: |
- suffix=-rootless
+ suffix=-rootless,onlatest=true
# this will generate tags in the following format (with -rootless suffix added):
# latest
# 1
# 1.2
# 1.2.3
tags: |
- type=raw,value=latest
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{version}}
diff --git a/.gitignore b/.gitignore
index 53365ed0b4e..8f2544866a1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,10 +11,11 @@ _test
.idea
# Goland's output filename can not be set manually
/go_build_*
+/gitea_*
# MS VSCode
.vscode
-__debug_bin
+__debug_bin*
*.cgo1.go
*.cgo2.c
diff --git a/.gitpod.yml b/.gitpod.yml
index 35b22c45ae7..ed2f57f4bf6 100644
--- a/.gitpod.yml
+++ b/.gitpod.yml
@@ -10,10 +10,19 @@ tasks:
- name: Run backend
command: |
gp sync-await setup
- if [ ! -f custom/conf/app.ini ]
- then
+
+ # Get the URL and extract the domain
+ url=$(gp url 3000)
+ domain=$(echo $url | awk -F[/:] '{print $4}')
+
+ if [ -f custom/conf/app.ini ]; then
+ sed -i "s|^ROOT_URL =.*|ROOT_URL = ${url}/|" custom/conf/app.ini
+ sed -i "s|^DOMAIN =.*|DOMAIN = ${domain}|" custom/conf/app.ini
+ sed -i "s|^SSH_DOMAIN =.*|SSH_DOMAIN = ${domain}|" custom/conf/app.ini
+ sed -i "s|^NO_REPLY_ADDRESS =.*|SSH_DOMAIN = noreply.${domain}|" custom/conf/app.ini
+ else
mkdir -p custom/conf/
- echo -e "[server]\nROOT_URL=$(gp url 3000)/" > custom/conf/app.ini
+ echo -e "[server]\nROOT_URL = ${url}/" > custom/conf/app.ini
echo -e "\n[database]\nDB_TYPE = sqlite3\nPATH = $GITPOD_REPO_ROOT/data/gitea.db" >> custom/conf/app.ini
fi
export TAGS="sqlite sqlite_unlock_notify"
diff --git a/.markdownlint.yaml b/.markdownlint.yaml
index 0af8bcd6fe2..b251ff796cb 100644
--- a/.markdownlint.yaml
+++ b/.markdownlint.yaml
@@ -1,17 +1,15 @@
commands-show-output: false
fenced-code-language: false
first-line-h1: false
-header-increment: false
+heading-increment: false
line-length: {code_blocks: false, tables: false, stern: true, line_length: -1}
no-alt-text: false
no-bare-urls: false
-no-blanks-blockquote: false
-no-emphasis-as-header: false
+no-emphasis-as-heading: false
no-empty-links: false
no-hard-tabs: {code_blocks: false}
no-inline-html: false
no-space-in-code: false
no-space-in-emphasis: false
-no-trailing-punctuation: false
no-trailing-spaces: {br_spaces: 0}
single-h1: false
diff --git a/.stylelintrc.yaml b/.stylelintrc.yaml
index f889a6867c9..c7725159f10 100644
--- a/.stylelintrc.yaml
+++ b/.stylelintrc.yaml
@@ -1,7 +1,7 @@
plugins:
- stylelint-declaration-strict-value
- stylelint-declaration-block-no-ignored-properties
- - stylelint-stylistic
+ - "@stylistic/stylelint-plugin"
ignoreFiles:
- "**/*.go"
@@ -17,12 +17,89 @@ overrides:
customSyntax: postcss-html
rules:
+ "@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/named-grid-areas-alignment": 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
alpha-value-notation: null
annotation-no-unknown: true
at-rule-allowed-list: null
at-rule-disallowed-list: null
at-rule-empty-line-before: null
- at-rule-no-unknown: true
+ at-rule-no-unknown: [true, {ignoreAtRules: [tailwind]}]
at-rule-no-vendor-prefix: true
at-rule-property-required-list: null
block-no-empty: true
@@ -74,7 +151,7 @@ rules:
keyframe-declaration-no-important: true
keyframe-selector-notation: null
keyframes-name-pattern: null
- length-zero-no-unit: true
+ length-zero-no-unit: [true, ignore: [custom-properties], ignoreFunctions: [var]]
max-nesting-depth: null
media-feature-name-allowed-list: null
media-feature-name-disallowed-list: null
@@ -137,82 +214,6 @@ 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/CHANGELOG.md b/CHANGELOG.md
index ea0f80c0c91..e119d0bec01 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,688 @@ This changelog goes through all the changes that have been made in each release
without substantial changes to our git log; to see the highlights of what has
been added to each release, please refer to the [blog](https://blog.gitea.com).
+## [1.21.6](https://github.com/go-gitea/gitea/releases/tag/v1.21.6) - 2024-02-22
+
+* SECURITY
+ * Fix XSS vulnerabilities (#29336)
+ * Use general token signing secret (#29205) (#29325)
+* ENHANCEMENTS
+ * Refactor git version functions and check compatibility (#29155) (#29157)
+ * Improve user experience for outdated comments (#29050) (#29086)
+ * Hide code links on release page if user cannot read code (#29064) (#29066)
+ * Wrap contained tags and branches again (#29021) (#29026)
+ * Fix incorrect button CSS usages (#29015) (#29023)
+ * Strip trailing newline in markdown code copy (#29019) (#29022)
+ * Implement some action notifier functions (#29173) (#29308)
+ * Load outdated comments when (un)resolving conversation on PR timeline (#29203) (#29221)
+* BUGFIXES
+ * Refactor issue template parsing and fix API endpoint (#29069) (#29140)
+ * Fix swift packages not resolving (#29095) (#29102)
+ * Remove SSH workaround (#27893) (#29332)
+ * Only log error when tag sync fails (#29295) (#29327)
+ * Fix SSPI user creation (#28948) (#29323)
+ * Improve the `issue_comment` workflow trigger event (#29277) (#29322)
+ * Discard unread data of `git cat-file` (#29297) (#29310)
+ * Fix error display when merging PRs (#29288) (#29309)
+ * Prevent double use of `git cat-file` session. (#29298) (#29301)
+ * Fix missing link on outgoing new release notifications (#29079) (#29300)
+ * Fix debian InRelease Acquire-By-Hash newline (#29204) (#29299)
+ * Always write proc-receive hook for all git versions (#29287) (#29291)
+ * Do not show delete button when time tracker is disabled (#29257) (#29279)
+ * Workaround to clean up old reviews on creating a new one (#28554) (#29264)
+ * Fix bug when the linked account was disactived and list the linked accounts (#29263)
+ * Do not use lower tag names to find releases/tags (#29261) (#29262)
+ * Fix missed edit issues event for actions (#29237) (#29251)
+ * Only delete scheduled workflows when needed (#29091) (#29235)
+ * Make submit event code work with both jQuery event and native event (#29223) (#29234)
+ * Fix push to create with capitalize repo name (#29090) (#29206)
+ * Use ghost user if user was not found (#29161) (#29169)
+ * Dont load Review if Comment is CommentTypeReviewRequest (#28551) (#29160)
+ * Refactor parseSignatureFromCommitLine (#29054) (#29108)
+ * Avoid showing unnecessary JS errors when there are elements with different origin on the page (#29081) (#29089)
+ * Fix gitea-origin-url with default ports (#29085) (#29088)
+ * Fix orgmode link resolving (#29024) (#29076)
+ * Fix Elasticsearh Request Entity Too Large #28117 (#29062) (#29075)
+ * Do not render empty comments (#29039) (#29049)
+ * Avoid sending update/delete release notice when it is draft (#29008) (#29025)
+ * Fix gitea-action user avatar broken on edited menu (#29190) (#29307)
+ * Disallow merge when required checked are missing (#29143) (#29268)
+ * Fix incorrect link to swift doc and swift package-registry login command (#29096) (#29103)
+ * Convert visibility to number (#29226) (#29244)
+* DOCS
+ * Remove outdated docs from some languages (#27530) (#29208)
+ * Fix typos in the documentation (#29048) (#29056)
+ * Explained where create issue/PR template (#29035)
+
+## [1.21.5](https://github.com/go-gitea/gitea/releases/tag/v1.21.5) - 2024-01-31
+
+* SECURITY
+ * Prevent anonymous container access if `RequireSignInView` is enabled (#28877) (#28882)
+ * Update go dependencies and fix go-git (#28893) (#28934)
+* BUGFIXES
+ * Revert "Speed up loading the dashboard on mysql/mariadb (#28546)" (#29006) (#29007)
+ * Fix an actions schedule bug (#28942) (#28999)
+ * Fix update enable_prune even if mirror_interval is not provided (#28905) (#28929)
+ * Fix uploaded artifacts should be overwritten (#28726) backport v1.21 (#28832)
+ * Preserve BOM in web editor (#28935) (#28959)
+ * Strip `/` from relative links (#28932) (#28952)
+ * Don't remove all mirror repository's releases when mirroring (#28817) (#28939)
+ * Implement `MigrateRepository` for the actions notifier (#28920) (#28923)
+ * Respect branch info for relative links (#28909) (#28922)
+ * Don't reload timeline page when (un)resolving or replying conversation (#28654) (#28917)
+ * Only migrate the first 255 chars of a Github issue title (#28902) (#28912)
+ * Fix sort bug on repository issues list (#28897) (#28901)
+ * Fix `DeleteCollaboration` transaction behaviour (#28886) (#28889)
+ * Fix schedule not trigger bug because matching full ref name with short ref name (#28874) (#28888)
+ * Fix migrate storage bug (#28830) (#28867)
+ * Fix archive creating LFS hooks and breaking pull requests (#28848) (#28851)
+ * Fix reverting a merge commit failing (#28794) (#28825)
+ * Upgrade xorm to v1.3.7 to fix a resource leak problem caused by Iterate (#28891) (#28895)
+ * Fix incorrect PostgreSQL connection string for Unix sockets (#28865) (#28870)
+* ENHANCEMENTS
+ * Make loading animation less aggressive (#28955) (#28956)
+ * Avoid duplicate JS error messages on UI (#28873) (#28881)
+ * Bump `@github/relative-time-element` to 4.3.1 (#28819) (#28826)
+* MISC
+ * Warn that `DISABLE_QUERY_AUTH_TOKEN` is false only if it's explicitly defined (#28783) (#28868)
+ * Remove duplicated checkinit on git module (#28824) (#28831)
+
+## [1.21.4](https://github.com/go-gitea/gitea/releases/tag/v1.21.4) - 2024-01-16
+
+* SECURITY
+ * Update github.com/cloudflare/circl (#28789) (#28790)
+ * Require token for GET subscription endpoint (#28765) (#28768)
+* BUGFIXES
+ * Use refname:strip-2 instead of refname:short when syncing tags (#28797) (#28811)
+ * Fix links in issue card (#28806) (#28807)
+ * Fix nil pointer panic when exec some gitea cli command (#28791) (#28795)
+ * Require token for GET subscription endpoint (#28765) (#28778)
+ * Fix button size in "attached header right" (#28770) (#28774)
+ * Fix `convert.ToTeams` on empty input (#28426) (#28767)
+ * Hide code related setting options in repository when code unit is disabled (#28631) (#28749)
+ * Fix incorrect URL for "Reference in New Issue" (#28716) (#28723)
+ * Fix panic when parsing empty pgsql host (#28708) (#28709)
+ * Upgrade xorm to new version which supported update join for all supported databases (#28590) (#28668)
+ * Fix alpine package files are not rebuilt (#28638) (#28665)
+ * Avoid cycle-redirecting user/login page (#28636) (#28658)
+ * Fix empty ref for cron workflow runs (#28640) (#28647)
+ * Remove unnecessary syncbranchToDB with tests (#28624) (#28629)
+ * Use known issue IID to generate new PR index number when migrating from GitLab (#28616) (#28618)
+ * Fix flex container width (#28603) (#28605)
+ * Fix the scroll behavior for emoji/mention list (#28597) (#28601)
+ * Fix wrong due date rendering in issue list page (#28588) (#28591)
+ * Fix `status_check_contexts` matching bug (#28582) (#28589)
+ * Fix 500 error of searching commits (#28576) (#28579)
+ * Use information from previous blame parts (#28572) (#28577)
+ * Update mermaid for 1.21 (#28571)
+ * Fix 405 method not allowed CORS / OIDC (#28583) (#28586) (#28587) (#28611)
+ * Fix `GetCommitStatuses` (#28787) (#28804)
+ * Forbid removing the last admin user (#28337) (#28793)
+ * Fix schedule tasks bugs (#28691) (#28780)
+ * Fix issue dependencies (#27736) (#28776)
+ * Fix system webhooks API bug (#28531) (#28666)
+ * Fix when private user following user, private user will not be counted in his own view (#28037) (#28792)
+ * Render code block in activity tab (#28816) (#28818)
+* ENHANCEMENTS
+ * Rework markup link rendering (#26745) (#28803)
+ * Modernize merge button (#28140) (#28786)
+ * Speed up loading the dashboard on mysql/mariadb (#28546) (#28784)
+ * Assign pull request to project during creation (#28227) (#28775)
+ * Show description as tooltip instead of title for labels (#28754) (#28766)
+ * Make template `DateTime` show proper tooltip (#28677) (#28683)
+ * Switch destination directory for apt signing keys (#28639) (#28642)
+ * Include heap pprof in diagnosis report to help debugging memory leaks (#28596) (#28599)
+* DOCS
+ * Suggest to use Type=simple for systemd service (#28717) (#28722)
+ * Extend description for ARTIFACT_RETENTION_DAYS (#28626) (#28630)
+* MISC
+ * Add -F to commit search to treat keywords as strings (#28744) (#28748)
+ * Add download attribute to release attachments (#28739) (#28740)
+ * Concatenate error in `checkIfPRContentChanged` (#28731) (#28737)
+ * Improve 1.21 document for Database Preparation (#28643) (#28644)
+
+## [1.21.3](https://github.com/go-gitea/gitea/releases/tag/v1.21.3) - 2023-12-21
+
+* SECURITY
+ * Update golang.org/x/crypto (#28519)
+* API
+ * chore(api): support ignore password if login source type is LDAP for creating user API (#28491) (#28525)
+ * Add endpoint for not implemented Docker auth (#28457) (#28462)
+* ENHANCEMENTS
+ * Add option to disable ambiguous unicode characters detection (#28454) (#28499)
+ * Refactor SSH clone URL generation code (#28421) (#28480)
+ * Polyfill SubmitEvent for PaleMoon (#28441) (#28478)
+* BUGFIXES
+ * Fix the issue ref rendering for wiki (#28556) (#28559)
+ * Fix duplicate ID when deleting repo (#28520) (#28528)
+ * Only check online runner when detecting matching runners in workflows (#28286) (#28512)
+ * Initalize stroage for orphaned repository doctor (#28487) (#28490)
+ * Fix possible nil pointer access (#28428) (#28440)
+ * Don't show unnecessary citation JS error on UI (#28433) (#28437)
+* DOCS
+ * Update actions document about comparsion as Github Actions (#28560) (#28564)
+ * Fix documents for "custom/public/assets/" (#28465) (#28467)
+* MISC
+ * Fix inperformant query on retrifing review from database. (#28552) (#28562)
+ * Improve the prompt for "ssh-keygen sign" (#28509) (#28510)
+ * Update docs for DISABLE_QUERY_AUTH_TOKEN (#28485) (#28488)
+ * Fix Chinese translation of config cheat sheet[API] (#28472) (#28473)
+ * Retry SSH key verification with additional CRLF if it failed (#28392) (#28464)
+
+## [1.21.2](https://github.com/go-gitea/gitea/releases/tag/v1.21.2) - 2023-12-12
+
+* SECURITY
+ * Rebuild with recently released golang version
+ * Fix missing check (#28406) (#28411)
+ * Do some missing checks (#28423) (#28432)
+* BUGFIXES
+ * Fix margin in server signed signature verification view (#28379) (#28381)
+ * Fix object does not exist error when checking citation file (#28314) (#28369)
+ * Use `filepath` instead of `path` to create SQLite3 database file (#28374) (#28378)
+ * Fix the runs will not be displayed bug when the main branch have no workflows but other branches have (#28359) (#28365)
+ * Handle repository.size column being NULL in migration v263 (#28336) (#28363)
+ * Convert git commit summary to valid UTF8. (#28356) (#28358)
+ * Fix migration panic due to an empty review comment diff (#28334) (#28362)
+ * Add `HEAD` support for rpm repo files (#28309) (#28360)
+ * Fix RPM/Debian signature key creation (#28352) (#28353)
+ * Keep profile tab when clicking on Language (#28320) (#28331)
+ * Fix missing issue search index update when changing status (#28325) (#28330)
+ * Fix wrong link in `protect_branch_name_pattern_desc` (#28313) (#28315)
+ * Read `previous` info from git blame (#28306) (#28310)
+ * Ignore "non-existing" errors when getDirectorySize calculates the size (#28276) (#28285)
+ * Use appSubUrl for OAuth2 callback URL tip (#28266) (#28275)
+ * Meilisearch: require all query terms to be matched (#28293) (#28296)
+ * Fix required error for token name (#28267) (#28284)
+ * Fix issue will be detected as pull request when checking `First-time contributor` (#28237) (#28271)
+ * Use full width for project boards (#28225) (#28245)
+ * Increase "version" when update the setting value to a same value as before (#28243) (#28244)
+ * Also sync DB branches on push if necessary (#28361) (#28403)
+ * Make gogit Repository.GetBranchNames consistent (#28348) (#28386)
+ * Recover from panic in cron task (#28409) (#28425)
+ * Deprecate query string auth tokens (#28390) (#28430)
+* ENHANCEMENTS
+ * Improve doctor cli behavior (#28422) (#28424)
+ * Fix margin in server signed signature verification view (#28379) (#28381)
+ * Refactor template empty checks (#28351) (#28354)
+ * Read `previous` info from git blame (#28306) (#28310)
+ * Use full width for project boards (#28225) (#28245)
+ * Enable system users search via the API (#28013) (#28018)
+
+## [1.21.1](https://github.com/go-gitea/gitea/releases/tag/v1.21.1) - 2023-11-26
+
+* SECURITY
+ * Fix comment permissions (#28213) (#28216)
+* BUGFIXES
+ * Fix delete-orphaned-repos (#28200) (#28202)
+ * Make CORS work for oauth2 handlers (#28184) (#28185)
+ * Fix missing buttons (#28179) (#28181)
+ * Fix no ActionTaskOutput table waring (#28149) (#28152)
+ * Fix empty action run title (#28113) (#28148)
+ * Use "is-loading" to avoid duplicate form submit for code comment (#28143) (#28147)
+ * Fix Matrix and MSTeams nil dereference (#28089) (#28105)
+ * Fix incorrect pgsql conn builder behavior (#28085) (#28098)
+ * Fix system config cache expiration timing (#28072) (#28090)
+ * Restricted users only see repos in orgs which their team was assigned to (#28025) (#28051)
+* API
+ * Fix permissions for Token DELETE endpoint to match GET and POST (#27610) (#28099)
+* ENHANCEMENTS
+ * Do not display search box when there's no packages yet (#28146) (#28159)
+ * Add missing `packages.cleanup.success` (#28129) (#28132)
+* DOCS
+ * Docs: Replace deprecated IS_TLS_ENABLED mailer setting in email setup (#28205) (#28208)
+ * Fix the description about the default setting for action in quick start document (#28160) (#28168)
+ * Add guide page to actions when there's no workflows (#28145) (#28153)
+* MISC
+ * Use full width for PR comparison (#28182) (#28186)
+
+## [1.21.0](https://github.com/go-gitea/gitea/releases/tag/v1.21.0) - 2023-11-14
+
+* BREAKING
+ * Restrict certificate type for builtin SSH server (#26789)
+ * Refactor to use urfave/cli/v2 (#25959)
+ * Move public asset files to the proper directory (#25907)
+ * Remove commit status running and warning to align GitHub (#25839) (partially reverted: Restore warning commit status (#27504) (#27529))
+ * Remove "CHARSET" config option for MySQL, always use "utf8mb4" (#25413)
+ * Set SSH_AUTHORIZED_KEYS_BACKUP to false (#25412)
+* FEATURES
+ * User details page (#26713)
+ * Chore(actions): support cron schedule task (#26655)
+ * Support rebuilding issue indexer manually (#26546)
+ * Allow to archive labels (#26478)
+ * Add disable workflow feature (#26413)
+ * Support `.git-blame-ignore-revs` file (#26395)
+ * Pre-register OAuth2 applications for git credential helpers (#26291)
+ * Add `Retry` button when creating a mirror-repo fails (#26228)
+ * Artifacts retention and auto clean up (#26131)
+ * Serve pre-defined files in "public", add "security.txt", add CORS header for ".well-known" (#25974)
+ * Implement auto-cancellation of concurrent jobs if the event is push (#25716)
+ * Newly pushed branches hints on repository home page (#25715)
+ * Display branch commit status (#25608)
+ * Add direct serving of package content (#25543)
+ * Add commits dropdown in PR files view and allow commit by commit review (#25528)
+ * Allow package cleanup from admin page (#25307)
+ * Batch delete issue and improve tippy opts (#25253)
+ * Show branches and tags that contain a commit (#25180)
+ * Add actor and status dropdowns to run list (#25118)
+ * Allow Organisations to have a E-Mail (#25082)
+ * Add codeowners feature (#24910)
+ * Actions Artifacts support uploading multiple files and directories (#24874)
+ * Support configuration variables on Gitea Actions (#24724)
+ * Support downloading raw task logs (#24451)
+* API
+ * Unify two factor check (#27915) (#27929)
+ * Fix package webhook (#27839) (#27855)
+ * Fix/upload artifact error windows (#27802) (#27840)
+ * Fix bad method call when deleting user secrets via API (#27829) (#27831)
+ * Do not force creation of _cargo-index repo on publish (#27266) (#27765)
+ * Delete repos of org when purge delete user (#27273) (#27728)
+ * Fix org team endpoint (#27721) (#27727)
+ * Api: GetPullRequestCommits: return file list (#27483) (#27539)
+ * Don't let API add 2 exclusive labels from same scope (#27433) (#27460)
+ * Redefine the meaning of column is_active to make Actions Registration Token generation easier (#27143) (#27304)
+ * Fix PushEvent NullPointerException jenkinsci/github-plugin (#27203) (#27251)
+ * Fix organization field being null in POST /orgs/{orgid}/teams (#27150) (#27163)
+ * Allow empty Conan files (#27092)
+ * Fix token endpoints ignore specified account (#27080)
+ * Reduce usage of `db.DefaultContext` (#27073) (#27083) (#27089) (#27103) (#27262) (#27265) (#27347) (#26076)
+ * Make SSPI auth mockable (#27036)
+ * Extract auth middleware from service (#27028)
+ * Add `RemoteAddress` to mirrors (#26952)
+ * Feat(API): add routes and functions for managing user's secrets (#26909)
+ * Feat(API): add secret deletion functionality for repository (#26808)
+ * Feat(API): add route and implementation for creating/updating repository secret (#26766)
+ * Add Upload URL to release API (#26663)
+ * Feat(API): update and delete secret for managing organization secrets (#26660)
+ * Feat: implement organization secret creation API (#26566)
+ * Add API route to list org secrets (#26485)
+ * Set commit id when ref used explicitly (#26447)
+ * PATCH branch-protection updates check list even when checks are disabled (#26351)
+ * Add file status for API "Get a single commit from a repository" (#16205) (#25831)
+ * Add API for changing Avatars (#25369)
+* BUGFIXES
+ * Fix viewing wiki commit on empty repo (#28040) (#28044)
+ * Enable system users for comment.LoadPoster (#28014) (#28032)
+ * Fixed duplicate attachments on dump on windows (#28019) (#28031)
+ * Fix wrong xorm Delete usage(backport for 1.21) (#28002)
+ * Add word-break to repo description in home page (#27924) (#27957)
+ * Fix rendering assignee changed comments without assignee (#27927) (#27952)
+ * Add word break to release title (#27942) (#27947)
+ * Fix JS NPE when viewing specific range of PR commits (#27912) (#27923)
+ * Show correct commit sha when viewing single commit diff (#27916) (#27921)
+ * Fix 500 when deleting a dismissed review (#27903) (#27910)
+ * Fix DownloadFunc when migrating releases (#27887) (#27890)
+ * Fix http protocol auth (#27875) (#27876)
+ * Refactor postgres connection string building (#27723) (#27869)
+ * Close all hashed buffers (#27787) (#27790)
+ * Fix label render containing invalid HTML (#27752) (#27762)
+ * Fix duplicate project board when hitting `enter` key (#27746) (#27751)
+ * Fix `link-action` redirect network error (#27734) (#27749)
+ * Fix sticky diff header background (#27697) (#27712)
+ * Always delete existing scheduled action tasks (#27662) (#27688)
+ * Support allowed hosts for webhook to work with proxy (#27655) (#27675)
+ * Fix poster is not loaded in get default merge message (#27657) (#27666)
+ * Improve dropdown button alignment and fix hover bug (#27632) (#27637)
+ * Improve retrying index issues (#27554) (#27634)
+ * Fix 404 when deleting Docker package with an internal version (#27615) (#27630)
+ * Backport manually for a tmpl issue in v1.21 (#27612)
+ * Don't show Link to TOTP if not set up (#27585) (#27588)
+ * Fix data-race bug when accessing task.LastRun (#27584) (#27586)
+ * Fix attachment download bug (#27486) (#27571)
+ * Respect SSH.KeygenPath option when calculating ssh key fingerprints (#27536) (#27551)
+ * Improve dropdown's behavior when there is a search input in menu (#27526) (#27534)
+ * Fix panic in storageHandler (#27446) (#27479)
+ * When comparing with an non-exist repository, return 404 but 500 (#27437) (#27442)
+ * Fix pr template (#27436) (#27440)
+ * Fix git 2.11 error when checking IsEmpty (#27393) (#27397)
+ * Allow get release download files and lfs files with oauth2 token format (#26430) (#27379)
+ * Fix missing ctx for GetRepoLink in dashboard (#27372) (#27375)
+ * Absolute positioned checkboxes overlay floated elements (#26870) (#27366)
+ * Introduce fixes and more rigorous tests for 'Show on a map' feature (#26803) (#27365)
+ * Fix repo count in org action settings (#27245) (#27353)
+ * Add logs for data broken of comment review (#27326) (#27345)
+ * Fix the approval count of PR when there is no protection branch rule (#27272) (#27343)
+ * Fix Bug in Issue Config when only contact links are set (#26521) (#27334)
+ * Improve issue history dialog and make poster can delete their own history (#27323) (#27327)
+ * Fix orphan check for deleted branch (#27310) (#27321)
+ * Fix protected branch icon location (#26576) (#27317)
+ * Fix yaml test (#27297) (#27303)
+ * Fix some animation bugs (#27287) (#27294)
+ * Fix incorrect change from #27231 (#27275) (#27282)
+ * Add missing public user visibility in user details page (#27246) (#27250)
+ * Fix EOL handling in web editor (#27141) (#27234)
+ * Fix issues on action runners page (#27226) (#27233)
+ * Quote table `release` in sql queries (#27205) (#27218)
+ * Fix release URL in webhooks (#27182) (#27185)
+ * Fix review request number and add more tests (#27104) (#27168)
+ * Fix the variable regexp pattern on web page (#27161) (#27164)
+ * Fix: treat tab "overview" as "repositories" in user profiles without readme (#27124)
+ * Fix NPE when editing OAuth2 applications (#27078)
+ * Fix the incorrect route path in the user edit page. (#27007)
+ * Fix the secret regexp pattern on web page (#26910)
+ * Allow users with write permissions for issues to add attachments with API (#26837)
+ * Make "link-action" backend code respond correct JSON content (#26680)
+ * Use line-height: normal by default (#26635)
+ * Fix NPM packages name validation (#26595)
+ * Rewrite the DiffFileTreeItem and fix misalignment (#26565)
+ * Return empty when searching issues with no repos (#26545)
+ * Explain SearchOptions and fix ToSearchOptions (#26542)
+ * Add missing triggers to update issue indexer (#26539)
+ * Handle base64 decoding correctly to avoid panic (#26483)
+ * Avoiding accessing undefined mentionValues (#26461)
+ * Fix incorrect redirection in new issue using references (#26440)
+ * Fix the bug when getting files changed for `pull_request_target` event (#26320)
+ * Remove IsWarning in tmpl (#26120)
+ * Fix loading `LFS_JWT_SECRET` from wrong section (#26109)
+ * Fixing redirection issue for logged-in users (#26105)
+ * Improve "gitea doctor" sub-command and fix "help" commands (#26072)
+ * Fix the truncate and alignment problem for some admin tables (#26042)
+ * Update minimum password length requirements (#25946)
+ * Do not "guess" the file encoding/BOM when using API to upload files (#25828)
+ * Restructure issue list template, styles (#25750)
+ * Fix `ref` for workflows triggered by `pull_request_target` (#25743)
+ * Fix issues indexer document mapping (#25619)
+ * Use JSON response for "user/logout" (#25522)
+ * Fix migrate page layout on mobile (#25507)
+ * Link to existing PR when trying to open a new PR on the same branches (#25494)
+ * Do not publish docker release images on `-dev` tags (#25471)
+ * Support `pull_request_target` event (#25229)
+ * Modify the content format of the Feishu webhook (#25106)
+* ENHANCEMENTS
+ * Render email addresses as such if followed by punctuation (#27987) (#27992)
+ * Show error toast when file size exceeds the limits (#27985) (#27986)
+ * Fix citation error when the file size is larger than 1024 bytes (#27958) (#27965)
+ * Remove action runners on user deletion (#27902) (#27908)
+ * Remove set tabindex on view issue (#27892) (#27896)
+ * Reduce margin/padding on flex-list items and divider (#27872) (#27874)
+ * Change katex limits (#27823) (#27868)
+ * Clean up template locale usage (#27856) (#27857)
+ * Add dedicated class for empty placeholders (#27788) (#27792)
+ * Add gap between diff boxes (#27776) (#27781)
+ * Fix incorrect "tab" parameter for repo search sub-template (#27755) (#27764)
+ * Enable followCursor for language stats bar (#27713) (#27739)
+ * Improve diff tree spacing (#27714) (#27719)
+ * Feed UI Improvements (#27356) (#27717)
+ * Improve feed icons and feed merge text color (#27498) (#27716)
+ * [FIX] resolve confusing colors in languages stats by insert a gap (#27704) (#27715)
+ * Add doctor dbconsistency fix to delete repos with no owner (#27290) (#27693)
+ * Fix required checkboxes in issue forms (#27592) (#27692)
+ * Hide archived labels by default from the suggestions when assigning labels for an issue (#27451) (#27661)
+ * Cleanup repo details icons/labels (#27644) (#27654)
+ * Keep filter when showing unfiltered results on explore page (#27192) (#27589)
+ * Show manual cron run's last time (#27544) (#27577)
+ * Revert "Fix pr template (#27436)" (#27567)
+ * Increase queue length (#27555) (#27562)
+ * Avoid run change title process when the title is same (#27467) (#27558)
+ * Remove max-width and add hide text overflow (#27359) (#27550)
+ * Add hover background to wiki list page (#27507) (#27521)
+ * Fix mermaid flowchart margin issue (#27503) (#27516)
+ * Refactor system setting (#27000) (#27452)
+ * Fix missing `ctx` in new_form.tmpl (#27434) (#27438)
+ * Add Index to `action.user_id` (#27403) (#27425)
+ * Don't use subselect in `DeleteIssuesByRepoID` (#27332) (#27408)
+ * Add support for HEAD ref in /src/branch and /src/commit routes (#27384) (#27407)
+ * Make Actions tasks/jobs timeouts configurable by the user (#27400) (#27402)
+ * Hide archived labels when filtering by labels on the issue list (#27115) (#27381)
+ * Highlight user details link (#26998) (#27376)
+ * Add protected branch name description (#27257) (#27351)
+ * Improve tree not found page (#26570) (#27346)
+ * Add Index to `comment.dependent_issue_id` (#27325) (#27340)
+ * Improve branch list UI (#27319) (#27324)
+ * Fix divider in subscription page (#27298) (#27301)
+ * Add missed return to actions view fetch (#27289) (#27293)
+ * Backport ctx locale refactoring manually (#27231) (#27259) (#27260)
+ * Disable `Test Delivery` and `Replay` webhook buttons when webhook is inactive (#27211) (#27253)
+ * Use mask-based fade-out effect for `.new-menu` (#27181) (#27243)
+ * Cleanup locale function usage (#27227) (#27240)
+ * Fix z-index on markdown completion (#27237) (#27239)
+ * Fix Fomantic UI dropdown icon bug when there is a search input in menu (#27225) (#27228)
+ * Allow copying issue comment link on archived repos and when not logged in (#27193) (#27210)
+ * Fix: text decorator on issue sidebar menu label (#27206) (#27209)
+ * Fix dropdown icon position (#27175) (#27177)
+ * Add index to `issue_user.issue_id` (#27154) (#27158)
+ * Increase auth provider icon size on login page (#27122)
+ * Remove a `gt-float-right` and some unnecessary helpers (#27110)
+ * Change green buttons to primary color (#27099)
+ * Use db.WithTx for AddTeamMember to avoid ctx abuse (#27095)
+ * Use `print` instead of `printf` (#27093)
+ * Remove the useless function `GetUserIssueStats` and move relevant tests to `indexer_test.go` (#27067)
+ * Search branches (#27055)
+ * Display all user types and org types on admin management UI (#27050)
+ * Ui correction in mobile view nav bar left aligned items. (#27046)
+ * Chroma color tweaks (#26978)
+ * Move some functions to service layer (#26969)
+ * Improve "language stats" UI (#26968)
+ * Replace `util.SliceXxx` with `slices.Xxx` (#26958)
+ * Refactor dashboard/feed.tmpl (#26956)
+ * Move repository deletion to service layer (#26948)
+ * Fix the missing repo count (#26942)
+ * Improve hint when uploading a too large avatar (#26935)
+ * Extract common code to new template (#26933)
+ * Move createrepository from module to service layer (#26927)
+ * Move notification interface to services layer (#26915)
+ * Move feed notification service layer (#26908)
+ * Move ui notification to service layer (#26907)
+ * Move indexer notification to service layer (#26906)
+ * Move mail notification logic to service layer (#26905)
+ * Extract common code to new template (#26903)
+ * Show queue's active worker number (#26896)
+ * Fix media description render for orgmode (#26895)
+ * Remove CSS `has` selector and improve various styles (#26891)
+ * Relocate the `RSS user feed` button (#26882)
+ * Refactor "shortsha" (#26877)
+ * Refactor `og:description` to limit the max length (#26876)
+ * Move web/api context related testing function into a separate package (#26859)
+ * Redable error on S3 storage connection failure (#26856)
+ * Improve opengraph previews (#26851)
+ * Add more descriptive error on forgot password page (#26848)
+ * Show always repo count in header (#26842)
+ * Remove "TODO" tasks from CSS file (#26835)
+ * Render code blocks in repo description (#26830)
+ * Minor dashboard tweaks, fix flex-list margins (#26829)
+ * Remove polluted `.ui.right` (#26825)
+ * Display archived labels specially when listing labels (#26820)
+ * Remove polluted ".ui.left" style (#26809)
+ * Make it posible to customize nav text color via css var (#26807)
+ * Refactor lfs requests (#26783)
+ * Improve flex list item padding (#26779)
+ * Remove fomantic `text` module (#26777)
+ * Remove fomantic `item` module (#26775)
+ * Remove redundant nil check in `WalkGitLog` (#26773)
+ * Reduce some allocations in type conversion (#26772)
+ * Refactor some CSS styles and simplify code (#26771)
+ * Unify `border-radius` behavior (#26770)
+ * Improve modal dialog UI (#26764)
+ * Allow "latest" to be used in release vTag when downloading file (#26748)
+ * Adding hint `Archived` to archive label. (#26741)
+ * Move `modules/mirror` to `services` (#26737)
+ * Add "dir=auto" for input/textarea elements by default (#26735)
+ * Add auth-required to config.json for Cargo http registry (#26729)
+ * Simplify helper CSS classes and avoid abuse (#26728)
+ * Make web context initialize correctly for different cases (#26726)
+ * Focus editor on "Write" tab click (#26714)
+ * Remove incorrect CSS helper classes (#26712)
+ * Fix review bar misalignment (#26711)
+ * Add reverseproxy auth for API back with default disabled (#26703)
+ * Add default label in branch select list (#26697)
+ * Improve Image Diff UI (#26696)
+ * Fixed text overflow in dropdown menu (#26694)
+ * [Refactor] getIssueStatsChunk to move inner function into own one (#26671)
+ * Remove fomantic loader module (#26670)
+ * Add `member`, `collaborator`, `contributor`, and `first-time contributor` roles and tooltips (#26658)
+ * Improve some flex layouts (#26649)
+ * Improve the branch selector tab UI (#26631)
+ * Improve show role (#26621)
+ * Remove avatarHTML from template helpers (#26598)
+ * Allow text selection in actions step header (#26588)
+ * Improve translation of milestone filters (#26569)
+ * Add optimistic lock to ActionRun table (#26563)
+ * Update team invitation email link (#26550)
+ * Differentiate better between user settings and admin settings (#26538)
+ * Check disabled workflow when rerun jobs (#26535)
+ * Improve deadline icon location in milestone list page (#26532)
+ * Improve repo sub menu (#26531)
+ * Fix the display of org level badges (#26504)
+ * Rename `Sync2` -> `Sync` (#26479)
+ * Fix stderr usages (#26477)
+ * Remove fomantic transition module (#26469)
+ * Refactor tests (#26464)
+ * Refactor project templates (#26448)
+ * Fall back to esbuild for css minify (#26445)
+ * Always show usernames in reaction tooltips (#26444)
+ * Use correct pull request commit link instead of a generic commit link (#26434)
+ * Refactor "editorconfig" (#26391)
+ * Make `user-content-* ` consistent with github (#26388)
+ * Remove unnecessary template helper repoAvatar (#26387)
+ * Remove unnecessary template helper DisableGravatar (#26386)
+ * Use template context function for avatar rendering (#26385)
+ * Rename code_langauge.go to code_language.go (#26377)
+ * Use more `IssueList` instead of `[]*Issue` (#26369)
+ * Do not highlight `#number` in documents (#26365)
+ * Fix display problems of members and teams unit (#26363)
+ * Fix 404 error when remove self from an organization (#26362)
+ * Improve CLI and messages (#26341)
+ * Refactor backend SVG package and add tests (#26335)
+ * Add link to job details and tooltip to commit status in repo list in dashboard (#26326)
+ * Use yellow if an approved review is stale (#26312)
+ * Remove commit load branches and tags in wiki repo (#26304)
+ * Add highlight to selected repos in milestone dashboard (#26300)
+ * Delete `issue_service.CreateComment` (#26298)
+ * Do not show Profile README when repository is private (#26295)
+ * Tweak actions menu (#26278)
+ * Start using template context function (#26254)
+ * Use calendar icon for `Joined on...` in profiles (#26215)
+ * Add 'Show on a map' button to Location in profile, fix layout (#26214)
+ * Render plaintext task list items for markdown files (#26186)
+ * Add tooltip to describe LFS table column and color `delete LFS file` button red (#26181)
+ * Release attachments duplicated check (#26176)
+ * De-emphasize issue sidebar buttons (#26171)
+ * Fixing the align of commit stats in commit_page template. (#26161)
+ * Allow editing push mirrors after creation (#26151)
+ * Move web JSON functions to web context and simplify code (#26132)
+ * Refactor improve NoBetterThan (#26126)
+ * Improve clickable area in repo action view page (#26115)
+ * Add context parameter to some database functions (#26055)
+ * Docusaurus-ify (#26051)
+ * Improve text for empty issue/pr description (#26047)
+ * Categorize admin settings sidebar panel (#26030)
+ * Remove redundant "RouteMethods" method (#26024)
+ * Refactor and enhance issue indexer to support both searching, filtering and paging (#26012)
+ * Add a link to OpenID Issuer URL in WebFinger response (#26000)
+ * Fix UI for release tag page / wiki page / subscription page (#25948)
+ * Support copy protected branch from template repository (#25889)
+ * Improve display of Labels/Projects/Assignees sort options (#25886)
+ * Fix margin on the new/edit project page. (#25885)
+ * Show image size on view page (#25884)
+ * Remove ref name in PR commits page (#25876)
+ * Allow the use of alternative net.Listener implementations by downstreams (#25855)
+ * Refactor "Content" for file uploading (#25851)
+ * Add error info if no user can fork the repo (#25820)
+ * Show edit title button on commits tab of PR, too (#25791)
+ * Introduce `flex-list` & `flex-item` elements for Gitea UI (#25790)
+ * Don't stack PR tab menu on small screens (#25789)
+ * Repository Archived text title center align (#25767)
+ * Make route middleware/handler mockable (#25766)
+ * Move issue filters to shared template (#25729)
+ * Use frontend fetch for branch dropdown component (#25719)
+ * Add open/closed field support for issue index (#25708)
+ * Some less naked returns (#25682)
+ * Fix inconsistent user profile layout across tabs (#25625)
+ * Get latest commit statuses from database instead of git data on dashboard for repositories (#25605)
+ * Adding branch-name copy to clipboard branches screen. (#25596)
+ * Update emoji set to Unicode 15 (#25595)
+ * Move some files under repo/setting (#25585)
+ * Add custom ansi colors and CSS variables for them (#25546)
+ * Add log line anchor for action logs (#25532)
+ * Use flex instead of float for sort button and search input (#25519)
+ * Update octicons and use `octicon-file-directory-symlink` (#25453)
+ * Add toasts to UI (#25449)
+ * Fine tune project board label colors and modal content background (#25419)
+ * Import additional secrets via file uri (#25408)
+ * Switch to ansi_up for ansi rendering in actions (#25401)
+ * Store and use seconds for timeline time comments (#25392)
+ * Support displaying diff stats in PR tab bar (#25387)
+ * Use fetch form action for lock/unlock/pin/unpin on sidebar (#25380)
+ * Refactor: TotalTimes return seconds (#25370)
+ * Navbar styling rework (#25343)
+ * Introduce shared template for search inputs (#25338)
+ * Only show 'Manage Account Links' when necessary (#25311)
+ * Improve 'Privacy' section in profile settings (#25309)
+ * Substitute variables in path names of template repos too (#25294)
+ * Fix tags line no margin see #25255 (#25280)
+ * Use fetch to send requests to create issues/comments (#25258)
+ * Change form actions to fetch for submit review box (#25219)
+ * Improve AJAX link and modal confirm dialog (#25210)
+ * Reduce unnecessary DB queries for Actions tasks (#25199)
+ * Disable `Create column` button while the column name is empty (#25192)
+ * Refactor indexer (#25174)
+ * Adjust style for action run list (align icons, adjust padding) (#25170)
+ * Remove duplicated functions when deleting a branch (#25128)
+ * Make confusable character warning less jarring (#25069)
+ * Highlight viewed files differently in the PR filetree (#24956)
+ * Support changing labels of Actions runner without re-registration (#24806)
+ * Fix duplicate Reviewed-by trailers (#24796)
+ * Resolve issue with sort icons on admin/users and admin/runners (#24360)
+ * Split lfs size from repository size (#22900)
+ * Sync branches into databases (#22743)
+ * Disable run user change in installation page (#22499)
+ * Add merge files files to GetCommitFileStatus (#20515)
+ * Show OpenID Connect and OAuth on signup page (#20242)
+* SECURITY
+ * Dont leak private users via extensions (#28023) (#28029)
+ * Expanded minimum RSA Keylength to 3072 (#26604)
+* TESTING
+ * Add user secrets API integration tests (#27832) (#27852)
+ * Add tests for db indexer in indexer_test.go (#27087)
+ * Speed up TestEventSourceManagerRun (#26262)
+ * Add unit test for user renaming (#26261)
+ * Add some Wiki unit tests (#26260)
+ * Improve unit test for caching (#26185)
+ * Add unit test for `HashAvatar` (#25662)
+* TRANSLATION
+ * Backport translations to v1.21 (#27899)
+ * Fix issues in translation file (#27699) (#27737)
+ * Add locale for deleted head branch (#26296)
+ * Improve multiple strings in en-US locale (#26213)
+ * Fix broken translations for package documantion (#25742)
+ * Correct translation wrong format (#25643)
+* BUILD
+ * Dockerfile small refactor (#27757) (#27826)
+ * Fix build errors on BSD (in BSDMakefile) (#27594) (#27608)
+ * Fully replace drone with actions (#27556) (#27575)
+ * Enable markdownlint `no-duplicate-header` (#27500) (#27506)
+ * Enable production source maps for index.js, fix CSS sourcemaps (#27291) (#27295)
+ * Update snap package (#27021)
+ * Bump go to 1.21 (#26608)
+ * Bump xgo to go-1.21.x and node to 20 in release-version (#26589)
+ * Add template linting via djlint (#25212)
+* DOCS
+ * Change default size of issue/pr attachments and repo file (#27946) (#28017)
+ * Remove `known issue` section in Gitea Actions Doc (#27930) (#27938)
+ * Remove outdated paragraphs when comparing Gitea Actions to GitHub Actions (#27119)
+ * Update brew installation documentation since gitea moved to brew core package (#27070)
+ * Actions are no longer experimental, so enable them by default (#27054)
+ * Add a documentation note for Windows Service (#26938)
+ * Add sparse url in cargo package guide (#26937)
+ * Update nginx recommendations (#26924)
+ * Update backup instructions to align with archive structure (#26902)
+ * Expanding documentation in queue.go (#26889)
+ * Update info regarding internet connection for build (#26776)
+ * Docs: template variables (#26547)
+ * Update index doc (#26455)
+ * Update zh-cn documentation (#26406)
+ * Fix typos and grammer problems for actions documentation (#26328)
+ * Update documentation for 1.21 actions (#26317)
+ * Doc update swagger doc for POST /orgs/{org}/teams (#26155)
+ * Doc sync authentication.md to zh-cn (#26117)
+ * Doc guide the user to create the appropriate level runner (#26091)
+ * Make organization redirect warning more clear (#26077)
+ * Update blog links (#25843)
+ * Fix default value for LocalURL (#25426)
+ * Update `from-source.zh-cn.md` & `from-source.en-us.md` - Cross Compile Using Zig (#25194)
+* MISC
+ * Replace deprecated `elliptic.Marshal` (#26800)
+ * Add elapsed time on debug for slow git commands (#25642)
+
## [1.20.5](https://github.com/go-gitea/gitea/releases/tag/v1.20.5) - 2023-10-03
* ENHANCEMENTS
@@ -455,7 +1137,6 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
* Add option to search for users is active join a team (#24093)
* Add PDF rendering via PDFObject (#24086)
* Refactor web route (#24080)
- * Make more functions use ctx instead of db.DefaultContext (#24068)
* Make HTML template functions support context (#24056)
* Refactor rename user and rename organization (#24052)
* Localize milestone related time strings (#24051)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 75d2cd22e6f..5d20bc25894 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -8,6 +8,7 @@
- [How to report issues](#how-to-report-issues)
- [Types of issues](#types-of-issues)
- [Discuss your design before the implementation](#discuss-your-design-before-the-implementation)
+ - [Issue locking](#issue-locking)
- [Building Gitea](#building-gitea)
- [Dependencies](#dependencies)
- [Backend](#backend)
@@ -47,6 +48,7 @@
- [Release Cycle](#release-cycle)
- [Maintainers](#maintainers)
- [Technical Oversight Committee (TOC)](#technical-oversight-committee-toc)
+ - [TOC election process](#toc-election-process)
- [Current TOC members](#current-toc-members)
- [Previous TOC/owners members](#previous-tocowners-members)
- [Governance Compensation](#governance-compensation)
@@ -102,6 +104,13 @@ the goals for the project and tools.
Pull requests should not be the place for architecture discussions.
+### Issue locking
+
+Commenting on closed or merged issues/PRs is strongly discouraged.
+Such comments will likely be overlooked as some maintainers may not view notifications on closed issues, thinking that the item is resolved.
+As such, commenting on closed/merged issues/PRs may be disabled prior to the scheduled auto-locking if a discussion starts or if unrelated comments are posted.
+If further discussion is needed, we encourage you to open a new issue instead and we recommend linking to the issue/PR in question for context.
+
## Building Gitea
See the [development setup instructions](https://docs.gitea.com/development/hacking-on-gitea).
@@ -110,7 +119,7 @@ See the [development setup instructions](https://docs.gitea.com/development/hack
### Backend
-Go dependencies are managed using [Go Modules](https://golang.org/cmd/go/#hdr-Module_maintenance). \
+Go dependencies are managed using [Go Modules](https://go.dev/cmd/go/#hdr-Module_maintenance). \
You can find more details in the [go mod documentation](https://go.dev/ref/mod) and the [Go Modules Wiki](https://github.com/golang/go/wiki/Modules).
Pull requests should only modify `go.mod` and `go.sum` where it is related to your change, be it a bugfix or a new feature. \
@@ -167,7 +176,7 @@ Here's how to run the test suite:
| Command | Action | |
| :------------------------------------- | :----------------------------------------------- | ------------ |
-|``make test[\#SpecificTestName]`` | run unit test(s) |
+|``make test[\#SpecificTestName]`` | run unit test(s) | |
|``make test-sqlite[\#SpecificTestName]``| run [integration](tests/integration) test(s) for SQLite |[More details](tests/integration/README.md) |
|``make test-e2e-sqlite[\#SpecificTestName]``| run [end-to-end](tests/e2e) test(s) for SQLite |[More details](tests/e2e/README.md) |
@@ -203,10 +212,20 @@ Some of the key points:
In the PR title, describe the problem you are fixing, not how you are fixing it. \
Use the first comment as a summary of your PR. \
-In the PR summary, you can describe exactly how you are fixing this problem. \
+In the PR summary, you can describe exactly how you are fixing this problem.
+
Keep this summary up-to-date as the PR evolves. \
If your PR changes the UI, you must add **after** screenshots in the PR summary. \
-If you are not implementing a new feature, you should also post **before** screenshots for comparison. \
+If you are not implementing a new feature, you should also post **before** screenshots for comparison.
+
+If you are implementing a new feature, your PR will only be merged if your screenshots are up to date.\
+Furthermore, feature PRs will only be merged if their summary contains a clear usage description (understandable for users) and testing description (understandable for reviewers).
+You should strive to combine both into a single description.
+
+Another requirement for merging PRs is that the PR is labeled correctly.\
+However, this is not your job as a contributor, but the job of the person merging your PR.\
+If you think that your PR was labeled incorrectly, or notice that it was merged without labels, please let us know.
+
If your PR closes some issues, you must note that in a way that both GitHub and Gitea understand, i.e. by appending a paragraph like
```text
@@ -255,13 +274,16 @@ Changing the default value of a setting or replacing the setting with another on
#### How to handle breaking PRs?
-If your PR has a breaking change, you must add a `BREAKING` section to your PR summary, e.g.
+If your PR has a breaking change, you must add two things to the summary of your PR:
-```
+1. A reasoning why this breaking change is necessary
+2. A `BREAKING` section explaining in simple terms (understandable for a typical user) how this PR affects users and how to mitigate these changes. This section can look for example like
+
+```md
## :warning: BREAKING :warning:
```
-To explain how this will affect users and how to mitigate these changes.
+Breaking PRs will not be merged as long as not both of these requirements are met.
### Maintaining open PRs
@@ -442,7 +464,7 @@ We assume in good faith that the information you provide is legally binding.
We adopted a release schedule to streamline the process of working on, finishing, and issuing releases. \
The overall goal is to make a major release every three or four months, which breaks down into two or three months of general development followed by one month of testing and polishing known as the release freeze. \
All the feature pull requests should be
-merged before feature freeze. And, during the frozen period, a corresponding
+merged before feature freeze. All feature pull requests haven't been merged before this feature freeze will be moved to next milestone, please notice our feature freeze announcement on discord. And, during the frozen period, a corresponding
release branch is open for fixes backported from main branch. Release candidates
are made during this period for user testing to
obtain a final version that is maintained in this branch.
@@ -473,36 +495,53 @@ if possible provide GPG signed commits.
https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/
https://help.github.com/articles/signing-commits-with-gpg/
+Furthermore, any account with write access (like bots and TOC members) **must** use 2FA.
+https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/
+
## Technical Oversight Committee (TOC)
-At the start of 2023, the `Owners` team was dissolved. Instead, the governance charter proposed a technical oversight committee (TOC) which expands the ownership team of the Gitea project from three elected positions to six positions. Three positions would be elected as it has been over the past years, and the other three would consist of appointed members from the Gitea company.
+At the start of 2023, the `Owners` team was dissolved. Instead, the governance charter proposed a technical oversight committee (TOC) which expands the ownership team of the Gitea project from three elected positions to six positions. Three positions are elected as it has been over the past years, and the other three consist of appointed members from the Gitea company.
https://blog.gitea.com/quarterly-23q1/
-When the new community members have been elected, the old members will give up ownership to the newly elected members. For security reasons, TOC members or any account with write access (like a bot) must use 2FA.
-https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/
+### TOC election process
+
+Any maintainer is eligible to be part of the community TOC if they are not associated with the Gitea company.
+A maintainer can either nominate themselves, or can be nominated by other maintainers to be a candidate for the TOC election.
+If you are nominated by someone else, you must first accept your nomination before the vote starts to be a candidate.
+
+The TOC is elected for one year, the TOC election happens yearly.
+After the announcement of the results of the TOC election, elected members have two weeks time to confirm or refuse the seat.
+If an elected member does not answer within this timeframe, they are automatically assumed to refuse the seat.
+Refusals result in the person with the next highest vote getting the same choice.
+As long as seats are empty in the TOC, members of the previous TOC can fill them until an elected member accepts the seat.
+
+If an elected member that accepts the seat does not have 2FA configured yet, they will be temporarily counted as `answer pending` until they manage to configure 2FA, thus leaving their seat empty for this duration.
### Current TOC members
-- 2023-01-01 ~ 2023-12-31 - https://blog.gitea.com/quarterly-23q1/
+- 2024-01-01 ~ 2024-12-31
- Company
- [Jason Song](https://gitea.com/wolfogre)
- [Lunny Xiao](https://gitea.com/lunny)
@@ -62,11 +59,16 @@ painless way of setting up a self-hosted Git service.
As Gitea is written in Go, it works across **all** the platforms and
architectures that are supported by Go, including Linux, macOS, and
Windows on x86, amd64, ARM and PowerPC architectures.
-You can try it out using [the online demo](https://try.gitea.io/).
This project has been
[forked](https://blog.gitea.com/welcome-to-gitea/) from
[Gogs](https://gogs.io) since November of 2016, but a lot has changed.
+For online demonstrations, you can visit [try.gitea.io](https://try.gitea.io).
+
+For accessing free Gitea service (with a limited number of repositories), you can visit [gitea.com](https://gitea.com/user/login).
+
+To quickly deploy your own dedicated Gitea instance on Gitea Cloud, you can start a free trial at [cloud.gitea.com](https://cloud.gitea.com).
+
## Building
From the root of the source tree, run:
@@ -84,25 +86,23 @@ The `build` target is split into two sub-targets:
Internet connectivity is required to download the go and npm modules. When building from the official source tarballs which include pre-built frontend files, the `frontend` target will not be triggered, making it possible to build without Node.js.
-Parallelism (`make -j
@@ -58,7 +55,11 @@
Gitea 的首要目标是创建一个极易安装,运行非常快速,安装和使用体验良好的自建 Git 服务。我们采用 Go 作为后端语言,这使我们只要生成一个可执行程序即可。并且他还支持跨平台,支持 Linux, macOS 和 Windows 以及各种架构,除了 x86,amd64,还包括 ARM 和 PowerPC。
-如果您想试用一下,请访问 [在线Demo](https://try.gitea.io/)!
+如果你想试用在线演示,请访问 [try.gitea.io](https://try.gitea.io/)。
+
+如果你想使用免费的 Gitea 服务(有仓库数量限制),请访问 [gitea.com](https://gitea.com/user/login)。
+
+如果你想在 Gitea Cloud 上快速部署你自己独享的 Gitea 实例,请访问 [cloud.gitea.com](https://cloud.gitea.com) 开始免费试用。
## 提示
@@ -94,5 +95,5 @@ Fork -> Patch -> Push -> Pull Request
|![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.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)|
+|![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/assets/go-licenses.json b/assets/go-licenses.json
index ee1e503f2a6..2aa60780c48 100644
--- a/assets/go-licenses.json
+++ b/assets/go-licenses.json
@@ -545,8 +545,8 @@
"licenseText": "Copyright (c) 2011 The Snappy-Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
},
{
- "name": "github.com/google/go-github/v53/github",
- "path": "github.com/google/go-github/v53/github/LICENSE",
+ "name": "github.com/google/go-github/v57/github",
+ "path": "github.com/google/go-github/v57/github/LICENSE",
"licenseText": "Copyright (c) 2013 The go-github AUTHORS. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
},
{
@@ -572,27 +572,27 @@
{
"name": "github.com/gorilla/css/scanner",
"path": "github.com/gorilla/css/scanner/LICENSE",
- "licenseText": "Copyright (c) 2013, Gorilla web toolkit\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n Redistributions in binary form must reproduce the above copyright notice, this\n list of conditions and the following disclaimer in the documentation and/or\n other materials provided with the distribution.\n\n Neither the name of the {organization} nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ "licenseText": "Copyright (c) 2023 The Gorilla Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n\t * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n\t * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n\t * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n"
},
{
"name": "github.com/gorilla/feeds",
"path": "github.com/gorilla/feeds/LICENSE",
- "licenseText": "Copyright (c) 2013-2018 The Gorilla Feeds Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ "licenseText": "Copyright (c) 2023 The Gorilla Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n\t * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n\t * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n\t * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n"
},
{
"name": "github.com/gorilla/mux",
"path": "github.com/gorilla/mux/LICENSE",
- "licenseText": "Copyright (c) 2012-2018 The Gorilla Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n\t * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n\t * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n\t * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ "licenseText": "Copyright (c) 2023 The Gorilla Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n\t * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n\t * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n\t * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
},
{
"name": "github.com/gorilla/securecookie",
"path": "github.com/gorilla/securecookie/LICENSE",
- "licenseText": "Copyright (c) 2012 Rodrigo Moraes. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n\t * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n\t * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n\t * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ "licenseText": "Copyright (c) 2023 The Gorilla Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n\t * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n\t * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n\t * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
},
{
"name": "github.com/gorilla/sessions",
"path": "github.com/gorilla/sessions/LICENSE",
- "licenseText": "Copyright (c) 2012-2018 The Gorilla Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n\t * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n\t * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n\t * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ "licenseText": "Copyright (c) 2023 The Gorilla Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n\t * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n\t * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n\t * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
},
{
"name": "github.com/hashicorp/go-cleanhttp",
@@ -734,15 +734,10 @@
"path": "github.com/mattn/go-runewidth/LICENSE",
"licenseText": "The MIT License (MIT)\n\nCopyright (c) 2016 Yasuhiro Matsumoto\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
},
- {
- "name": "github.com/matttproud/golang_protobuf_extensions/pbutil",
- "path": "github.com/matttproud/golang_protobuf_extensions/pbutil/LICENSE",
- "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
- },
{
"name": "github.com/meilisearch/meilisearch-go",
"path": "github.com/meilisearch/meilisearch-go/LICENSE",
- "licenseText": "MIT License\n\nCopyright (c) 2020-2022 Meili SAS\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ "licenseText": "MIT License\n\nCopyright (c) 2020-2024 Meili SAS\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
},
{
"name": "github.com/mholt/acmez",
diff --git a/build/generate-images.js b/build/generate-images.js
index a3a0f8d8f39..db31d19e2a4 100755
--- a/build/generate-images.js
+++ b/build/generate-images.js
@@ -1,20 +1,13 @@
#!/usr/bin/env node
import imageminZopfli from 'imagemin-zopfli';
import {optimize} from 'svgo';
-import {fabric} from 'fabric';
+import {loadSVGFromString, Canvas, Rect, util} from 'fabric/node';
import {readFile, writeFile} from 'node:fs/promises';
+import {argv, exit} from 'node:process';
-function exit(err) {
+function doExit(err) {
if (err) console.error(err);
- process.exit(err ? 1 : 0);
-}
-
-function loadSvg(svg) {
- return new Promise((resolve) => {
- fabric.loadSVGFromString(svg, (objects, options) => {
- resolve({objects, options});
- });
- });
+ exit(err ? 1 : 0);
}
async function generate(svg, path, {size, bg}) {
@@ -35,14 +28,14 @@ async function generate(svg, path, {size, bg}) {
return;
}
- const {objects, options} = await loadSvg(svg);
- const canvas = new fabric.Canvas();
+ const {objects, options} = await loadSVGFromString(svg);
+ const canvas = new Canvas();
canvas.setDimensions({width: size, height: size});
const ctx = canvas.getContext('2d');
ctx.scale(options.width ? (size / options.width) : 1, options.height ? (size / options.height) : 1);
if (bg) {
- canvas.add(new fabric.Rect({
+ canvas.add(new Rect({
left: 0,
top: 0,
height: size * (1 / (size / options.height)),
@@ -51,7 +44,7 @@ async function generate(svg, path, {size, bg}) {
}));
}
- canvas.add(fabric.util.groupSVGElements(objects, options));
+ canvas.add(util.groupSVGElements(objects, options));
canvas.renderAll();
let png = Buffer.from([]);
@@ -64,7 +57,7 @@ async function generate(svg, path, {size, bg}) {
}
async function main() {
- const gitea = process.argv.slice(2).includes('gitea');
+ const gitea = argv.slice(2).includes('gitea');
const logoSvg = await readFile(new URL('../assets/logo.svg', import.meta.url), 'utf8');
const faviconSvg = await readFile(new URL('../assets/favicon.svg', import.meta.url), 'utf8');
@@ -79,4 +72,8 @@ async function main() {
]);
}
-main().then(exit).catch(exit);
+try {
+ doExit(await main());
+} catch (err) {
+ doExit(err);
+}
diff --git a/build/generate-svg.js b/build/generate-svg.js
index b845da9367c..f26b60d9607 100755
--- a/build/generate-svg.js
+++ b/build/generate-svg.js
@@ -4,15 +4,16 @@ import {optimize} from 'svgo';
import {parse} from 'node:path';
import {readFile, writeFile, mkdir} from 'node:fs/promises';
import {fileURLToPath} from 'node:url';
+import {exit} from 'node:process';
const glob = (pattern) => fastGlob.sync(pattern, {
cwd: fileURLToPath(new URL('..', import.meta.url)),
absolute: true,
});
-function exit(err) {
+function doExit(err) {
if (err) console.error(err);
- process.exit(err ? 1 : 0);
+ exit(err ? 1 : 0);
}
async function processFile(file, {prefix, fullName} = {}) {
@@ -63,4 +64,8 @@ async function main() {
]);
}
-main().then(exit).catch(exit);
+try {
+ doExit(await main());
+} catch (err) {
+ doExit(err);
+}
diff --git a/cmd/actions.go b/cmd/actions.go
index 052afb9ebc3..f582c16c81c 100644
--- a/cmd/actions.go
+++ b/cmd/actions.go
@@ -15,9 +15,8 @@ import (
var (
// CmdActions represents the available actions sub-commands.
CmdActions = &cli.Command{
- Name: "actions",
- Usage: "",
- Description: "Commands for managing Gitea Actions",
+ Name: "actions",
+ Usage: "Manage Gitea Actions",
Subcommands: []*cli.Command{
subcmdActionsGenRunnerToken,
},
@@ -51,6 +50,6 @@ func runGenerateActionsRunnerToken(c *cli.Context) error {
if extra.HasError() {
return handleCliResponseExtra(extra)
}
- _, _ = fmt.Printf("%s\n", respText)
+ _, _ = fmt.Printf("%s\n", respText.Text)
return nil
}
diff --git a/cmd/admin.go b/cmd/admin.go
index 49d0e4ef74a..6c9480e76eb 100644
--- a/cmd/admin.go
+++ b/cmd/admin.go
@@ -11,6 +11,7 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
@@ -21,7 +22,7 @@ var (
// CmdAdmin represents the available admin sub-command.
CmdAdmin = &cli.Command{
Name: "admin",
- Usage: "Command line interface to perform common administrative operations",
+ Usage: "Perform common administrative operations",
Subcommands: []*cli.Command{
subcmdUser,
subcmdRepoSyncReleases,
@@ -122,7 +123,7 @@ func runRepoSyncReleases(_ *cli.Context) error {
log.Trace("Processing next %d repos of %d", len(repos), count)
for _, repo := range repos {
log.Trace("Synchronizing repo %s with path %s", repo.FullName(), repo.RepoPath())
- gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
+ gitRepo, err := gitrepo.OpenRepository(ctx, repo)
if err != nil {
log.Warn("OpenRepository: %v", err)
continue
@@ -157,10 +158,10 @@ func runRepoSyncReleases(_ *cli.Context) error {
}
func getReleaseCount(ctx context.Context, id int64) (int64, error) {
- return repo_model.GetReleaseCountByRepoID(
+ return db.Count[repo_model.Release](
ctx,
- id,
repo_model.FindReleasesOptions{
+ RepoID: id,
IncludeTags: true,
},
)
diff --git a/cmd/admin_auth.go b/cmd/admin_auth.go
index 014ddf329f9..ec92e342d4d 100644
--- a/cmd/admin_auth.go
+++ b/cmd/admin_auth.go
@@ -9,6 +9,7 @@ import (
"text/tabwriter"
auth_model "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/db"
auth_service "code.gitea.io/gitea/services/auth"
"github.com/urfave/cli/v2"
@@ -62,7 +63,7 @@ func runListAuth(c *cli.Context) error {
return err
}
- authSources, err := auth_model.FindSources(ctx, auth_model.FindSourcesOptions{})
+ authSources, err := db.Find[auth_model.Source](ctx, auth_model.FindSourcesOptions{})
if err != nil {
return err
}
diff --git a/cmd/admin_regenerate.go b/cmd/admin_regenerate.go
index 0db505ff9c7..ab769f6d0c6 100644
--- a/cmd/admin_regenerate.go
+++ b/cmd/admin_regenerate.go
@@ -4,8 +4,8 @@
package cmd
import (
- asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/modules/graceful"
+ asymkey_service "code.gitea.io/gitea/services/asymkey"
repo_service "code.gitea.io/gitea/services/repository"
"github.com/urfave/cli/v2"
@@ -42,5 +42,5 @@ func runRegenerateKeys(_ *cli.Context) error {
if err := initDB(ctx); err != nil {
return err
}
- return asymkey_model.RewriteAllPublicKeys(ctx)
+ return asymkey_service.RewriteAllPublicKeys(ctx)
}
diff --git a/cmd/admin_user_change_password.go b/cmd/admin_user_change_password.go
index eebbfb3b671..824d66d1125 100644
--- a/cmd/admin_user_change_password.go
+++ b/cmd/admin_user_change_password.go
@@ -4,13 +4,14 @@
package cmd
import (
- "context"
"errors"
"fmt"
user_model "code.gitea.io/gitea/models/user"
- pwd "code.gitea.io/gitea/modules/auth/password"
+ "code.gitea.io/gitea/modules/auth/password"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
+ user_service "code.gitea.io/gitea/services/user"
"github.com/urfave/cli/v2"
)
@@ -32,6 +33,10 @@ var microcmdUserChangePassword = &cli.Command{
Value: "",
Usage: "New password to set for user",
},
+ &cli.BoolFlag{
+ Name: "must-change-password",
+ Usage: "User must change password",
+ },
},
}
@@ -46,31 +51,32 @@ func runChangePassword(c *cli.Context) error {
if err := initDB(ctx); err != nil {
return err
}
- if len(c.String("password")) < setting.MinPasswordLength {
- return fmt.Errorf("Password is not long enough. Needs to be at least %d", setting.MinPasswordLength)
- }
- if !pwd.IsComplexEnough(c.String("password")) {
- return errors.New("Password does not meet complexity requirements")
- }
- pwned, err := pwd.IsPwned(context.Background(), c.String("password"))
+ user, err := user_model.GetUserByName(ctx, c.String("username"))
if err != nil {
return err
}
- if pwned {
- return errors.New("The password you chose is on a list of stolen passwords previously exposed in public data breaches. Please try again with a different password.\nFor more details, see https://haveibeenpwned.com/Passwords")
- }
- uname := c.String("username")
- user, err := user_model.GetUserByName(ctx, uname)
- if err != nil {
- return err
- }
- if err = user.SetPassword(c.String("password")); err != nil {
- return err
+
+ var mustChangePassword optional.Option[bool]
+ if c.IsSet("must-change-password") {
+ mustChangePassword = optional.Some(c.Bool("must-change-password"))
}
- if err = user_model.UpdateUserCols(ctx, user, "passwd", "passwd_hash_algo", "salt"); err != nil {
- return err
+ opts := &user_service.UpdateAuthOptions{
+ Password: optional.Some(c.String("password")),
+ MustChangePassword: mustChangePassword,
+ }
+ if err := user_service.UpdateAuth(ctx, user, opts); err != nil {
+ switch {
+ case errors.Is(err, password.ErrMinLength):
+ return fmt.Errorf("Password is not long enough. Needs to be at least %d", setting.MinPasswordLength)
+ case errors.Is(err, password.ErrComplexity):
+ return errors.New("Password does not meet complexity requirements")
+ case errors.Is(err, password.ErrIsPwned):
+ return errors.New("The password you chose is on a list of stolen passwords previously exposed in public data breaches. Please try again with a different password.\nFor more details, see https://haveibeenpwned.com/Passwords")
+ default:
+ return err
+ }
}
fmt.Printf("%s's password has been successfully updated!\n", user.Name)
diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go
index fefe18d39c5..a257ce21c8d 100644
--- a/cmd/admin_user_create.go
+++ b/cmd/admin_user_create.go
@@ -10,8 +10,8 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
pwd "code.gitea.io/gitea/modules/auth/password"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
"github.com/urfave/cli/v2"
)
@@ -123,10 +123,10 @@ func runCreateUser(c *cli.Context) error {
changePassword = c.Bool("must-change-password")
}
- restricted := util.OptionalBoolNone
+ restricted := optional.None[bool]()
if c.IsSet("restricted") {
- restricted = util.OptionalBoolOf(c.Bool("restricted"))
+ restricted = optional.Some(c.Bool("restricted"))
}
// default user visibility in app.ini
@@ -142,7 +142,7 @@ func runCreateUser(c *cli.Context) error {
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
- IsActive: util.OptionalBoolTrue,
+ IsActive: optional.Some(true),
IsRestricted: restricted,
}
diff --git a/cmd/doctor.go b/cmd/doctor.go
index d040a3af1c8..e433f4adc5d 100644
--- a/cmd/doctor.go
+++ b/cmd/doctor.go
@@ -14,14 +14,28 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/migrations"
migrate_base "code.gitea.io/gitea/models/migrations/base"
- "code.gitea.io/gitea/modules/doctor"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/doctor"
"github.com/urfave/cli/v2"
"xorm.io/xorm"
)
+// CmdDoctor represents the available doctor sub-command.
+var CmdDoctor = &cli.Command{
+ Name: "doctor",
+ Usage: "Diagnose and optionally fix problems, convert or re-create database tables",
+ Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.",
+
+ Subcommands: []*cli.Command{
+ cmdDoctorCheck,
+ cmdRecreateTable,
+ cmdDoctorConvert,
+ },
+}
+
var cmdDoctorCheck = &cli.Command{
Name: "check",
Usage: "Diagnose and optionally fix problems",
@@ -60,19 +74,6 @@ var cmdDoctorCheck = &cli.Command{
},
}
-// CmdDoctor represents the available doctor sub-command.
-var CmdDoctor = &cli.Command{
- Name: "doctor",
- Usage: "Diagnose and optionally fix problems",
- Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.",
-
- Subcommands: []*cli.Command{
- cmdDoctorCheck,
- cmdRecreateTable,
- cmdDoctorConvert,
- },
-}
-
var cmdRecreateTable = &cli.Command{
Name: "recreate-table",
Usage: "Recreate tables from XORM definitions and copy the data.",
@@ -177,6 +178,7 @@ func runDoctorCheck(ctx *cli.Context) error {
if ctx.IsSet("list") {
w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
_, _ = w.Write([]byte("Default\tName\tTitle\n"))
+ doctor.SortChecks(doctor.Checks)
for _, check := range doctor.Checks {
if check.IsDefault {
_, _ = w.Write([]byte{'*'})
@@ -192,26 +194,20 @@ func runDoctorCheck(ctx *cli.Context) error {
var checks []*doctor.Check
if ctx.Bool("all") {
- checks = doctor.Checks
+ checks = make([]*doctor.Check, len(doctor.Checks))
+ copy(checks, doctor.Checks)
} else if ctx.IsSet("run") {
addDefault := ctx.Bool("default")
- names := ctx.StringSlice("run")
- for i, name := range names {
- names[i] = strings.ToLower(strings.TrimSpace(name))
- }
-
+ runNamesSet := container.SetOf(ctx.StringSlice("run")...)
for _, check := range doctor.Checks {
- if addDefault && check.IsDefault {
+ if (addDefault && check.IsDefault) || runNamesSet.Contains(check.Name) {
checks = append(checks, check)
- continue
- }
- for _, name := range names {
- if name == check.Name {
- checks = append(checks, check)
- break
- }
+ runNamesSet.Remove(check.Name)
}
}
+ if len(runNamesSet) > 0 {
+ return fmt.Errorf("unknown checks: %q", strings.Join(runNamesSet.Values(), ","))
+ }
} else {
for _, check := range doctor.Checks {
if check.IsDefault {
@@ -219,6 +215,5 @@ func runDoctorCheck(ctx *cli.Context) error {
}
}
}
-
return doctor.RunChecks(stdCtx, colorize, ctx.Bool("fix"), checks)
}
diff --git a/cmd/doctor_convert.go b/cmd/doctor_convert.go
index 2385f23e521..48c835ad0e2 100644
--- a/cmd/doctor_convert.go
+++ b/cmd/doctor_convert.go
@@ -37,8 +37,8 @@ func runDoctorConvert(ctx *cli.Context) error {
switch {
case setting.Database.Type.IsMySQL():
- if err := db.ConvertUtf8ToUtf8mb4(); err != nil {
- log.Fatal("Failed to convert database from utf8 to utf8mb4: %v", err)
+ if err := db.ConvertDatabaseTable(); err != nil {
+ log.Fatal("Failed to convert database & table: %v", err)
return err
}
fmt.Println("Converted successfully, please confirm your database's character set is now utf8mb4")
diff --git a/cmd/doctor_test.go b/cmd/doctor_test.go
new file mode 100644
index 00000000000..3e1ff299c5a
--- /dev/null
+++ b/cmd/doctor_test.go
@@ -0,0 +1,33 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package cmd
+
+import (
+ "context"
+ "testing"
+
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/services/doctor"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/urfave/cli/v2"
+)
+
+func TestDoctorRun(t *testing.T) {
+ doctor.Register(&doctor.Check{
+ Title: "Test Check",
+ Name: "test-check",
+ Run: func(ctx context.Context, logger log.Logger, autofix bool) error { return nil },
+
+ SkipDatabaseInitialization: true,
+ })
+ app := cli.NewApp()
+ app.Commands = []*cli.Command{cmdDoctorCheck}
+ err := app.Run([]string{"./gitea", "check", "--run", "test-check"})
+ assert.NoError(t, err)
+ err = app.Run([]string{"./gitea", "check", "--run", "no-such"})
+ assert.ErrorContains(t, err, `unknown checks: "no-such"`)
+ err = app.Run([]string{"./gitea", "check", "--run", "test-check,no-such"})
+ assert.ErrorContains(t, err, `unknown checks: "no-such"`)
+}
diff --git a/cmd/dump.go b/cmd/dump.go
index 97f292ae096..69ecdcec124 100644
--- a/cmd/dump.go
+++ b/cmd/dump.go
@@ -452,7 +452,7 @@ func addRecursiveExclude(w archiver.Writer, insidePath, absPath string, excludeA
return err
}
for _, file := range files {
- currentAbsPath := path.Join(absPath, file.Name())
+ currentAbsPath := filepath.Join(absPath, file.Name())
currentInsidePath := path.Join(insidePath, file.Name())
if file.IsDir() {
if !util.SliceContainsString(excludeAbsPath, currentAbsPath) {
diff --git a/cmd/generate.go b/cmd/generate.go
index 5922617217a..90b32ecaf0e 100644
--- a/cmd/generate.go
+++ b/cmd/generate.go
@@ -18,7 +18,7 @@ var (
// CmdGenerate represents the available generate sub-command.
CmdGenerate = &cli.Command{
Name: "generate",
- Usage: "Command line interface for running generators",
+ Usage: "Generate Gitea's secrets/keys/tokens",
Subcommands: []*cli.Command{
subcmdSecret,
},
@@ -70,7 +70,7 @@ func runGenerateInternalToken(c *cli.Context) error {
}
func runGenerateLfsJwtSecret(c *cli.Context) error {
- _, jwtSecretBase64, err := generate.NewJwtSecretBase64()
+ _, jwtSecretBase64, err := generate.NewJwtSecretWithBase64()
if err != nil {
return err
}
diff --git a/cmd/hook.go b/cmd/hook.go
index f38fd8831b8..6a3358853dd 100644
--- a/cmd/hook.go
+++ b/cmd/hook.go
@@ -31,8 +31,8 @@ var (
// CmdHook represents the available hooks sub-command.
CmdHook = &cli.Command{
Name: "hook",
- Usage: "Delegate commands to corresponding Git hooks",
- Description: "This should only be called by Git",
+ Usage: "(internal) Should only be called by Git",
+ Description: "Delegate commands to corresponding Git hooks",
Before: PrepareConsoleLoggerLevel(log.FATAL),
Subcommands: []*cli.Command{
subcmdHookPreReceive,
@@ -376,7 +376,9 @@ Gitea or set your environment appropriately.`, "")
oldCommitIDs[count] = string(fields[0])
newCommitIDs[count] = string(fields[1])
refFullNames[count] = git.RefName(fields[2])
- if refFullNames[count] == git.BranchPrefix+"master" && newCommitIDs[count] != git.EmptySHA && count == total {
+
+ commitID, _ := git.NewIDFromString(newCommitIDs[count])
+ if refFullNames[count] == git.BranchPrefix+"master" && !commitID.IsZero() && count == total {
masterPushed = true
}
count++
@@ -669,7 +671,8 @@ Gitea or set your environment appropriately.`, "")
if err != nil {
return err
}
- if rs.OldOID != git.EmptySHA {
+ commitID, _ := git.NewIDFromString(rs.OldOID)
+ if !commitID.IsZero() {
err = writeDataPktLine(ctx, os.Stdout, []byte("option old-oid "+rs.OldOID))
if err != nil {
return err
diff --git a/cmd/keys.go b/cmd/keys.go
index b8467825293..7fdbe16119f 100644
--- a/cmd/keys.go
+++ b/cmd/keys.go
@@ -16,10 +16,11 @@ import (
// CmdKeys represents the available keys sub-command
var CmdKeys = &cli.Command{
- Name: "keys",
- Usage: "This command queries the Gitea database to get the authorized command for a given ssh key fingerprint",
- Before: PrepareConsoleLoggerLevel(log.FATAL),
- Action: runKeys,
+ Name: "keys",
+ Usage: "(internal) Should only be called by SSH server",
+ Description: "Queries the Gitea database to get the authorized command for a given ssh key fingerprint",
+ Before: PrepareConsoleLoggerLevel(log.FATAL),
+ Action: runKeys,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "expected",
@@ -70,13 +71,13 @@ func runKeys(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
- setup(ctx, false)
+ setup(ctx, c.Bool("debug"))
authorizedString, extra := private.AuthorizedPublicKeyByContent(ctx, content)
// do not use handleCliResponseExtra or cli.NewExitError, if it exists immediately, it breaks some tests like Test_CmdKeys
if extra.Error != nil {
return extra.Error
}
- _, _ = fmt.Fprintln(c.App.Writer, strings.TrimSpace(authorizedString))
+ _, _ = fmt.Fprintln(c.App.Writer, strings.TrimSpace(authorizedString.Text))
return nil
}
diff --git a/cmd/mailer.go b/cmd/mailer.go
index 646330e85aa..0c5f2c8c8d4 100644
--- a/cmd/mailer.go
+++ b/cmd/mailer.go
@@ -45,6 +45,6 @@ func runSendMail(c *cli.Context) error {
if extra.HasError() {
return handleCliResponseExtra(extra)
}
- _, _ = fmt.Printf("Sent %s email(s) to all users\n", respText)
+ _, _ = fmt.Printf("Sent %s email(s) to all users\n", respText.Text)
return nil
}
diff --git a/cmd/main.go b/cmd/main.go
index feda41e68b2..02dd660e9ee 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -10,12 +10,12 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
"github.com/urfave/cli/v2"
)
// cmdHelp is our own help subcommand with more information
+// Keep in mind that the "./gitea help"(subcommand) is different from "./gitea --help"(flag), the flag doesn't parse the config or output "DEFAULT CONFIGURATION:" information
func cmdHelp() *cli.Command {
c := &cli.Command{
Name: "help",
@@ -47,16 +47,10 @@ DEFAULT CONFIGURATION:
return c
}
-var helpFlag = cli.HelpFlag
-
-func init() {
- // cli.HelpFlag = nil TODO: after https://github.com/urfave/cli/issues/1794 we can use this
-}
-
func appGlobalFlags() []cli.Flag {
return []cli.Flag{
// make the builtin flags at the top
- helpFlag,
+ cli.HelpFlag,
// shared configuration flags, they are for global and for each sub-command at the same time
// eg: such command is valid: "./gitea --config /tmp/app.ini web --config /tmp/app.ini", while it's discouraged indeed
@@ -121,20 +115,22 @@ func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(ctx *cli.Context)
func NewMainApp(version, versionExtra string) *cli.App {
app := cli.NewApp()
app.Name = "Gitea"
+ app.HelpName = "gitea"
app.Usage = "A painless self-hosted Git service"
- 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.Description = `Gitea program contains "web" and other subcommands. If no subcommand is given, it starts the web server by default. Use "web" subcommand for more web server arguments, use other subcommands for other purposes.`
app.Version = version + versionExtra
app.EnableBashCompletion = true
// these sub-commands need to use config file
subCmdWithConfig := []*cli.Command{
+ cmdHelp(), // the "help" sub-command was used to show the more information for "work path" and "custom config"
CmdWeb,
CmdServ,
CmdHook,
+ CmdKeys,
CmdDump,
CmdAdmin,
CmdMigrate,
- CmdKeys,
CmdDoctor,
CmdManager,
CmdEmbedded,
@@ -142,13 +138,8 @@ func NewMainApp(version, versionExtra string) *cli.App {
CmdDumpRepository,
CmdRestoreRepository,
CmdActions,
- cmdHelp(), // the "help" sub-command was used to show the more information for "work path" and "custom config"
}
- cmdConvert := util.ToPointer(*cmdDoctorConvert)
- cmdConvert.Hidden = true // still support the legacy "./gitea doctor" by the hidden sub-command, remove it in next release
- subCmdWithConfig = append(subCmdWithConfig, cmdConvert)
-
// these sub-commands do not need the config file, and they do not depend on any path or environment variable.
subCmdStandalone := []*cli.Command{
CmdCert,
diff --git a/cmd/migrate_storage.go b/cmd/migrate_storage.go
index acc3ba16bab..aa49445a89e 100644
--- a/cmd/migrate_storage.go
+++ b/cmd/migrate_storage.go
@@ -110,6 +110,9 @@ func migrateLFS(ctx context.Context, dstStorage storage.ObjectStorage) error {
func migrateAvatars(ctx context.Context, dstStorage storage.ObjectStorage) error {
return db.Iterate(ctx, nil, func(ctx context.Context, user *user_model.User) error {
+ if user.CustomAvatarRelativePath() == "" {
+ return nil
+ }
_, err := storage.Copy(dstStorage, user.CustomAvatarRelativePath(), storage.Avatars, user.CustomAvatarRelativePath())
return err
})
@@ -117,6 +120,9 @@ func migrateAvatars(ctx context.Context, dstStorage storage.ObjectStorage) error
func migrateRepoAvatars(ctx context.Context, dstStorage storage.ObjectStorage) error {
return db.Iterate(ctx, nil, func(ctx context.Context, repo *repo_model.Repository) error {
+ if repo.CustomAvatarRelativePath() == "" {
+ return nil
+ }
_, err := storage.Copy(dstStorage, repo.CustomAvatarRelativePath(), storage.RepoAvatars, repo.CustomAvatarRelativePath())
return err
})
diff --git a/cmd/serv.go b/cmd/serv.go
index 26fc91a3b7a..90190a19db6 100644
--- a/cmd/serv.go
+++ b/cmd/serv.go
@@ -42,7 +42,7 @@ const (
// CmdServ represents the available serv sub-command.
var CmdServ = &cli.Command{
Name: "serv",
- Usage: "This command should only be called by SSH shell",
+ Usage: "(internal) Should only be called by SSH shell",
Description: "Serv provides access auth for repositories",
Before: PrepareConsoleLoggerLevel(log.FATAL),
Action: runServ,
@@ -63,21 +63,10 @@ func setup(ctx context.Context, debug bool) {
setupConsoleLogger(log.FATAL, false, os.Stderr)
}
setting.MustInstalled()
- if debug {
- setting.RunMode = "dev"
- }
-
- // Check if setting.RepoRootPath exists. It could be the case that it doesn't exist, this can happen when
- // `[repository]` `ROOT` is a relative path and $GITEA_WORK_DIR isn't passed to the SSH connection.
if _, err := os.Stat(setting.RepoRootPath); err != nil {
- if os.IsNotExist(err) {
- _ = fail(ctx, "Incorrect configuration, no repository directory.", "Directory `[repository].ROOT` %q was not found, please check if $GITEA_WORK_DIR is passed to the SSH connection or make `[repository].ROOT` an absolute value.", setting.RepoRootPath)
- } else {
- _ = fail(ctx, "Incorrect configuration, repository directory is inaccessible", "Directory `[repository].ROOT` %q is inaccessible. err: %v", setting.RepoRootPath, err)
- }
+ _ = fail(ctx, "Unable to access repository path", "Unable to access repository path %q, err: %v", setting.RepoRootPath, err)
return
}
-
if err := git.InitSimple(context.Background()); err != nil {
_ = fail(ctx, "Failed to init git", "Failed to init git, err: %v", err)
}
@@ -216,16 +205,18 @@ func runServ(c *cli.Context) error {
}
}
- // LowerCase and trim the repoPath as that's how they are stored.
- repoPath = strings.ToLower(strings.TrimSpace(repoPath))
-
rr := strings.SplitN(repoPath, "/", 2)
if len(rr) != 2 {
return fail(ctx, "Invalid repository path", "Invalid repository path: %v", repoPath)
}
- username := strings.ToLower(rr[0])
- reponame := strings.ToLower(strings.TrimSuffix(rr[1], ".git"))
+ username := rr[0]
+ reponame := strings.TrimSuffix(rr[1], ".git")
+
+ // LowerCase and trim the repoPath as that's how they are stored.
+ // This should be done after splitting the repoPath into username and reponame
+ // so that username and reponame are not affected.
+ repoPath = strings.ToLower(strings.TrimSpace(repoPath))
if alphaDashDotPattern.MatchString(reponame) {
return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame)
diff --git a/contrib/backport/backport.go b/contrib/backport/backport.go
index 5cd0fe0f6e6..820c0702b74 100644
--- a/contrib/backport/backport.go
+++ b/contrib/backport/backport.go
@@ -17,7 +17,7 @@ import (
"strings"
"syscall"
- "github.com/google/go-github/v53/github"
+ "github.com/google/go-github/v57/github"
"github.com/urfave/cli/v2"
"gopkg.in/yaml.v3"
)
diff --git a/contrib/systemd/gitea.service b/contrib/systemd/gitea.service
index c097fb0d176..c091722a743 100644
--- a/contrib/systemd/gitea.service
+++ b/contrib/systemd/gitea.service
@@ -1,6 +1,5 @@
[Unit]
Description=Gitea (Git with a cup of tea)
-After=syslog.target
After=network.target
###
# Don't forget to add the database service dependencies
@@ -52,7 +51,7 @@ After=network.target
# Uncomment the next line if you have repos with lots of files and get a HTTP 500 error because of that
# LimitNOFILE=524288:524288
RestartSec=2s
-Type=notify
+Type=simple
User=git
Group=git
WorkingDirectory=/var/lib/gitea/
@@ -62,7 +61,6 @@ WorkingDirectory=/var/lib/gitea/
ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini
Restart=always
Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea
-WatchdogSec=30s
# If you install Git to directory prefix other than default PATH (which happens
# for example if you install other versions of Git side-to-side with
# distribution version), uncomment below line and add that prefix to PATH
diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 325e31af390..b4b4f3a8a2b 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -234,7 +234,7 @@ RUN_USER = ; git
;MINIMUM_KEY_SIZE_CHECK = false
;;
;; Disable CDN even in "prod" mode
-;OFFLINE_MODE = false
+;OFFLINE_MODE = true
;;
;; TLS Settings: Either ACME or manual
;; (Other common TLS configuration are found before)
@@ -351,6 +351,7 @@ 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_COLLATION = ; Empty as default, Gitea will try to find a case-sensitive collation. Don't change it unless you clearly know what you need.
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
@@ -382,6 +383,7 @@ USER = root
;NAME = gitea
;USER = SA
;PASSWD = MwantsaSecurePassword1
+;CHARSET_COLLATION = ; Empty as default, Gitea will try to find a case-sensitive collation. Don't change it unless you clearly know what you need.
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
@@ -410,6 +412,10 @@ USER = root
;;
;; Whether execute database models migrations automatically
;AUTO_MIGRATION = true
+;;
+;; Threshold value (in seconds) beyond which query execution time is logged as a warning in the xorm logger
+;;
+;SLOW_QUERY_THRESHOLD = 5s
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -429,7 +435,7 @@ SECRET_KEY =
;SECRET_KEY_URI = file:/etc/gitea/secret_key
;;
;; Secret used to validate communication within Gitea binary.
-INTERNAL_TOKEN=
+INTERNAL_TOKEN =
;;
;; Alternative location to specify internal token, instead of this file; you cannot specify both this and INTERNAL_TOKEN, and must pick one
;INTERNAL_TOKEN_URI = file:/etc/gitea/internal_token
@@ -492,6 +498,11 @@ INTERNAL_TOKEN=
;; Cache successful token hashes. API tokens are stored in the DB as pbkdf2 hashes however, this means that there is a potentially significant hashing load when there are multiple API operations.
;; This cache will store the successfully hashed tokens in a LRU cache as a balance between performance and security.
;SUCCESSFUL_TOKENS_CACHE_SIZE = 20
+;;
+;; Reject API tokens sent in URL query string (Accept Header-based API tokens only). This avoids security vulnerabilities
+;; stemming from cached/logged plain-text API tokens.
+;; In future releases, this will become the default behavior
+;DISABLE_QUERY_AUTH_TOKEN = false
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -517,7 +528,7 @@ INTERNAL_TOKEN=
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Enables OAuth2 provider
-ENABLE = true
+ENABLED = true
;;
;; Algorithm used to sign OAuth2 tokens. Valid values: HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512, EdDSA
;JWT_SIGNING_ALGORITHM = RS256
@@ -945,6 +956,12 @@ LEVEL = Info
;GO_GET_CLONE_URL_PROTOCOL = https
;;
;; Close issues as long as a commit on any branch marks it as fixed
+;DEFAULT_CLOSE_ISSUES_VIA_COMMITS_IN_ANY_BRANCH = false
+;;
+;; Allow users to push local repositories to Gitea and have them automatically created for a user or an org
+;ENABLE_PUSH_CREATE_USER = false
+;ENABLE_PUSH_CREATE_ORG = false
+;;
;; Comma separated list of globally disabled repo units. Allowed values: repo.issues, repo.ext_issues, repo.pulls, repo.wiki, repo.ext_wiki, repo.projects, repo.packages, repo.actions.
;DISABLED_REPO_UNITS =
;;
@@ -1017,7 +1034,7 @@ LEVEL = Info
;ALLOWED_TYPES =
;;
;; Max size of each file in megabytes. Defaults to 50MB
-;FILE_MAX_SIZE = 50
+;FILE_MAX_SIZE = 50
;;
;; Max number of files per upload. Defaults to 5
;MAX_FILES = 5
@@ -1037,7 +1054,7 @@ LEVEL = Info
;; List of keywords used in Pull Request comments to automatically reopen a related issue
;REOPEN_KEYWORDS = reopen,reopens,reopened
;;
-;; Set default merge style for repository creating, valid options: merge, rebase, rebase-merge, squash
+;; Set default merge style for repository creating, valid options: merge, rebase, rebase-merge, squash, fast-forward-only
;DEFAULT_MERGE_STYLE = merge
;;
;; In the default merge message for squash commits include at most this many commits
@@ -1060,6 +1077,9 @@ LEVEL = Info
;;
;; In addition to testing patches using the three-way merge method, re-test conflicting patches with git apply
;TEST_CONFLICTING_PATCHES_WITH_GIT_APPLY = false
+;;
+;; Retarget child pull requests to the parent pull request branch target on merge of parent pull request. It only works on merged PRs where the head and base branch target the same repo.
+;RETARGET_CHILDREN_ON_MERGE = true
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -1153,15 +1173,9 @@ LEVEL = Info
;; enable cors headers (disabled by default)
;ENABLED = false
;;
-;; scheme of allowed requests
-;SCHEME = http
-;;
-;; list of requesting domains that are allowed
+;; list of requesting origins that are allowed, eg: "https://*.example.com"
;ALLOW_DOMAIN = *
;;
-;; allow subdomains of headers listed above to request
-;ALLOW_SUBDOMAIN = false
-;;
;; list of methods allowed to request
;METHODS = GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS
;;
@@ -1207,6 +1221,9 @@ LEVEL = Info
;; Max size of files to be displayed (default is 8MiB)
;MAX_DISPLAY_FILE_SIZE = 8388608
;;
+;; Detect ambiguous unicode characters in file contents and show warnings on the UI
+;AMBIGUOUS_UNICODE_DETECTION = true
+;;
;; Whether the email of the user should be shown in the Explore Users page
;SHOW_USER_EMAIL = true
;;
@@ -1242,6 +1259,10 @@ LEVEL = Info
;; Change the sort type of the explore pages.
;; Default is "recentupdate", but you also have "alphabetically", "reverselastlogin", "newest", "oldest".
;EXPLORE_PAGING_DEFAULT_SORT = recentupdate
+;;
+;; The tense all timestamps should be rendered in. Possible values are `absolute` time (i.e. 1970-01-01, 11:59) and `mixed`.
+;; `mixed` means most timestamps are rendered in relative time (i.e. 2 days ago).
+;PREFERRED_TIMESTAMP_TENSE = mixed
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -1459,6 +1480,11 @@ LEVEL = Info
;;
;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
;DEFAULT_EMAIL_NOTIFICATIONS = enabled
+;; Disabled features for users, could be "deletion", "manage_ssh_keys","manage_gpg_keys" more features can be disabled in future
+;; - deletion: a user cannot delete their own account
+;; - manage_ssh_keys: a user cannot configure ssh keys
+;; - manage_gpg_keys: a user cannot configure gpg keys
+;USER_DISABLED_FEATURES =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -1523,6 +1549,10 @@ LEVEL = Info
;; userid = use the userid / sub attribute
;; nickname = use the nickname attribute
;; email = use the username part of the email attribute
+;; Note: `nickname` and `email` options will normalize input strings using the following criteria:
+;; - diacritics are removed
+;; - the characters in the set `['´\x60]` are removed
+;; - the characters in the set `[\s~+]` are replaced with `-`
;USERNAME = nickname
;;
;; Update avatar if available from oauth2 provider.
@@ -1697,9 +1727,6 @@ LEVEL = Info
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
-;; if the cache enabled
-;ENABLED = true
-;;
;; Either "memory", "redis", "memcache", or "twoqueue". default is "memory"
;ADAPTER = memory
;;
@@ -1724,8 +1751,6 @@ LEVEL = Info
;[cache.last_commit]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; if the cache enabled
-;ENABLED = true
;;
;; Time to keep items in cache if not used, default is 8760 hours.
;; Setting it to -1 disables caching
@@ -2575,7 +2600,7 @@ LEVEL = Info
;;
;; Default platform to get action plugins, `github` for `https://github.com`, `self` for the current Gitea instance.
;DEFAULT_ACTIONS_URL = github
-;; Default artifact retention time in days, default is 90 days
+;; Default artifact retention time in days. Artifacts could have their own retention periods by setting the `retention-days` option in `actions/upload-artifact` step.
;ARTIFACT_RETENTION_DAYS = 90
;; Timeout to stop the task which have running status, but haven't been updated for a long time
;ZOMBIE_TASK_TIMEOUT = 10m
@@ -2583,6 +2608,8 @@ LEVEL = Info
;ENDLESS_TASK_TIMEOUT = 3h
;; Timeout to cancel the jobs which have waiting status, but haven't been picked by a runner for a long time
;ABANDONED_JOB_TIMEOUT = 24h
+;; Strings committers can place inside a commit message or PR title to skip executing the corresponding actions workflow
+;SKIP_WORKFLOW_STRINGS = [skip ci],[ci skip],[no ci],[skip actions],[actions skip]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
diff --git a/docs/content/administration/adding-legal-pages.en-us.md b/docs/content/administration/adding-legal-pages.en-us.md
index c6f68edcd0e..1ff0c0132d2 100644
--- a/docs/content/administration/adding-legal-pages.en-us.md
+++ b/docs/content/administration/adding-legal-pages.en-us.md
@@ -19,10 +19,10 @@ Some jurisdictions (such as EU), requires certain legal pages (e.g. Privacy Poli
## Getting Pages
-Gitea source code ships with sample pages, available in `contrib/legal` directory. Copy them to `custom/public/`. For example, to add Privacy Policy:
+Gitea source code ships with sample pages, available in `contrib/legal` directory. Copy them to `custom/public/assets/`. For example, to add Privacy Policy:
```
-wget -O /path/to/custom/public/privacy.html https://raw.githubusercontent.com/go-gitea/gitea/main/contrib/legal/privacy.html.sample
+wget -O /path/to/custom/public/assets/privacy.html https://raw.githubusercontent.com/go-gitea/gitea/main/contrib/legal/privacy.html.sample
```
Now you need to edit the page to meet your requirements. In particular you must change the email addresses, web addresses and references to "Your Gitea Instance" to match your situation.
diff --git a/docs/content/administration/adding-legal-pages.zh-cn.md b/docs/content/administration/adding-legal-pages.zh-cn.md
index 5d582e871de..3e18c6e6b03 100644
--- a/docs/content/administration/adding-legal-pages.zh-cn.md
+++ b/docs/content/administration/adding-legal-pages.zh-cn.md
@@ -19,10 +19,10 @@ menu:
## 获取页面
-Gitea 源代码附带了示例页面,位于 `contrib/legal` 目录中。将它们复制到 `custom/public/` 目录下。例如,如果要添加隐私政策:
+Gitea 源代码附带了示例页面,位于 `contrib/legal` 目录中。将它们复制到 `custom/public/assets/` 目录下。例如,如果要添加隐私政策:
```
-wget -O /path/to/custom/public/privacy.html https://raw.githubusercontent.com/go-gitea/gitea/main/contrib/legal/privacy.html.sample
+wget -O /path/to/custom/public/assets/privacy.html https://raw.githubusercontent.com/go-gitea/gitea/main/contrib/legal/privacy.html.sample
```
现在,你需要编辑该页面以满足你的需求。特别是,你必须更改电子邮件地址、网址以及与 "Your Gitea Instance" 相关的引用,以匹配你的情况。
diff --git a/docs/content/administration/backup-and-restore.en-us.md b/docs/content/administration/backup-and-restore.en-us.md
index d46efecf990..451ef5c944c 100644
--- a/docs/content/administration/backup-and-restore.en-us.md
+++ b/docs/content/administration/backup-and-restore.en-us.md
@@ -92,7 +92,7 @@ cd gitea-dump-1610949662
mv app.ini /etc/gitea/conf/app.ini
mv data/* /var/lib/gitea/data/
mv log/* /var/lib/gitea/log/
-mv repos/* /var/lib/gitea/gitea-repositories/
+mv repos/* /var/lib/gitea/data/gitea-repositories/
chown -R gitea:gitea /etc/gitea/conf/app.ini /var/lib/gitea
# mysql
@@ -111,6 +111,8 @@ With Gitea running, and from the directory Gitea's binary is located, execute: `
This ensures that application and configuration file paths in repository Git Hooks are consistent and applicable to the current installation. If these paths are not updated, repository `push` actions will fail.
+If you still have issues, consider running `./gitea doctor check` to inspect possible errors (or run with `--fix`).
+
### Using Docker (`restore`)
There is also no support for a recovery command in a Docker-based gitea instance. The restore process contains the same steps as described in the previous section but with different paths.
diff --git a/docs/content/administration/backup-and-restore.zh-cn.md b/docs/content/administration/backup-and-restore.zh-cn.md
index 98d378d5dcb..db7eba84f7e 100644
--- a/docs/content/administration/backup-and-restore.zh-cn.md
+++ b/docs/content/administration/backup-and-restore.zh-cn.md
@@ -19,6 +19,12 @@ menu:
Gitea 已经实现了 `dump` 命令可以用来备份所有需要的文件到一个zip压缩文件。该压缩文件可以被用来进行数据恢复。
+## 备份一致性
+
+为了确保 Gitea 实例的一致性,在备份期间必须关闭它。
+
+Gitea 包括数据库、文件和 Git 仓库,当它被使用时所有这些都会发生变化。例如,当迁移正在进行时,在数据库中创建一个事务,而 Git 仓库正在被复制。如果备份发生在迁移的中间,Git 仓库可能是不完整的,尽管数据库声称它是完整的,因为它是在之后被转储的。避免这种竞争条件的唯一方法是在备份期间停止 Gitea 实例。
+
## 备份命令 (`dump`)
先转到git用户的权限: `su git`. 再Gitea目录运行 `./gitea dump`。一般会显示类似如下的输出:
@@ -34,15 +40,43 @@ Gitea 已经实现了 `dump` 命令可以用来备份所有需要的文件到一
最后生成的 `gitea-dump-1482906742.zip` 文件将会包含如下内容:
-* `custom` - 所有保存在 `custom/` 目录下的配置和自定义的文件。
-* `data` - 数据目录下的所有内容不包含使用文件session的文件。该目录包含 `attachments`, `avatars`, `lfs`, `indexers`, 如果使用sqlite 还会包含 sqlite 数据库文件。
+* `app.ini` - 如果原先存储在默认的 custom/ 目录之外,则是配置文件的可选副本
+* `custom/` - 所有保存在 `custom/` 目录下的配置和自定义的文件。
+* `data/` - 数据目录(APP_DATA_PATH),如果使用文件会话,则不包括会话。该目录包括 `attachments`、`avatars`、`lfs`、`indexers`、如果使用 SQLite 则包括 SQLite 文件。
+* `repos/` - 仓库目录的完整副本。
* `gitea-db.sql` - 数据库dump出来的 SQL。
-* `gitea-repo.zip` - Git仓库压缩文件。
* `log/` - Logs文件,如果用作迁移不是必须的。
中间备份文件将会在临时目录进行创建,如果您要重新指定临时目录,可以用 `--tempdir` 参数,或者用 `TMPDIR` 环境变量。
-## Restore Command (`restore`)
+## 备份数据库
+
+`gitea dump` 创建的 SQL 转储使用 XORM,Gitea 管理员可能更喜欢使用本地的 MySQL 和 PostgreSQL 转储工具。使用 XORM 转储数据库时仍然存在一些问题,可能会导致在尝试恢复时出现问题。
+
+```sh
+# mysql
+mysqldump -u$USER -p$PASS --database $DATABASE > gitea-db.sql
+# postgres
+pg_dump -U $USER $DATABASE > gitea-db.sql
+```
+
+### 使用Docker (`dump`)
+
+在使用 Docker 时,使用 `dump` 命令有一些注意事项。
+
+必须以 `gitea/conf/app.ini` 中指定的 `RUN_USER = Wiki! Enjoy :) Guardfile-DSL / Configuring-Guard Guardfile-DSL / Configuring-Guard Wine Staging on website wine-staging.com. Here are some links to the most important topics. You can find the full list of pages at the sidebar.
-
`,
// Guard wiki sidebar: special syntax
`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
// rendered
- `Quick Links
")
diff --git a/routers/utils/utils_test.go b/routers/utils/utils_test.go
index 6d19214c88e..440aad87c6b 100644
--- a/routers/utils/utils_test.go
+++ b/routers/utils/utils_test.go
@@ -11,12 +11,6 @@ import (
"github.com/stretchr/testify/assert"
)
-func TestRemoveUsernameParameterSuffix(t *testing.T) {
- assert.Equal(t, "foobar", RemoveUsernameParameterSuffix("foobar (Foo Bar)"))
- assert.Equal(t, "foobar", RemoveUsernameParameterSuffix("foobar"))
- assert.Equal(t, "", RemoveUsernameParameterSuffix(""))
-}
-
func TestIsExternalURL(t *testing.T) {
setting.AppURL = "https://try.gitea.io/"
type test struct {
diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go
index 1838fe190ca..f3f10fd1b82 100644
--- a/routers/web/admin/admin.go
+++ b/routers/web/admin/admin.go
@@ -12,26 +12,30 @@ import (
"time"
activities_model "code.gitea.io/gitea/models/activities"
+ "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/updatechecker"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/cron"
"code.gitea.io/gitea/services/forms"
+ release_service "code.gitea.io/gitea/services/release"
repo_service "code.gitea.io/gitea/services/repository"
)
const (
- tplDashboard base.TplName = "admin/dashboard"
- tplCron base.TplName = "admin/cron"
- tplQueue base.TplName = "admin/queue"
- tplStacktrace base.TplName = "admin/stacktrace"
- tplQueueManage base.TplName = "admin/queue_manage"
- tplStats base.TplName = "admin/stats"
+ tplDashboard base.TplName = "admin/dashboard"
+ tplSystemStatus base.TplName = "admin/system_status"
+ tplSelfCheck base.TplName = "admin/self_check"
+ tplCron base.TplName = "admin/cron"
+ tplQueue base.TplName = "admin/queue"
+ tplStacktrace base.TplName = "admin/stacktrace"
+ tplQueueManage base.TplName = "admin/queue_manage"
+ tplStats base.TplName = "admin/stats"
)
var sysStatus struct {
@@ -69,7 +73,7 @@ var sysStatus struct {
// Garbage collector statistics.
NextGC string // next run in HeapAlloc time (bytes)
- LastGC string // last run in absolute time (ns)
+ LastGCTime string // last run time
PauseTotalNs string
PauseNs string // circular buffer of recent GC pause times, most recent at [(NumGC+255)%256]
NumGC uint32
@@ -107,7 +111,7 @@ func updateSystemStatus() {
sysStatus.OtherSys = base.FileSize(int64(m.OtherSys))
sysStatus.NextGC = base.FileSize(int64(m.NextGC))
- sysStatus.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000)
+ sysStatus.LastGCTime = time.Unix(0, int64(m.LastGC)).Format(time.RFC3339)
sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000)
sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000)
sysStatus.NumGC = m.NumGC
@@ -129,7 +133,6 @@ func Dashboard(ctx *context.Context) {
ctx.Data["PageIsAdminDashboard"] = true
ctx.Data["NeedUpdate"] = updatechecker.GetNeedUpdate(ctx)
ctx.Data["RemoteVersion"] = updatechecker.GetRemoteVersion(ctx)
- // FIXME: update periodically
updateSystemStatus()
ctx.Data["SysStatus"] = sysStatus
ctx.Data["SSH"] = setting.SSH
@@ -137,6 +140,12 @@ func Dashboard(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplDashboard)
}
+func SystemStatus(ctx *context.Context) {
+ updateSystemStatus()
+ ctx.Data["SysStatus"] = sysStatus
+ ctx.HTML(http.StatusOK, tplSystemStatus)
+}
+
// DashboardPost run an admin operation
func DashboardPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.AdminDashboardForm)
@@ -155,6 +164,13 @@ func DashboardPost(ctx *context.Context) {
}
}()
ctx.Flash.Success(ctx.Tr("admin.dashboard.sync_branch.started"))
+ case "sync_repo_tags":
+ go func() {
+ if err := release_service.AddAllRepoTagsToSyncQueue(graceful.GetManager().ShutdownContext()); err != nil {
+ log.Error("AddAllRepoTagsToSyncQueue: %v: %v", ctx.Doer.ID, err)
+ }
+ }()
+ ctx.Flash.Success(ctx.Tr("admin.dashboard.sync_tag.started"))
default:
task := cron.GetTask(form.Op)
if task != nil {
@@ -172,6 +188,33 @@ func DashboardPost(ctx *context.Context) {
}
}
+func SelfCheck(ctx *context.Context) {
+ ctx.Data["PageIsAdminSelfCheck"] = true
+ r, err := db.CheckCollationsDefaultEngine()
+ if err != nil {
+ ctx.Flash.Error(fmt.Sprintf("CheckCollationsDefaultEngine: %v", err), true)
+ }
+
+ if r != nil {
+ ctx.Data["DatabaseType"] = setting.Database.Type
+ ctx.Data["DatabaseCheckResult"] = r
+ hasProblem := false
+ if !r.CollationEquals(r.DatabaseCollation, r.ExpectedCollation) {
+ ctx.Data["DatabaseCheckCollationMismatch"] = true
+ hasProblem = true
+ }
+ if !r.IsCollationCaseSensitive(r.DatabaseCollation) {
+ ctx.Data["DatabaseCheckCollationCaseInsensitive"] = true
+ hasProblem = true
+ }
+ ctx.Data["DatabaseCheckInconsistentCollationColumns"] = r.InconsistentCollationColumns
+ hasProblem = hasProblem || len(r.InconsistentCollationColumns) > 0
+
+ ctx.Data["DatabaseCheckHasProblems"] = hasProblem
+ }
+ ctx.HTML(http.StatusOK, tplSelfCheck)
+}
+
func CronTasks(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.monitor.cron")
ctx.Data["PageIsAdminMonitorCron"] = true
diff --git a/routers/web/admin/applications.go b/routers/web/admin/applications.go
index b26912db482..85833980746 100644
--- a/routers/web/admin/applications.go
+++ b/routers/web/admin/applications.go
@@ -8,10 +8,11 @@ import (
"net/http"
"code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
user_setting "code.gitea.io/gitea/routers/web/user/setting"
+ "code.gitea.io/gitea/services/context"
)
var (
@@ -33,7 +34,9 @@ func Applications(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings.applications")
ctx.Data["PageIsAdminApplications"] = true
- apps, err := auth.GetOAuth2ApplicationsByUserID(ctx, 0)
+ apps, err := db.Find[auth.OAuth2Application](ctx, auth.FindOAuth2ApplicationsOptions{
+ IsGlobal: true,
+ })
if err != nil {
ctx.ServerError("GetOAuth2ApplicationsByUserID", err)
return
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go
index 23946d64afa..ba487d10457 100644
--- a/routers/web/admin/auths.go
+++ b/routers/web/admin/auths.go
@@ -13,9 +13,9 @@ import (
"strings"
"code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/auth/pam"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
@@ -26,6 +26,7 @@ import (
pam_service "code.gitea.io/gitea/services/auth/source/pam"
"code.gitea.io/gitea/services/auth/source/smtp"
"code.gitea.io/gitea/services/auth/source/sspi"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"xorm.io/xorm/convert"
@@ -48,13 +49,12 @@ func Authentications(ctx *context.Context) {
ctx.Data["PageIsAdminAuthentications"] = true
var err error
- ctx.Data["Sources"], err = auth.FindSources(ctx, auth.FindSourcesOptions{})
+ ctx.Data["Sources"], ctx.Data["Total"], err = db.FindAndCount[auth.Source](ctx, auth.FindSourcesOptions{})
if err != nil {
ctx.ServerError("auth.Sources", err)
return
}
- ctx.Data["Total"] = auth.CountSources(ctx, auth.FindSourcesOptions{})
ctx.HTML(http.StatusOK, tplAuths)
}
@@ -210,16 +210,16 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*sspi.Source, error) {
if util.IsEmptyString(form.SSPISeparatorReplacement) {
ctx.Data["Err_SSPISeparatorReplacement"] = true
- return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.require_error"))
+ return nil, errors.New(ctx.Locale.TrString("form.SSPISeparatorReplacement") + ctx.Locale.TrString("form.require_error"))
}
if separatorAntiPattern.MatchString(form.SSPISeparatorReplacement) {
ctx.Data["Err_SSPISeparatorReplacement"] = true
- return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.alpha_dash_dot_error"))
+ return nil, errors.New(ctx.Locale.TrString("form.SSPISeparatorReplacement") + ctx.Locale.TrString("form.alpha_dash_dot_error"))
}
if form.SSPIDefaultLanguage != "" && !langCodePattern.MatchString(form.SSPIDefaultLanguage) {
ctx.Data["Err_SSPIDefaultLanguage"] = true
- return nil, errors.New(ctx.Tr("form.lang_select_error"))
+ return nil, errors.New(ctx.Locale.TrString("form.lang_select_error"))
}
return &sspi.Source{
@@ -284,7 +284,7 @@ func NewAuthSourcePost(ctx *context.Context) {
ctx.RenderWithErr(err.Error(), tplAuthNew, form)
return
}
- existing, err := auth.FindSources(ctx, auth.FindSourcesOptions{LoginType: auth.SSPI})
+ existing, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{LoginType: auth.SSPI})
if err != nil || len(existing) > 0 {
ctx.Data["Err_Type"] = true
ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_of_type_exist"), tplAuthNew, form)
diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go
index c827f2a4f50..2f5f17e2013 100644
--- a/routers/web/admin/config.go
+++ b/routers/web/admin/config.go
@@ -7,24 +7,27 @@ package admin
import (
"net/http"
"net/url"
+ "strconv"
"strings"
system_model "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/setting/config"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/mailer"
"gitea.com/go-chi/session"
)
-const tplConfig base.TplName = "admin/config"
+const (
+ tplConfig base.TplName = "admin/config"
+ tplConfigSettings base.TplName = "admin/config_settings"
+)
// SendTestMail send test mail to confirm mail service is OK
func SendTestMail(ctx *context.Context) {
@@ -98,8 +101,9 @@ func shadowPassword(provider, cfgItem string) string {
// Config show admin config page
func Config(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("admin.config")
+ ctx.Data["Title"] = ctx.Tr("admin.config_summary")
ctx.Data["PageIsAdminConfig"] = true
+ ctx.Data["PageIsAdminConfigSummary"] = true
ctx.Data["CustomConf"] = setting.CustomConf
ctx.Data["AppUrl"] = setting.AppURL
@@ -161,23 +165,70 @@ func Config(ctx *context.Context) {
ctx.Data["Loggers"] = log.GetManager().DumpLoggers()
config.GetDynGetter().InvalidateCache()
- ctx.Data["SystemConfig"] = setting.Config()
prepareDeprecatedWarningsAlert(ctx)
ctx.HTML(http.StatusOK, tplConfig)
}
+func ConfigSettings(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("admin.config_settings")
+ ctx.Data["PageIsAdminConfig"] = true
+ ctx.Data["PageIsAdminConfigSettings"] = true
+ ctx.Data["DefaultOpenWithEditorAppsString"] = setting.DefaultOpenWithEditorApps().ToTextareaString()
+ ctx.HTML(http.StatusOK, tplConfigSettings)
+}
+
func ChangeConfig(ctx *context.Context) {
key := strings.TrimSpace(ctx.FormString("key"))
value := ctx.FormString("value")
cfg := setting.Config()
- allowedKeys := container.SetOf(cfg.Picture.DisableGravatar.DynKey(), cfg.Picture.EnableFederatedAvatar.DynKey())
- if !allowedKeys.Contains(key) {
+
+ marshalBool := func(v string) (string, error) {
+ if b, _ := strconv.ParseBool(v); b {
+ return "true", nil
+ }
+ return "false", nil
+ }
+ marshalOpenWithApps := func(value string) (string, error) {
+ lines := strings.Split(value, "\n")
+ var openWithEditorApps setting.OpenWithEditorAppsType
+ for _, line := range lines {
+ line = strings.TrimSpace(line)
+ if line == "" {
+ continue
+ }
+ displayName, openURL, ok := strings.Cut(line, "=")
+ displayName, openURL = strings.TrimSpace(displayName), strings.TrimSpace(openURL)
+ if !ok || displayName == "" || openURL == "" {
+ continue
+ }
+ openWithEditorApps = append(openWithEditorApps, setting.OpenWithEditorApp{
+ DisplayName: strings.TrimSpace(displayName),
+ OpenURL: strings.TrimSpace(openURL),
+ })
+ }
+ b, err := json.Marshal(openWithEditorApps)
+ if err != nil {
+ return "", err
+ }
+ return string(b), nil
+ }
+ marshallers := map[string]func(string) (string, error){
+ cfg.Picture.DisableGravatar.DynKey(): marshalBool,
+ cfg.Picture.EnableFederatedAvatar.DynKey(): marshalBool,
+ cfg.Repository.OpenWithEditorApps.DynKey(): marshalOpenWithApps,
+ }
+ marshaller, hasMarshaller := marshallers[key]
+ if !hasMarshaller {
+ ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
+ return
+ }
+ marshaledValue, err := marshaller(value)
+ if err != nil {
ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
return
}
- if err := system_model.SetSettings(ctx, map[string]string{key: value}); err != nil {
- log.Error("set setting failed: %v", err)
+ if err = system_model.SetSettings(ctx, map[string]string{key: marshaledValue}); err != nil {
ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
return
}
diff --git a/routers/web/admin/diagnosis.go b/routers/web/admin/diagnosis.go
index 5637894e6de..020554a35a4 100644
--- a/routers/web/admin/diagnosis.go
+++ b/routers/web/admin/diagnosis.go
@@ -9,8 +9,8 @@ import (
"runtime/pprof"
"time"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/httplib"
+ "code.gitea.io/gitea/services/context"
)
func MonitorDiagnosis(ctx *context.Context) {
@@ -58,4 +58,11 @@ func MonitorDiagnosis(ctx *context.Context) {
return
}
_ = pprof.Lookup("goroutine").WriteTo(f, 1)
+
+ f, err = zipWriter.CreateHeader(&zip.FileHeader{Name: "heap.dat", Method: zip.Deflate, Modified: time.Now()})
+ if err != nil {
+ ctx.ServerError("Failed to create zip file", err)
+ return
+ }
+ _ = pprof.Lookup("heap").WriteTo(f, 0)
}
diff --git a/routers/web/admin/emails.go b/routers/web/admin/emails.go
index 59f80035d89..2cf4035c6a8 100644
--- a/routers/web/admin/emails.go
+++ b/routers/web/admin/emails.go
@@ -11,10 +11,10 @@ import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -68,10 +68,10 @@ func Emails(ctx *context.Context) {
opts.Keyword = ctx.FormTrim("q")
opts.SortType = orderBy
if len(ctx.FormString("is_activated")) != 0 {
- opts.IsActivated = util.OptionalBoolOf(ctx.FormBool("activated"))
+ opts.IsActivated = optional.Some(ctx.FormBool("activated"))
}
if len(ctx.FormString("is_primary")) != 0 {
- opts.IsPrimary = util.OptionalBoolOf(ctx.FormBool("primary"))
+ opts.IsPrimary = optional.Some(ctx.FormBool("primary"))
}
if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
diff --git a/routers/web/admin/hooks.go b/routers/web/admin/hooks.go
index cd8cc29cdfb..8d59fbb858e 100644
--- a/routers/web/admin/hooks.go
+++ b/routers/web/admin/hooks.go
@@ -8,9 +8,9 @@ import (
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -35,7 +35,7 @@ func DefaultOrSystemWebhooks(ctx *context.Context) {
sys["Title"] = ctx.Tr("admin.systemhooks")
sys["Description"] = ctx.Tr("admin.systemhooks.desc")
- sys["Webhooks"], err = webhook.GetSystemWebhooks(ctx, util.OptionalBoolNone)
+ sys["Webhooks"], err = webhook.GetSystemWebhooks(ctx, optional.None[bool]())
sys["BaseLink"] = setting.AppSubURL + "/admin/hooks"
sys["BaseLinkNew"] = setting.AppSubURL + "/admin/system-hooks"
if err != nil {
diff --git a/routers/web/admin/notice.go b/routers/web/admin/notice.go
index 99039a2a9f9..36303cbc06e 100644
--- a/routers/web/admin/notice.go
+++ b/routers/web/admin/notice.go
@@ -8,11 +8,12 @@ import (
"net/http"
"strconv"
+ "code.gitea.io/gitea/models/db"
system_model "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -55,7 +56,7 @@ func DeleteNotices(ctx *context.Context) {
}
}
- if err := system_model.DeleteNoticesByIDs(ctx, ids); err != nil {
+ if err := db.DeleteByIDs[system_model.Notice](ctx, ids...); err != nil {
ctx.Flash.Error("DeleteNoticesByIDs: " + err.Error())
ctx.Status(http.StatusInternalServerError)
} else {
diff --git a/routers/web/admin/orgs.go b/routers/web/admin/orgs.go
index 00131c9e2f7..c5454db71e3 100644
--- a/routers/web/admin/orgs.go
+++ b/routers/web/admin/orgs.go
@@ -8,10 +8,10 @@ import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/web/explore"
+ "code.gitea.io/gitea/services/context"
)
const (
diff --git a/routers/web/admin/packages.go b/routers/web/admin/packages.go
index f4e1a93a22f..39f064a1be6 100644
--- a/routers/web/admin/packages.go
+++ b/routers/web/admin/packages.go
@@ -11,9 +11,9 @@ import (
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
packages_cleanup_service "code.gitea.io/gitea/services/packages/cleanup"
)
@@ -36,7 +36,7 @@ func Packages(ctx *context.Context) {
Type: packages_model.Type(packageType),
Name: packages_model.SearchValue{Value: query},
Sort: sort,
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
Paginator: &db.ListOptions{
PageSize: setting.UI.PackagesPagingNum,
Page: page,
@@ -108,6 +108,6 @@ func CleanupExpiredData(ctx *context.Context) {
return
}
- ctx.Flash.Success(ctx.Tr("packages.cleanup.success"))
+ ctx.Flash.Success(ctx.Tr("admin.packages.cleanup.success"))
ctx.Redirect(setting.AppSubURL + "/admin/packages")
}
diff --git a/routers/web/admin/queue.go b/routers/web/admin/queue.go
index 18a8d7d3e68..d8c50730b14 100644
--- a/routers/web/admin/queue.go
+++ b/routers/web/admin/queue.go
@@ -7,9 +7,9 @@ import (
"net/http"
"strconv"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
)
func Queues(ctx *context.Context) {
diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go
index 45c280ef731..0815879bb3e 100644
--- a/routers/web/admin/repos.go
+++ b/routers/web/admin/repos.go
@@ -4,6 +4,7 @@
package admin
import (
+ "fmt"
"net/http"
"net/url"
"strings"
@@ -12,11 +13,11 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/web/explore"
+ "code.gitea.io/gitea/services/context"
repo_service "code.gitea.io/gitea/services/repository"
)
@@ -84,7 +85,7 @@ func UnadoptedRepos(ctx *context.Context) {
if !doSearch {
pager := context.NewPagination(0, opts.PageSize, opts.Page, 5)
pager.SetDefaultParams(ctx)
- pager.AddParam(ctx, "search", "search")
+ pager.AddParamString("search", fmt.Sprint(doSearch))
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplUnadoptedRepos)
return
@@ -98,7 +99,7 @@ func UnadoptedRepos(ctx *context.Context) {
ctx.Data["Dirs"] = repoNames
pager := context.NewPagination(count, opts.PageSize, opts.Page, 5)
pager.SetDefaultParams(ctx)
- pager.AddParam(ctx, "search", "search")
+ pager.AddParamString("search", fmt.Sprint(doSearch))
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplUnadoptedRepos)
}
diff --git a/routers/web/admin/runners.go b/routers/web/admin/runners.go
index eaa268b4f14..d73290a8dba 100644
--- a/routers/web/admin/runners.go
+++ b/routers/web/admin/runners.go
@@ -4,8 +4,8 @@
package admin
import (
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
)
func RedirectToDefaultSetting(ctx *context.Context) {
diff --git a/routers/web/admin/stacktrace.go b/routers/web/admin/stacktrace.go
index b603fb59a26..d6def94bb49 100644
--- a/routers/web/admin/stacktrace.go
+++ b/routers/web/admin/stacktrace.go
@@ -7,9 +7,9 @@ import (
"net/http"
"runtime"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
)
// Stacktrace show admin monitor goroutines page
diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go
index 58120818b0b..6dfcfc3d9a1 100644
--- a/routers/web/admin/users.go
+++ b/routers/web/admin/users.go
@@ -5,6 +5,7 @@
package admin
import (
+ "errors"
"net/http"
"net/url"
"strconv"
@@ -18,13 +19,14 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/password"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/explore"
user_setting "code.gitea.io/gitea/routers/web/user/setting"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/mailer"
user_service "code.gitea.io/gitea/services/user"
@@ -93,8 +95,8 @@ func NewUser(ctx *context.Context) {
ctx.Data["login_type"] = "0-0"
- sources, err := auth.FindSources(ctx, auth.FindSourcesOptions{
- IsActive: util.OptionalBoolTrue,
+ sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
+ IsActive: optional.Some(true),
})
if err != nil {
ctx.ServerError("auth.Sources", err)
@@ -114,8 +116,8 @@ func NewUserPost(ctx *context.Context) {
ctx.Data["DefaultUserVisibilityMode"] = setting.Service.DefaultUserVisibilityMode
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
- sources, err := auth.FindSources(ctx, auth.FindSourcesOptions{
- IsActive: util.OptionalBoolTrue,
+ sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
+ IsActive: optional.Some(true),
})
if err != nil {
ctx.ServerError("auth.Sources", err)
@@ -138,7 +140,7 @@ func NewUserPost(ctx *context.Context) {
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
- IsActive: util.OptionalBoolTrue,
+ IsActive: optional.Some(true),
Visibility: &form.Visibility,
}
@@ -162,11 +164,10 @@ func NewUserPost(ctx *context.Context) {
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplUserNew, &form)
return
}
- pwned, err := password.IsPwned(ctx, form.Password)
- if pwned {
+ if err := password.IsPwned(ctx, form.Password); err != nil {
ctx.Data["Err_Password"] = true
errMsg := ctx.Tr("auth.password_pwned")
- if err != nil {
+ if password.IsErrIsPwnedRequest(err) {
log.Error(err.Error())
errMsg = ctx.Tr("auth.password_pwned_err")
}
@@ -176,7 +177,7 @@ func NewUserPost(ctx *context.Context) {
u.MustChangePassword = form.MustChangePassword
}
- if err := user_model.CreateUser(ctx, u, overwriteDefault); err != nil {
+ if err := user_model.AdminCreateUser(ctx, u, overwriteDefault); err != nil {
switch {
case user_model.IsErrUserAlreadyExist(err):
ctx.Data["Err_UserName"] = true
@@ -184,10 +185,7 @@ func NewUserPost(ctx *context.Context) {
case user_model.IsErrEmailAlreadyUsed(err):
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserNew, &form)
- case user_model.IsErrEmailCharIsNotSupported(err):
- ctx.Data["Err_Email"] = true
- ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserNew, &form)
- case user_model.IsErrEmailInvalid(err):
+ case user_model.IsErrEmailInvalid(err), user_model.IsErrEmailCharIsNotSupported(err):
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserNew, &form)
case db.IsErrNameReserved(err):
@@ -204,6 +202,11 @@ func NewUserPost(ctx *context.Context) {
}
return
}
+
+ if !user_model.IsEmailDomainAllowed(u.Email) {
+ ctx.Flash.Warning(ctx.Tr("form.email_domain_is_not_allowed", u.Email))
+ }
+
log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)
// Send email notification.
@@ -237,7 +240,7 @@ func prepareUserInfo(ctx *context.Context) *user_model.User {
ctx.Data["LoginSource"] = &auth.Source{}
}
- sources, err := auth.FindSources(ctx, auth.FindSourcesOptions{})
+ sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{})
if err != nil {
ctx.ServerError("auth.Sources", err)
return nil
@@ -278,7 +281,7 @@ func ViewUser(ctx *context.Context) {
OwnerID: u.ID,
OrderBy: db.SearchOrderByAlphabetically,
Private: true,
- Collaborate: util.OptionalBoolFalse,
+ Collaborate: optional.Some(false),
})
if err != nil {
ctx.ServerError("SearchRepository", err)
@@ -296,7 +299,7 @@ func ViewUser(ctx *context.Context) {
ctx.Data["Emails"] = emails
ctx.Data["EmailsTotal"] = len(emails)
- orgs, err := org_model.FindOrgs(ctx, org_model.FindOrgOptions{
+ orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{
ListOptions: db.ListOptions{
ListAll: true,
},
@@ -348,67 +351,114 @@ func EditUserPost(ctx *context.Context) {
return
}
+ if form.UserName != "" {
+ if err := user_service.RenameUser(ctx, u, form.UserName); err != nil {
+ switch {
+ case user_model.IsErrUserIsNotLocal(err):
+ ctx.Data["Err_UserName"] = true
+ ctx.RenderWithErr(ctx.Tr("form.username_change_not_local_user"), tplUserEdit, &form)
+ case user_model.IsErrUserAlreadyExist(err):
+ ctx.Data["Err_UserName"] = true
+ ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplUserEdit, &form)
+ case db.IsErrNameReserved(err):
+ ctx.Data["Err_UserName"] = true
+ ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", form.UserName), tplUserEdit, &form)
+ case db.IsErrNamePatternNotAllowed(err):
+ ctx.Data["Err_UserName"] = true
+ ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", form.UserName), tplUserEdit, &form)
+ case db.IsErrNameCharsNotAllowed(err):
+ ctx.Data["Err_UserName"] = true
+ ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", form.UserName), tplUserEdit, &form)
+ default:
+ ctx.ServerError("RenameUser", err)
+ }
+ return
+ }
+ }
+
+ authOpts := &user_service.UpdateAuthOptions{
+ Password: optional.FromNonDefault(form.Password),
+ LoginName: optional.Some(form.LoginName),
+ }
+
+ // skip self Prohibit Login
+ if ctx.Doer.ID == u.ID {
+ authOpts.ProhibitLogin = optional.Some(false)
+ } else {
+ authOpts.ProhibitLogin = optional.Some(form.ProhibitLogin)
+ }
+
fields := strings.Split(form.LoginType, "-")
if len(fields) == 2 {
- loginType, _ := strconv.ParseInt(fields[0], 10, 0)
authSource, _ := strconv.ParseInt(fields[1], 10, 64)
- if u.LoginSource != authSource {
- u.LoginSource = authSource
- u.LoginType = auth.Type(loginType)
- }
+ authOpts.LoginSource = optional.Some(authSource)
}
- if len(form.Password) > 0 && (u.IsLocal() || u.IsOAuth2()) {
- var err error
- if len(form.Password) < setting.MinPasswordLength {
+ if err := user_service.UpdateAuth(ctx, u, authOpts); err != nil {
+ switch {
+ case errors.Is(err, password.ErrMinLength):
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserEdit, &form)
- return
- }
- if !password.IsComplexEnough(form.Password) {
+ case errors.Is(err, password.ErrComplexity):
+ ctx.Data["Err_Password"] = true
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplUserEdit, &form)
- return
- }
- pwned, err := password.IsPwned(ctx, form.Password)
- if pwned {
+ case errors.Is(err, password.ErrIsPwned):
ctx.Data["Err_Password"] = true
- errMsg := ctx.Tr("auth.password_pwned")
- if err != nil {
- log.Error(err.Error())
- errMsg = ctx.Tr("auth.password_pwned_err")
- }
- ctx.RenderWithErr(errMsg, tplUserEdit, &form)
- return
- }
-
- if err := user_model.ValidateEmail(form.Email); err != nil {
- ctx.Data["Err_Email"] = true
- ctx.RenderWithErr(ctx.Tr("form.email_error"), tplUserEdit, &form)
- return
+ ctx.RenderWithErr(ctx.Tr("auth.password_pwned"), tplUserEdit, &form)
+ case password.IsErrIsPwnedRequest(err):
+ log.Error("%s", err.Error())
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplUserEdit, &form)
+ default:
+ ctx.ServerError("UpdateUser", err)
}
+ return
+ }
- if u.Salt, err = user_model.GetUserSalt(); err != nil {
- ctx.ServerError("UpdateUser", err)
+ if form.Email != "" {
+ if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, u, form.Email); err != nil {
+ switch {
+ case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
+ ctx.Data["Err_Email"] = true
+ ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserEdit, &form)
+ case user_model.IsErrEmailAlreadyUsed(err):
+ ctx.Data["Err_Email"] = true
+ ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserEdit, &form)
+ default:
+ ctx.ServerError("AddOrSetPrimaryEmailAddress", err)
+ }
return
}
- if err = u.SetPassword(form.Password); err != nil {
- ctx.ServerError("SetPassword", err)
- return
+ if !user_model.IsEmailDomainAllowed(form.Email) {
+ ctx.Flash.Warning(ctx.Tr("form.email_domain_is_not_allowed", form.Email))
}
}
- if len(form.UserName) != 0 && u.Name != form.UserName {
- if err := user_setting.HandleUsernameChange(ctx, u, form.UserName); err != nil {
- if ctx.Written() {
- return
- }
- ctx.RenderWithErr(ctx.Flash.ErrorMsg, tplUserEdit, &form)
- return
+ opts := &user_service.UpdateOptions{
+ FullName: optional.Some(form.FullName),
+ Website: optional.Some(form.Website),
+ Location: optional.Some(form.Location),
+ IsActive: optional.Some(form.Active),
+ IsAdmin: optional.Some(form.Admin),
+ AllowGitHook: optional.Some(form.AllowGitHook),
+ AllowImportLocal: optional.Some(form.AllowImportLocal),
+ MaxRepoCreation: optional.Some(form.MaxRepoCreation),
+ AllowCreateOrganization: optional.Some(form.AllowCreateOrganization),
+ IsRestricted: optional.Some(form.Restricted),
+ Visibility: optional.Some(form.Visibility),
+ Language: optional.Some(form.Language),
+ }
+
+ if err := user_service.UpdateUser(ctx, u, opts); err != nil {
+ if models.IsErrDeleteLastAdminUser(err) {
+ ctx.RenderWithErr(ctx.Tr("auth.last_admin"), tplUserEdit, &form)
+ } else {
+ ctx.ServerError("UpdateUser", err)
}
- u.Name = form.UserName
- u.LowerName = strings.ToLower(form.UserName)
+ return
}
+ log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, u.Name)
if form.Reset2FA {
tf, err := auth.GetTwoFactorByUID(ctx, u.ID)
@@ -433,46 +483,7 @@ func EditUserPost(ctx *context.Context) {
return
}
}
-
- }
-
- u.LoginName = form.LoginName
- u.FullName = form.FullName
- emailChanged := !strings.EqualFold(u.Email, form.Email)
- u.Email = form.Email
- u.Website = form.Website
- u.Location = form.Location
- u.MaxRepoCreation = form.MaxRepoCreation
- u.IsActive = form.Active
- u.IsAdmin = form.Admin
- u.IsRestricted = form.Restricted
- u.AllowGitHook = form.AllowGitHook
- u.AllowImportLocal = form.AllowImportLocal
- u.AllowCreateOrganization = form.AllowCreateOrganization
-
- u.Visibility = form.Visibility
-
- // skip self Prohibit Login
- if ctx.Doer.ID == u.ID {
- u.ProhibitLogin = false
- } else {
- u.ProhibitLogin = form.ProhibitLogin
- }
-
- if err := user_model.UpdateUser(ctx, u, emailChanged); err != nil {
- if user_model.IsErrEmailAlreadyUsed(err) {
- ctx.Data["Err_Email"] = true
- ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserEdit, &form)
- } else if user_model.IsErrEmailCharIsNotSupported(err) ||
- user_model.IsErrEmailInvalid(err) {
- ctx.Data["Err_Email"] = true
- ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserEdit, &form)
- } else {
- ctx.ServerError("UpdateUser", err)
- }
- return
}
- log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, u.Name)
ctx.Flash.Success(ctx.Tr("admin.users.update_profile_success"))
ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")))
@@ -503,7 +514,10 @@ func DeleteUser(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")))
case models.IsErrUserOwnPackages(err):
ctx.Flash.Error(ctx.Tr("admin.users.still_own_packages"))
- ctx.Redirect(setting.AppSubURL + "/admin/users/" + ctx.Params(":userid"))
+ ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")))
+ case models.IsErrDeleteLastAdminUser(err):
+ ctx.Flash.Error(ctx.Tr("auth.last_admin"))
+ ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")))
default:
ctx.ServerError("DeleteUser", err)
}
diff --git a/routers/web/admin/users_test.go b/routers/web/admin/users_test.go
index 560ee70ea04..f6f92378581 100644
--- a/routers/web/admin/users_test.go
+++ b/routers/web/admin/users_test.go
@@ -8,10 +8,10 @@ import (
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/contexttest"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/contexttest"
"code.gitea.io/gitea/services/forms"
"github.com/stretchr/testify/assert"
diff --git a/routers/web/auth/2fa.go b/routers/web/auth/2fa.go
index dc0062ebaa5..f93177bf96b 100644
--- a/routers/web/auth/2fa.go
+++ b/routers/web/auth/2fa.go
@@ -10,9 +10,9 @@ import (
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/forms"
)
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index 0ea91fc759a..da6bef207ad 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -7,6 +7,7 @@ package auth
import (
"errors"
"fmt"
+ "html/template"
"net/http"
"strings"
@@ -15,9 +16,9 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/password"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/eventsource"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
@@ -27,28 +28,24 @@ import (
"code.gitea.io/gitea/routers/utils"
auth_service "code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/auth/source/oauth2"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/mailer"
+ user_service "code.gitea.io/gitea/services/user"
"github.com/markbates/goth"
)
const (
- // tplSignIn template for sign in page
- tplSignIn base.TplName = "user/auth/signin"
- // tplSignUp template path for sign up page
- tplSignUp base.TplName = "user/auth/signup"
- // TplActivate template path for activate user
- TplActivate base.TplName = "user/auth/activate"
+ tplSignIn base.TplName = "user/auth/signin" // for sign in page
+ tplSignUp base.TplName = "user/auth/signup" // for sign up page
+ TplActivate base.TplName = "user/auth/activate" // for activate user
+ TplActivatePrompt base.TplName = "user/auth/activate_prompt" // for showing a message for user activation
)
// autoSignIn reads cookie and try to auto-login.
func autoSignIn(ctx *context.Context) (bool, error) {
- if !db.HasEngine {
- return false, nil
- }
-
isSucceed := false
defer func() {
if !isSucceed {
@@ -108,9 +105,11 @@ func autoSignIn(ctx *context.Context) (bool, error) {
func resetLocale(ctx *context.Context, u *user_model.User) error {
// Language setting of the user overwrites the one previously set
// If the user does not have a locale set, we save the current one.
- if len(u.Language) == 0 {
- u.Language = ctx.Locale.Language()
- if err := user_model.UpdateUserCols(ctx, u, "language"); err != nil {
+ if u.Language == "" {
+ opts := &user_service.UpdateOptions{
+ Language: optional.Some(ctx.Locale.Language()),
+ }
+ if err := user_service.UpdateUser(ctx, u, opts); err != nil {
return err
}
}
@@ -124,9 +123,21 @@ func resetLocale(ctx *context.Context, u *user_model.User) error {
return nil
}
+func RedirectAfterLogin(ctx *context.Context) {
+ redirectTo := ctx.FormString("redirect_to")
+ if redirectTo == "" {
+ redirectTo = ctx.GetSiteCookie("redirect_to")
+ }
+ middleware.DeleteRedirectToCookie(ctx.Resp)
+ nextRedirectTo := setting.AppSubURL + string(setting.LandingPageURL)
+ if setting.LandingPageURL == setting.LandingPageLogin {
+ nextRedirectTo = setting.AppSubURL + "/" // do not cycle-redirect to the login page
+ }
+ ctx.RedirectToFirst(redirectTo, nextRedirectTo)
+}
+
func CheckAutoLogin(ctx *context.Context) bool {
- // Check auto-login
- isSucceed, err := autoSignIn(ctx)
+ isSucceed, err := autoSignIn(ctx) // try to auto-login
if err != nil {
if errors.Is(err, auth_service.ErrAuthTokenInvalidHash) {
ctx.Flash.Error(ctx.Tr("auth.remember_me.compromised"), true)
@@ -139,13 +150,10 @@ func CheckAutoLogin(ctx *context.Context) bool {
redirectTo := ctx.FormString("redirect_to")
if len(redirectTo) > 0 {
middleware.SetRedirectToCookie(ctx.Resp, redirectTo)
- } else {
- redirectTo = ctx.GetSiteCookie("redirect_to")
}
if isSucceed {
- middleware.DeleteRedirectToCookie(ctx.Resp)
- ctx.RedirectToFirst(redirectTo, setting.AppSubURL+string(setting.LandingPageURL))
+ RedirectAfterLogin(ctx)
return true
}
@@ -160,7 +168,12 @@ func SignIn(ctx *context.Context) {
return
}
- oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue)
+ if ctx.IsSigned {
+ RedirectAfterLogin(ctx)
+ return
+ }
+
+ oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
if err != nil {
ctx.ServerError("UserSignIn", err)
return
@@ -183,7 +196,7 @@ func SignIn(ctx *context.Context) {
func SignInPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("sign_in")
- oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue)
+ oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
if err != nil {
ctx.ServerError("UserSignIn", err)
return
@@ -330,10 +343,12 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
// Language setting of the user overwrites the one previously set
// If the user does not have a locale set, we save the current one.
- if len(u.Language) == 0 {
- u.Language = ctx.Locale.Language()
- if err := user_model.UpdateUserCols(ctx, u, "language"); err != nil {
- ctx.ServerError("UpdateUserCols Language", fmt.Errorf("Error updating user language [user: %d, locale: %s]", u.ID, u.Language))
+ if u.Language == "" {
+ opts := &user_service.UpdateOptions{
+ Language: optional.Some(ctx.Locale.Language()),
+ }
+ if err := user_service.UpdateUser(ctx, u, opts); err != nil {
+ ctx.ServerError("UpdateUser Language", fmt.Errorf("Error updating user language [user: %d, locale: %s]", u.ID, ctx.Locale.Language()))
return setting.AppSubURL + "/"
}
}
@@ -348,9 +363,8 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
ctx.Csrf.DeleteCookie(ctx)
// Register last login
- u.SetLastLogin()
- if err := user_model.UpdateUserCols(ctx, u, "last_login_unix"); err != nil {
- ctx.ServerError("UpdateUserCols", err)
+ if err := user_service.UpdateUser(ctx, u, &user_service.UpdateOptions{SetLastLogin: true}); err != nil {
+ ctx.ServerError("UpdateUser", err)
return setting.AppSubURL + "/"
}
@@ -368,14 +382,14 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
return setting.AppSubURL + "/"
}
-func getUserName(gothUser *goth.User) string {
+func getUserName(gothUser *goth.User) (string, error) {
switch setting.OAuth2Client.Username {
case setting.OAuth2UsernameEmail:
- return strings.Split(gothUser.Email, "@")[0]
+ return user_model.NormalizeUserName(strings.Split(gothUser.Email, "@")[0])
case setting.OAuth2UsernameNickname:
- return gothUser.NickName
+ return user_model.NormalizeUserName(gothUser.NickName)
default: // OAuth2UsernameUserid
- return gothUser.UserID
+ return gothUser.UserID, nil
}
}
@@ -406,7 +420,7 @@ func SignUp(ctx *context.Context) {
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
- oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue)
+ oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
if err != nil {
ctx.ServerError("UserSignUp", err)
return
@@ -435,7 +449,7 @@ func SignUpPost(ctx *context.Context) {
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
- oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue)
+ oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
if err != nil {
ctx.ServerError("UserSignUp", err)
return
@@ -482,10 +496,9 @@ func SignUpPost(ctx *context.Context) {
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplSignUp, &form)
return
}
- pwned, err := password.IsPwned(ctx, form.Password)
- if pwned {
+ if err := password.IsPwned(ctx, form.Password); err != nil {
errMsg := ctx.Tr("auth.password_pwned")
- if err != nil {
+ if password.IsErrIsPwnedRequest(err) {
log.Error(err.Error())
errMsg = ctx.Tr("auth.password_pwned_err")
}
@@ -589,10 +602,12 @@ func createUserInContext(ctx *context.Context, tpl base.TplName, form any, u *us
func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.User) (ok bool) {
// Auto-set admin for the only user.
if user_model.CountUsers(ctx, nil) == 1 {
- u.IsAdmin = true
- u.IsActive = true
- u.SetLastLogin()
- if err := user_model.UpdateUserCols(ctx, u, "is_admin", "is_active", "last_login_unix"); err != nil {
+ opts := &user_service.UpdateOptions{
+ IsActive: optional.Some(true),
+ IsAdmin: optional.Some(true),
+ SetLastLogin: true,
+ }
+ if err := user_service.UpdateUser(ctx, u, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return false
}
@@ -607,76 +622,87 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
}
}
- // Send confirmation email
- if !u.IsActive && u.ID > 1 {
- if setting.Service.RegisterManualConfirm {
- ctx.Data["ManualActivationOnly"] = true
- ctx.HTML(http.StatusOK, TplActivate)
- return false
- }
+ // for active user or the first (admin) user, we don't need to send confirmation email
+ if u.IsActive || u.ID == 1 {
+ return true
+ }
- mailer.SendActivateAccountMail(ctx.Locale, u)
+ if setting.Service.RegisterManualConfirm {
+ renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.manual_activation_only"))
+ return false
+ }
- ctx.Data["IsSendRegisterMail"] = true
- ctx.Data["Email"] = u.Email
- ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
- ctx.HTML(http.StatusOK, TplActivate)
+ sendActivateEmail(ctx, u)
+ return false
+}
- if setting.CacheService.Enabled {
- if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
- log.Error("Set cache(MailResendLimit) fail: %v", err)
- }
- }
- return false
+func renderActivationPromptMessage(ctx *context.Context, msg template.HTML) {
+ ctx.Data["ActivationPromptMessage"] = msg
+ ctx.HTML(http.StatusOK, TplActivatePrompt)
+}
+
+func sendActivateEmail(ctx *context.Context, u *user_model.User) {
+ if ctx.Cache.IsExist("MailResendLimit_" + u.LowerName) {
+ renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.resent_limit_prompt"))
+ return
}
- return true
+ if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
+ log.Error("Set cache(MailResendLimit) fail: %v", err)
+ renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.resent_limit_prompt"))
+ return
+ }
+
+ mailer.SendActivateAccountMail(ctx.Locale, u)
+
+ activeCodeLives := timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
+ msgHTML := ctx.Locale.Tr("auth.confirmation_mail_sent_prompt_ex", u.Email, activeCodeLives)
+ renderActivationPromptMessage(ctx, msgHTML)
+}
+
+func renderActivationVerifyPassword(ctx *context.Context, code string) {
+ ctx.Data["ActivationCode"] = code
+ ctx.Data["NeedVerifyLocalPassword"] = true
+ ctx.HTML(http.StatusOK, TplActivate)
+}
+
+func renderActivationChangeEmail(ctx *context.Context) {
+ ctx.HTML(http.StatusOK, TplActivate)
}
// Activate render activate user page
func Activate(ctx *context.Context) {
code := ctx.FormString("code")
- if len(code) == 0 {
- ctx.Data["IsActivatePage"] = true
- if ctx.Doer == nil || ctx.Doer.IsActive {
- ctx.NotFound("invalid user", nil)
+ if code == "" {
+ if ctx.Doer == nil {
+ ctx.Redirect(setting.AppSubURL + "/user/login")
+ return
+ } else if ctx.Doer.IsActive {
+ ctx.Redirect(setting.AppSubURL + "/")
return
}
- // Resend confirmation email.
- if setting.Service.RegisterEmailConfirm {
- if setting.CacheService.Enabled && ctx.Cache.IsExist("MailResendLimit_"+ctx.Doer.LowerName) {
- ctx.Data["ResendLimited"] = true
- } else {
- ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
- mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer)
- if setting.CacheService.Enabled {
- if err := ctx.Cache.Put("MailResendLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
- log.Error("Set cache(MailResendLimit) fail: %v", err)
- }
- }
- }
- } else {
- ctx.Data["ServiceNotEnabled"] = true
+ if setting.MailService == nil || !setting.Service.RegisterEmailConfirm {
+ renderActivationPromptMessage(ctx, ctx.Tr("auth.disable_register_mail"))
+ return
}
- ctx.HTML(http.StatusOK, TplActivate)
+
+ // Resend confirmation email. FIXME: ideally this should be in a POST request
+ sendActivateEmail(ctx, ctx.Doer)
return
}
+ // TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated
user := user_model.VerifyUserActiveCode(ctx, code)
- // if code is wrong
- if user == nil {
- ctx.Data["IsCodeInvalid"] = true
- ctx.HTML(http.StatusOK, TplActivate)
+ if user == nil { // if code is wrong
+ renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code"))
return
}
// if account is local account, verify password
if user.LoginSource == 0 {
- ctx.Data["Code"] = code
- ctx.Data["NeedsPassword"] = true
- ctx.HTML(http.StatusOK, TplActivate)
+ renderActivationVerifyPassword(ctx, code)
return
}
@@ -686,31 +712,49 @@ func Activate(ctx *context.Context) {
// ActivatePost handles account activation with password check
func ActivatePost(ctx *context.Context) {
code := ctx.FormString("code")
- if len(code) == 0 {
+ if ctx.Doer != nil && ctx.Doer.IsActive {
+ ctx.Redirect(setting.AppSubURL + "/user/activate") // it will redirect again to the correct page
+ return
+ }
+
+ if code == "" {
+ newEmail := strings.TrimSpace(ctx.FormString("change_email"))
+ if ctx.Doer != nil && newEmail != "" && !strings.EqualFold(ctx.Doer.Email, newEmail) {
+ if user_model.ValidateEmail(newEmail) != nil {
+ ctx.Flash.Error(ctx.Locale.Tr("form.email_invalid"), true)
+ renderActivationChangeEmail(ctx)
+ return
+ }
+ err := user_model.ChangeInactivePrimaryEmail(ctx, ctx.Doer.ID, ctx.Doer.Email, newEmail)
+ if err != nil {
+ ctx.Flash.Error(ctx.Locale.Tr("admin.emails.not_updated", newEmail), true)
+ renderActivationChangeEmail(ctx)
+ return
+ }
+ ctx.Doer.Email = newEmail
+ }
+ // FIXME: at the moment, GET request handles the "send confirmation email" action. But the old code does this redirect and then send a confirmation email.
ctx.Redirect(setting.AppSubURL + "/user/activate")
return
}
+ // TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated
user := user_model.VerifyUserActiveCode(ctx, code)
- // if code is wrong
- if user == nil {
- ctx.Data["IsCodeInvalid"] = true
- ctx.HTML(http.StatusOK, TplActivate)
+ if user == nil { // if code is wrong
+ renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code"))
return
}
// if account is local account, verify password
if user.LoginSource == 0 {
password := ctx.FormString("password")
- if len(password) == 0 {
- ctx.Data["Code"] = code
- ctx.Data["NeedsPassword"] = true
- ctx.HTML(http.StatusOK, TplActivate)
+ if password == "" {
+ renderActivationVerifyPassword(ctx, code)
return
}
if !user.ValidatePassword(password) {
- ctx.Data["IsPasswordInvalid"] = true
- ctx.HTML(http.StatusOK, TplActivate)
+ ctx.Flash.Error(ctx.Locale.Tr("auth.invalid_password"), true)
+ renderActivationVerifyPassword(ctx, code)
return
}
}
@@ -756,10 +800,8 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
return
}
- // Register last login
- user.SetLastLogin()
- if err := user_model.UpdateUserCols(ctx, user, "last_login_unix"); err != nil {
- ctx.ServerError("UpdateUserCols", err)
+ if err := user_service.UpdateUser(ctx, user, &user_service.UpdateOptions{SetLastLogin: true}); err != nil {
+ ctx.ServerError("UpdateUser", err)
return
}
@@ -789,7 +831,7 @@ func ActivateEmail(ctx *context.Context) {
if u, err := user_model.GetUserByID(ctx, email.UID); err != nil {
log.Warn("GetUserByID: %d", email.UID)
- } else if setting.CacheService.Enabled {
+ } else {
// Allow user to validate more emails
_ = ctx.Cache.Delete("MailResendLimit_" + u.LowerName)
}
diff --git a/routers/web/auth/auth_test.go b/routers/web/auth/auth_test.go
new file mode 100644
index 00000000000..c6afbf877c0
--- /dev/null
+++ b/routers/web/auth/auth_test.go
@@ -0,0 +1,43 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package auth
+
+import (
+ "net/http"
+ "net/url"
+ "testing"
+
+ "code.gitea.io/gitea/modules/test"
+ "code.gitea.io/gitea/services/contexttest"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestUserLogin(t *testing.T) {
+ ctx, resp := contexttest.MockContext(t, "/user/login")
+ SignIn(ctx)
+ assert.Equal(t, http.StatusOK, resp.Code)
+
+ ctx, resp = contexttest.MockContext(t, "/user/login")
+ ctx.IsSigned = true
+ SignIn(ctx)
+ assert.Equal(t, http.StatusSeeOther, resp.Code)
+ assert.Equal(t, "/", test.RedirectURL(resp))
+
+ ctx, resp = contexttest.MockContext(t, "/user/login?redirect_to=/other")
+ ctx.IsSigned = true
+ SignIn(ctx)
+ assert.Equal(t, "/other", test.RedirectURL(resp))
+
+ ctx, resp = contexttest.MockContext(t, "/user/login")
+ ctx.Req.AddCookie(&http.Cookie{Name: "redirect_to", Value: "/other-cookie"})
+ ctx.IsSigned = true
+ SignIn(ctx)
+ assert.Equal(t, "/other-cookie", test.RedirectURL(resp))
+
+ ctx, resp = contexttest.MockContext(t, "/user/login?redirect_to="+url.QueryEscape("https://example.com"))
+ ctx.IsSigned = true
+ SignIn(ctx)
+ assert.Equal(t, "/", test.RedirectURL(resp))
+}
diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go
index f41590dc135..f744a57a43f 100644
--- a/routers/web/auth/linkaccount.go
+++ b/routers/web/auth/linkaccount.go
@@ -12,13 +12,13 @@ import (
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
auth_service "code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/auth/source/oauth2"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/forms"
@@ -55,7 +55,11 @@ func LinkAccount(ctx *context.Context) {
}
gu, _ := gothUser.(goth.User)
- uname := getUserName(&gu)
+ uname, err := getUserName(&gu)
+ if err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
email := gu.Email
ctx.Data["user_name"] = uname
ctx.Data["email"] = email
diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go
index 21d82cea450..d5ca7397f06 100644
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"html"
+ "html/template"
"io"
"net/http"
"net/url"
@@ -21,9 +22,9 @@ import (
auth_module "code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
@@ -32,6 +33,7 @@ import (
auth_service "code.gitea.io/gitea/services/auth"
source_service "code.gitea.io/gitea/services/auth/source"
"code.gitea.io/gitea/services/auth/source/oauth2"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/forms"
user_service "code.gitea.io/gitea/services/user"
@@ -498,11 +500,11 @@ func AuthorizeOAuth(ctx *context.Context) {
ctx.Data["Scope"] = form.Scope
ctx.Data["Nonce"] = form.Nonce
if user != nil {
- ctx.Data["ApplicationCreatorLinkHTML"] = fmt.Sprintf(`@%s`, html.EscapeString(user.HomeLink()), html.EscapeString(user.Name))
+ ctx.Data["ApplicationCreatorLinkHTML"] = template.HTML(fmt.Sprintf(`@%s`, html.EscapeString(user.HomeLink()), html.EscapeString(user.Name)))
} else {
- ctx.Data["ApplicationCreatorLinkHTML"] = fmt.Sprintf(`%s`, html.EscapeString(setting.AppSubURL+"/"), html.EscapeString(setting.AppName))
+ ctx.Data["ApplicationCreatorLinkHTML"] = template.HTML(fmt.Sprintf(`%s`, html.EscapeString(setting.AppSubURL+"/"), html.EscapeString(setting.AppName)))
}
- ctx.Data["ApplicationRedirectDomainHTML"] = "" + html.EscapeString(form.RedirectURI) + ""
+ ctx.Data["ApplicationRedirectDomainHTML"] = template.HTML("" + html.EscapeString(form.RedirectURI) + "")
// TODO document SESSION <=> FORM
err = ctx.Session.Set("client_id", app.ClientID)
if err != nil {
@@ -578,16 +580,8 @@ func GrantApplicationOAuth(ctx *context.Context) {
// OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
func OIDCWellKnown(ctx *context.Context) {
- t, err := ctx.Render.TemplateLookup("user/auth/oidc_wellknown", nil)
- if err != nil {
- ctx.ServerError("unable to find template", err)
- return
- }
- ctx.Resp.Header().Set("Content-Type", "application/json")
ctx.Data["SigningKey"] = oauth2.DefaultSigningKey
- if err = t.Execute(ctx.Resp, ctx.Data); err != nil {
- ctx.ServerError("unable to execute template", err)
- }
+ ctx.JSONTemplate("user/auth/oidc_wellknown")
}
// OIDCKeys generates the JSON Web Key Set
@@ -970,8 +964,13 @@ func SignInOAuthCallback(ctx *context.Context) {
ctx.ServerError("CreateUser", err)
return
}
+ uname, err := getUserName(&gothUser)
+ if err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
u = &user_model.User{
- Name: getUserName(&gothUser),
+ Name: uname,
FullName: gothUser.Name,
Email: gothUser.Email,
LoginType: auth.OAuth2,
@@ -980,12 +979,14 @@ func SignInOAuthCallback(ctx *context.Context) {
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
- IsActive: util.OptionalBoolOf(!setting.OAuth2Client.RegisterEmailConfirm && !setting.Service.RegisterManualConfirm),
+ IsActive: optional.Some(!setting.OAuth2Client.RegisterEmailConfirm && !setting.Service.RegisterManualConfirm),
}
source := authSource.Cfg.(*oauth2.Source)
- setUserAdminAndRestrictedFromGroupClaims(source, u, &gothUser)
+ isAdmin, isRestricted := getUserAdminAndRestrictedFromGroupClaims(source, &gothUser)
+ u.IsAdmin = isAdmin.ValueOrDefault(false)
+ u.IsRestricted = isRestricted.ValueOrDefault(false)
if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
// error already handled
@@ -1049,19 +1050,17 @@ func getClaimedGroups(source *oauth2.Source, gothUser *goth.User) container.Set[
return claimValueToStringSet(groupClaims)
}
-func setUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, u *user_model.User, gothUser *goth.User) bool {
+func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *goth.User) (isAdmin, isRestricted optional.Option[bool]) {
groups := getClaimedGroups(source, gothUser)
- wasAdmin, wasRestricted := u.IsAdmin, u.IsRestricted
-
if source.AdminGroup != "" {
- u.IsAdmin = groups.Contains(source.AdminGroup)
+ isAdmin = optional.Some(groups.Contains(source.AdminGroup))
}
if source.RestrictedGroup != "" {
- u.IsRestricted = groups.Contains(source.RestrictedGroup)
+ isRestricted = optional.Some(groups.Contains(source.RestrictedGroup))
}
- return wasAdmin != u.IsAdmin || wasRestricted != u.IsRestricted
+ return isAdmin, isRestricted
}
func showLinkingLogin(ctx *context.Context, gothUser goth.User) {
@@ -1128,18 +1127,12 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
// Clear whatever CSRF cookie has right now, force to generate a new one
ctx.Csrf.DeleteCookie(ctx)
- // Register last login
- u.SetLastLogin()
-
- // Update GroupClaims
- changed := setUserAdminAndRestrictedFromGroupClaims(oauth2Source, u, &gothUser)
- cols := []string{"last_login_unix"}
- if changed {
- cols = append(cols, "is_admin", "is_restricted")
+ opts := &user_service.UpdateOptions{
+ SetLastLogin: true,
}
-
- if err := user_model.UpdateUserCols(ctx, u, cols...); err != nil {
- ctx.ServerError("UpdateUserCols", err)
+ opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
+ if err := user_service.UpdateUser(ctx, u, opts); err != nil {
+ ctx.ServerError("UpdateUser", err)
return
}
@@ -1172,10 +1165,11 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
return
}
- changed := setUserAdminAndRestrictedFromGroupClaims(oauth2Source, u, &gothUser)
- if changed {
- if err := user_model.UpdateUserCols(ctx, u, "is_admin", "is_restricted"); err != nil {
- ctx.ServerError("UpdateUserCols", err)
+ opts := &user_service.UpdateOptions{}
+ opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
+ if opts.IsAdmin.Has() || opts.IsRestricted.Has() {
+ if err := user_service.UpdateUser(ctx, u, opts); err != nil {
+ ctx.ServerError("UpdateUser", err)
return
}
}
diff --git a/routers/web/auth/openid.go b/routers/web/auth/openid.go
index 29ef772b1c6..2143b8096a0 100644
--- a/routers/web/auth/openid.go
+++ b/routers/web/auth/openid.go
@@ -11,12 +11,12 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/openid"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/auth"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go
index bdfa8c40258..c9e03860412 100644
--- a/routers/web/auth/password.go
+++ b/routers/web/auth/password.go
@@ -12,15 +12,17 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/password"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/routers/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/mailer"
+ user_service "code.gitea.io/gitea/services/user"
)
var (
@@ -35,7 +37,7 @@ func ForgotPasswd(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title")
if setting.MailService == nil {
- log.Warn(ctx.Tr("auth.disable_forgot_password_mail_admin"))
+ log.Warn("no mail service configured")
ctx.Data["IsResetDisable"] = true
ctx.HTML(http.StatusOK, tplForgotPassword)
return
@@ -79,7 +81,7 @@ func ForgotPasswdPost(ctx *context.Context) {
return
}
- if setting.CacheService.Enabled && ctx.Cache.IsExist("MailResendLimit_"+u.LowerName) {
+ if ctx.Cache.IsExist("MailResendLimit_" + u.LowerName) {
ctx.Data["ResendLimited"] = true
ctx.HTML(http.StatusOK, tplForgotPassword)
return
@@ -87,10 +89,8 @@ func ForgotPasswdPost(ctx *context.Context) {
mailer.SendResetPasswordMail(u)
- if setting.CacheService.Enabled {
- if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
- log.Error("Set cache(MailResendLimit) fail: %v", err)
- }
+ if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
+ log.Error("Set cache(MailResendLimit) fail: %v", err)
}
ctx.Data["ResetPwdCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, ctx.Locale)
@@ -167,30 +167,6 @@ func ResetPasswdPost(ctx *context.Context) {
return
}
- // Validate password length.
- passwd := ctx.FormString("password")
- if len(passwd) < setting.MinPasswordLength {
- ctx.Data["IsResetForm"] = true
- ctx.Data["Err_Password"] = true
- ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil)
- return
- } else if !password.IsComplexEnough(passwd) {
- ctx.Data["IsResetForm"] = true
- ctx.Data["Err_Password"] = true
- ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplResetPassword, nil)
- return
- } else if pwned, err := password.IsPwned(ctx, passwd); pwned || err != nil {
- errMsg := ctx.Tr("auth.password_pwned")
- if err != nil {
- log.Error(err.Error())
- errMsg = ctx.Tr("auth.password_pwned_err")
- }
- ctx.Data["IsResetForm"] = true
- ctx.Data["Err_Password"] = true
- ctx.RenderWithErr(errMsg, tplResetPassword, nil)
- return
- }
-
// Handle two-factor
regenerateScratchToken := false
if twofa != nil {
@@ -223,18 +199,27 @@ func ResetPasswdPost(ctx *context.Context) {
}
}
}
- var err error
- if u.Rands, err = user_model.GetUserSalt(); err != nil {
- ctx.ServerError("UpdateUser", err)
- return
- }
- if err = u.SetPassword(passwd); err != nil {
- ctx.ServerError("UpdateUser", err)
- return
+
+ opts := &user_service.UpdateAuthOptions{
+ Password: optional.Some(ctx.FormString("password")),
+ MustChangePassword: optional.Some(false),
}
- u.MustChangePassword = false
- if err := user_model.UpdateUserCols(ctx, u, "must_change_password", "passwd", "passwd_hash_algo", "rands", "salt"); err != nil {
- ctx.ServerError("UpdateUser", err)
+ if err := user_service.UpdateAuth(ctx, u, opts); err != nil {
+ ctx.Data["IsResetForm"] = true
+ ctx.Data["Err_Password"] = true
+ switch {
+ case errors.Is(err, password.ErrMinLength):
+ ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil)
+ case errors.Is(err, password.ErrComplexity):
+ ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplResetPassword, nil)
+ case errors.Is(err, password.ErrIsPwned):
+ ctx.RenderWithErr(ctx.Tr("auth.password_pwned"), tplResetPassword, nil)
+ case password.IsErrIsPwnedRequest(err):
+ log.Error("%s", err.Error())
+ ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplResetPassword, nil)
+ default:
+ ctx.ServerError("UpdateAuth", err)
+ }
return
}
@@ -244,7 +229,7 @@ func ResetPasswdPost(ctx *context.Context) {
if regenerateScratchToken {
// Invalidate the scratch token.
- _, err = twofa.GenerateScratchToken()
+ _, err := twofa.GenerateScratchToken()
if err != nil {
ctx.ServerError("UserSignIn", err)
return
@@ -284,11 +269,11 @@ func MustChangePasswordPost(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplMustChangePassword)
return
}
- u := ctx.Doer
+
// Make sure only requests for users who are eligible to change their password via
// this method passes through
- if !u.MustChangePassword {
- ctx.ServerError("MustUpdatePassword", errors.New("cannot update password.. Please visit the settings page"))
+ if !ctx.Doer.MustChangePassword {
+ ctx.ServerError("MustUpdatePassword", errors.New("cannot update password. Please visit the settings page"))
return
}
@@ -298,44 +283,34 @@ func MustChangePasswordPost(ctx *context.Context) {
return
}
- if len(form.Password) < setting.MinPasswordLength {
- ctx.Data["Err_Password"] = true
- ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form)
- return
- }
-
- if !password.IsComplexEnough(form.Password) {
- ctx.Data["Err_Password"] = true
- ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplMustChangePassword, &form)
- return
+ opts := &user_service.UpdateAuthOptions{
+ Password: optional.Some(form.Password),
+ MustChangePassword: optional.Some(false),
}
- pwned, err := password.IsPwned(ctx, form.Password)
- if pwned {
- ctx.Data["Err_Password"] = true
- errMsg := ctx.Tr("auth.password_pwned")
- if err != nil {
- log.Error(err.Error())
- errMsg = ctx.Tr("auth.password_pwned_err")
+ if err := user_service.UpdateAuth(ctx, ctx.Doer, opts); err != nil {
+ switch {
+ case errors.Is(err, password.ErrMinLength):
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form)
+ case errors.Is(err, password.ErrComplexity):
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplMustChangePassword, &form)
+ case errors.Is(err, password.ErrIsPwned):
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(ctx.Tr("auth.password_pwned"), tplMustChangePassword, &form)
+ case password.IsErrIsPwnedRequest(err):
+ log.Error("%s", err.Error())
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplMustChangePassword, &form)
+ default:
+ ctx.ServerError("UpdateAuth", err)
}
- ctx.RenderWithErr(errMsg, tplMustChangePassword, &form)
- return
- }
-
- if err = u.SetPassword(form.Password); err != nil {
- ctx.ServerError("UpdateUser", err)
- return
- }
-
- u.MustChangePassword = false
-
- if err := user_model.UpdateUserCols(ctx, u, "must_change_password", "passwd", "passwd_hash_algo", "salt"); err != nil {
- ctx.ServerError("UpdateUser", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.change_password_success"))
- log.Trace("User updated password: %s", u.Name)
+ log.Trace("User updated password: %s", ctx.Doer.Name)
if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
middleware.DeleteRedirectToCookie(ctx.Resp)
diff --git a/routers/web/auth/webauthn.go b/routers/web/auth/webauthn.go
index 95c8d262a53..1079f44a085 100644
--- a/routers/web/auth/webauthn.go
+++ b/routers/web/auth/webauthn.go
@@ -11,9 +11,9 @@ import (
user_model "code.gitea.io/gitea/models/user"
wa "code.gitea.io/gitea/modules/auth/webauthn"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/externalaccount"
"github.com/go-webauthn/webauthn/protocol"
diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go
index 525ca9be53c..dd20663f94b 100644
--- a/routers/web/devtest/devtest.go
+++ b/routers/web/devtest/devtest.go
@@ -10,8 +10,8 @@ import (
"time"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/services/context"
)
// List all devtest templates, they will be used for e2e tests for the UI components
diff --git a/routers/web/events/events.go b/routers/web/events/events.go
index 1a5a162c1a2..52f20e07dc3 100644
--- a/routers/web/events/events.go
+++ b/routers/web/events/events.go
@@ -7,11 +7,11 @@ import (
"net/http"
"time"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/eventsource"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/routers/web/auth"
+ "code.gitea.io/gitea/services/context"
)
// Events listens for events
diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go
index d81884ec62d..ecd7c33e016 100644
--- a/routers/web/explore/code.go
+++ b/routers/web/explore/code.go
@@ -6,11 +6,12 @@ package explore
import (
"net/http"
+ "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
code_indexer "code.gitea.io/gitea/modules/indexer/code"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -34,12 +35,11 @@ func Code(ctx *context.Context) {
language := ctx.FormTrim("l")
keyword := ctx.FormTrim("q")
- queryType := ctx.FormTrim("t")
- isMatch := queryType == "match"
+ isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true)
ctx.Data["Keyword"] = keyword
ctx.Data["Language"] = language
- ctx.Data["queryType"] = queryType
+ ctx.Data["IsFuzzy"] = isFuzzy
ctx.Data["PageIsViewCode"] = true
if keyword == "" {
@@ -77,7 +77,16 @@ func Code(ctx *context.Context) {
)
if (len(repoIDs) > 0) || isAdmin {
- total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
+ total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{
+ RepoIDs: repoIDs,
+ Keyword: keyword,
+ IsKeywordFuzzy: isFuzzy,
+ Language: language,
+ Paginator: &db.ListOptions{
+ Page: page,
+ PageSize: setting.UI.RepoSearchPagingNum,
+ },
+ })
if err != nil {
if code_indexer.IsAvailable(ctx) {
ctx.ServerError("SearchResults", err)
@@ -128,7 +137,7 @@ func Code(ctx *context.Context) {
pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
pager.SetDefaultParams(ctx)
- pager.AddParam(ctx, "l", "Language")
+ pager.AddParamString("l", language)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplExploreCode)
diff --git a/routers/web/explore/org.go b/routers/web/explore/org.go
index dc1318beefe..f8fd6ec38ef 100644
--- a/routers/web/explore/org.go
+++ b/routers/web/explore/org.go
@@ -6,9 +6,10 @@ package explore
import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/context"
)
// Organizations render explore organizations page
@@ -24,8 +25,16 @@ func Organizations(ctx *context.Context) {
visibleTypes = append(visibleTypes, structs.VisibleTypeLimited, structs.VisibleTypePrivate)
}
- if ctx.FormString("sort") == "" {
- ctx.SetFormString("sort", setting.UI.ExploreDefaultSort)
+ supportedSortOrders := container.SetOf(
+ "newest",
+ "oldest",
+ "alphabetically",
+ "reversealphabetically",
+ )
+ sortOrder := ctx.FormString("sort")
+ if sortOrder == "" {
+ sortOrder = "newest"
+ ctx.SetFormString("sort", sortOrder)
}
RenderUserSearch(ctx, &user_model.SearchUserOptions{
@@ -33,5 +42,7 @@ func Organizations(ctx *context.Context) {
Type: user_model.UserTypeOrganization,
ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum},
Visible: visibleTypes,
+
+ SupportedSortOrders: supportedSortOrders,
}, tplExploreUsers)
}
diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go
index 0446edebe69..66477a255cc 100644
--- a/routers/web/explore/repo.go
+++ b/routers/web/explore/repo.go
@@ -10,10 +10,10 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/sitemap"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -109,6 +109,21 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
language := ctx.FormTrim("language")
ctx.Data["Language"] = language
+ archived := ctx.FormOptionalBool("archived")
+ ctx.Data["IsArchived"] = archived
+
+ fork := ctx.FormOptionalBool("fork")
+ ctx.Data["IsFork"] = fork
+
+ mirror := ctx.FormOptionalBool("mirror")
+ ctx.Data["IsMirror"] = mirror
+
+ template := ctx.FormOptionalBool("template")
+ ctx.Data["IsTemplate"] = template
+
+ private := ctx.FormOptionalBool("private")
+ ctx.Data["IsPrivate"] = private
+
repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
Page: page,
@@ -125,6 +140,11 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
Language: language,
IncludeDescription: setting.UI.SearchRepoDescription,
OnlyShowRelevant: opts.OnlyShowRelevant,
+ Archived: archived,
+ Fork: fork,
+ Mirror: mirror,
+ Template: template,
+ IsPrivate: private,
})
if err != nil {
ctx.ServerError("SearchRepository", err)
@@ -149,8 +169,8 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
pager := context.NewPagination(int(count), opts.PageSize, page, 5)
pager.SetDefaultParams(ctx)
- pager.AddParam(ctx, "topic", "TopicOnly")
- pager.AddParam(ctx, "language", "Language")
+ pager.AddParamString("topic", fmt.Sprint(topicOnly))
+ pager.AddParamString("language", language)
pager.AddParamString(relevantReposOnlyParam, fmt.Sprint(opts.OnlyShowRelevant))
ctx.Data["Page"] = pager
diff --git a/routers/web/explore/topic.go b/routers/web/explore/topic.go
index bb1be310de7..95fecfe2b81 100644
--- a/routers/web/explore/topic.go
+++ b/routers/web/explore/topic.go
@@ -8,8 +8,8 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go
index 09d31f95ef4..b79a79fb2c8 100644
--- a/routers/web/explore/user.go
+++ b/routers/web/explore/user.go
@@ -10,12 +10,13 @@ import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/sitemap"
"code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -79,10 +80,16 @@ func RenderUserSearch(ctx *context.Context, opts *user_model.SearchUserOptions,
fallthrough
default:
// in case the sortType is not valid, we set it to recentupdate
+ sortOrder = "recentupdate"
ctx.Data["SortType"] = "recentupdate"
orderBy = "`user`.updated_unix DESC"
}
+ if opts.SupportedSortOrders != nil && !opts.SupportedSortOrders.Contains(sortOrder) {
+ ctx.NotFound("unsupported sort order", nil)
+ return
+ }
+
opts.Keyword = ctx.FormTrim("q")
opts.OrderBy = orderBy
if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
@@ -132,15 +139,25 @@ func Users(ctx *context.Context) {
ctx.Data["PageIsExploreUsers"] = true
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
- if ctx.FormString("sort") == "" {
- ctx.SetFormString("sort", setting.UI.ExploreDefaultSort)
+ supportedSortOrders := container.SetOf(
+ "newest",
+ "oldest",
+ "alphabetically",
+ "reversealphabetically",
+ )
+ sortOrder := ctx.FormString("sort")
+ if sortOrder == "" {
+ sortOrder = "newest"
+ ctx.SetFormString("sort", sortOrder)
}
RenderUserSearch(ctx, &user_model.SearchUserOptions{
Actor: ctx.Doer,
Type: user_model.UserTypeIndividual,
ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum},
- IsActive: util.OptionalBoolTrue,
+ IsActive: optional.Some(true),
Visible: []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate},
+
+ SupportedSortOrders: supportedSortOrders,
}, tplExploreUsers)
}
diff --git a/routers/web/feed/branch.go b/routers/web/feed/branch.go
index f13038ff9b4..80ce2ad198b 100644
--- a/routers/web/feed/branch.go
+++ b/routers/web/feed/branch.go
@@ -9,7 +9,7 @@ import (
"time"
"code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
"github.com/gorilla/feeds"
)
diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go
index 4d4918a8fd8..3defa436a70 100644
--- a/routers/web/feed/convert.go
+++ b/routers/web/feed/convert.go
@@ -6,6 +6,7 @@ package feed
import (
"fmt"
"html"
+ "html/template"
"net/http"
"net/url"
"strconv"
@@ -13,12 +14,12 @@ import (
activities_model "code.gitea.io/gitea/models/activities"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
"github.com/gorilla/feeds"
)
@@ -49,11 +50,13 @@ func toReleaseLink(ctx *context.Context, act *activities_model.Action) string {
// renderMarkdown creates a minimal markdown render context from an action.
// If rendering fails, the original markdown text is returned
-func renderMarkdown(ctx *context.Context, act *activities_model.Action, content string) string {
+func renderMarkdown(ctx *context.Context, act *activities_model.Action, content string) template.HTML {
markdownCtx := &markup.RenderContext{
- Ctx: ctx,
- URLPrefix: act.GetRepoLink(ctx),
- Type: markdown.MarkupName,
+ Ctx: ctx,
+ Links: markup.Links{
+ Base: act.GetRepoLink(ctx),
+ },
+ Type: markdown.MarkupName,
Metas: map[string]string{
"user": act.GetRepoUserName(ctx),
"repo": act.GetRepoName(ctx),
@@ -61,7 +64,7 @@ func renderMarkdown(ctx *context.Context, act *activities_model.Action, content
}
markdown, err := markdown.RenderString(markdownCtx, content)
if err != nil {
- return content
+ return templates.SanitizeHTML(content) // old code did so: use SanitizeHTML to render in tmpl
}
return markdown
}
@@ -71,125 +74,130 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
for _, act := range actions {
act.LoadActUser(ctx)
- var content, desc, title string
+ // TODO: the code seems quite strange (maybe not right)
+ // sometimes it uses text content but sometimes it uses HTML content
+ // it should clearly defines which kind of content it should use for the feed items: plan text or rich HTML
+ var title, desc string
+ var content template.HTML
link := &feeds.Link{Href: act.GetCommentHTMLURL(ctx)}
// title
title = act.ActUser.DisplayName() + " "
+ var titleExtra template.HTML
switch act.OpType {
case activities_model.ActionCreateRepo:
- title += ctx.TrHTMLEscapeArgs("action.create_repo", act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.create_repo", act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
link.Href = act.GetRepoAbsoluteLink(ctx)
case activities_model.ActionRenameRepo:
- title += ctx.TrHTMLEscapeArgs("action.rename_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.rename_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
link.Href = act.GetRepoAbsoluteLink(ctx)
case activities_model.ActionCommitRepo:
link.Href = toBranchLink(ctx, act)
if len(act.Content) != 0 {
- title += ctx.TrHTMLEscapeArgs("action.commit_repo", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.commit_repo", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx))
} else {
- title += ctx.TrHTMLEscapeArgs("action.create_branch", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.create_branch", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx))
}
case activities_model.ActionCreateIssue:
link.Href = toIssueLink(ctx, act)
- title += ctx.TrHTMLEscapeArgs("action.create_issue", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.create_issue", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionCreatePullRequest:
link.Href = toPullLink(ctx, act)
- title += ctx.TrHTMLEscapeArgs("action.create_pull_request", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.create_pull_request", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionTransferRepo:
link.Href = act.GetRepoAbsoluteLink(ctx)
- title += ctx.TrHTMLEscapeArgs("action.transfer_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.transfer_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
case activities_model.ActionPushTag:
link.Href = toTagLink(ctx, act)
- title += ctx.TrHTMLEscapeArgs("action.push_tag", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetTag(), act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.push_tag", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetTag(), act.ShortRepoPath(ctx))
case activities_model.ActionCommentIssue:
issueLink := toIssueLink(ctx, act)
if link.Href == "#" {
link.Href = issueLink
}
- title += ctx.TrHTMLEscapeArgs("action.comment_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.comment_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionMergePullRequest:
pullLink := toPullLink(ctx, act)
if link.Href == "#" {
link.Href = pullLink
}
- title += ctx.TrHTMLEscapeArgs("action.merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionAutoMergePullRequest:
pullLink := toPullLink(ctx, act)
if link.Href == "#" {
link.Href = pullLink
}
- title += ctx.TrHTMLEscapeArgs("action.auto_merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.auto_merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionCloseIssue:
issueLink := toIssueLink(ctx, act)
if link.Href == "#" {
link.Href = issueLink
}
- title += ctx.TrHTMLEscapeArgs("action.close_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.close_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionReopenIssue:
issueLink := toIssueLink(ctx, act)
if link.Href == "#" {
link.Href = issueLink
}
- title += ctx.TrHTMLEscapeArgs("action.reopen_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.reopen_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionClosePullRequest:
pullLink := toPullLink(ctx, act)
if link.Href == "#" {
link.Href = pullLink
}
- title += ctx.TrHTMLEscapeArgs("action.close_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.close_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionReopenPullRequest:
pullLink := toPullLink(ctx, act)
if link.Href == "#" {
link.Href = pullLink
}
- title += ctx.TrHTMLEscapeArgs("action.reopen_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.reopen_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionDeleteTag:
link.Href = act.GetRepoAbsoluteLink(ctx)
- title += ctx.TrHTMLEscapeArgs("action.delete_tag", act.GetRepoAbsoluteLink(ctx), act.GetTag(), act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.delete_tag", act.GetRepoAbsoluteLink(ctx), act.GetTag(), act.ShortRepoPath(ctx))
case activities_model.ActionDeleteBranch:
link.Href = act.GetRepoAbsoluteLink(ctx)
- title += ctx.TrHTMLEscapeArgs("action.delete_branch", act.GetRepoAbsoluteLink(ctx), html.EscapeString(act.GetBranch()), act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.delete_branch", act.GetRepoAbsoluteLink(ctx), html.EscapeString(act.GetBranch()), act.ShortRepoPath(ctx))
case activities_model.ActionMirrorSyncPush:
srcLink := toSrcLink(ctx, act)
if link.Href == "#" {
link.Href = srcLink
}
- title += ctx.TrHTMLEscapeArgs("action.mirror_sync_push", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.mirror_sync_push", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
case activities_model.ActionMirrorSyncCreate:
srcLink := toSrcLink(ctx, act)
if link.Href == "#" {
link.Href = srcLink
}
- title += ctx.TrHTMLEscapeArgs("action.mirror_sync_create", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.mirror_sync_create", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
case activities_model.ActionMirrorSyncDelete:
link.Href = act.GetRepoAbsoluteLink(ctx)
- title += ctx.TrHTMLEscapeArgs("action.mirror_sync_delete", act.GetRepoAbsoluteLink(ctx), act.GetBranch(), act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.mirror_sync_delete", act.GetRepoAbsoluteLink(ctx), act.GetBranch(), act.ShortRepoPath(ctx))
case activities_model.ActionApprovePullRequest:
pullLink := toPullLink(ctx, act)
- title += ctx.TrHTMLEscapeArgs("action.approve_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.approve_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionRejectPullRequest:
pullLink := toPullLink(ctx, act)
- title += ctx.TrHTMLEscapeArgs("action.reject_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.reject_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionCommentPull:
pullLink := toPullLink(ctx, act)
- title += ctx.TrHTMLEscapeArgs("action.comment_pull", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.comment_pull", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionPublishRelease:
releaseLink := toReleaseLink(ctx, act)
if link.Href == "#" {
link.Href = releaseLink
}
- title += ctx.TrHTMLEscapeArgs("action.publish_release", act.GetRepoAbsoluteLink(ctx), releaseLink, act.ShortRepoPath(ctx), act.Content)
+ titleExtra = ctx.Locale.Tr("action.publish_release", act.GetRepoAbsoluteLink(ctx), releaseLink, act.ShortRepoPath(ctx), act.Content)
case activities_model.ActionPullReviewDismissed:
pullLink := toPullLink(ctx, act)
- title += ctx.TrHTMLEscapeArgs("action.review_dismissed", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx), act.GetIssueInfos()[1])
+ titleExtra = ctx.Locale.Tr("action.review_dismissed", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx), act.GetIssueInfos()[1])
case activities_model.ActionStarRepo:
link.Href = act.GetRepoAbsoluteLink(ctx)
- title += ctx.TrHTMLEscapeArgs("action.starred_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.starred_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx))
case activities_model.ActionWatchRepo:
link.Href = act.GetRepoAbsoluteLink(ctx)
- title += ctx.TrHTMLEscapeArgs("action.watched_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.watched_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx))
default:
return nil, fmt.Errorf("unknown action type: %v", act.OpType)
}
@@ -199,7 +207,6 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
switch act.OpType {
case activities_model.ActionCommitRepo, activities_model.ActionMirrorSyncPush:
push := templates.ActionContent2Commits(act)
- repoLink := act.GetRepoAbsoluteLink(ctx)
for _, commit := range push.Commits {
if len(desc) != 0 {
@@ -208,7 +215,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
desc += fmt.Sprintf("%s\n%s",
html.EscapeString(fmt.Sprintf("%s/commit/%s", act.GetRepoAbsoluteLink(ctx), commit.Sha1)),
commit.Sha1,
- templates.RenderCommitMessage(ctx, commit.Message, repoLink, nil),
+ templates.RenderCommitMessage(ctx, commit.Message, nil),
)
}
@@ -225,31 +232,32 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
desc = act.GetIssueTitle(ctx)
comment := act.GetIssueInfos()[1]
if len(comment) != 0 {
- desc += "\n\n" + renderMarkdown(ctx, act, comment)
+ desc += "\n\n" + string(renderMarkdown(ctx, act, comment))
}
case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest:
desc = act.GetIssueInfos()[1]
case activities_model.ActionCloseIssue, activities_model.ActionReopenIssue, activities_model.ActionClosePullRequest, activities_model.ActionReopenPullRequest:
desc = act.GetIssueTitle(ctx)
case activities_model.ActionPullReviewDismissed:
- desc = ctx.Tr("action.review_dismissed_reason") + "\n\n" + act.GetIssueInfos()[2]
+ desc = ctx.Locale.TrString("action.review_dismissed_reason") + "\n\n" + act.GetIssueInfos()[2]
}
}
if len(content) == 0 {
- content = desc
+ content = templates.SanitizeHTML(desc)
}
items = append(items, &feeds.Item{
- Title: title,
+ Title: template.HTMLEscapeString(title) + string(titleExtra),
Link: link,
Description: desc,
+ IsPermaLink: "false",
Author: &feeds.Author{
Name: act.ActUser.DisplayName(),
Email: act.ActUser.GetEmail(),
},
Id: fmt.Sprintf("%v: %v", strconv.FormatInt(act.ID, 10), link.Href),
Created: act.CreatedUnix.AsTime(),
- Content: content,
+ Content: string(content),
})
}
return items, err
@@ -278,7 +286,8 @@ func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release, i
return nil, err
}
- var title, content string
+ var title string
+ var content template.HTML
if rel.IsTag {
title = rel.TagName
@@ -288,11 +297,12 @@ func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release, i
link := &feeds.Link{Href: rel.HTMLURL()}
content, err = markdown.RenderString(&markup.RenderContext{
- Ctx: ctx,
- URLPrefix: rel.Repo.Link(),
- Metas: rel.Repo.ComposeMetas(ctx),
+ Ctx: ctx,
+ Links: markup.Links{
+ Base: rel.Repo.Link(),
+ },
+ Metas: rel.Repo.ComposeMetas(ctx),
}, rel.Note)
-
if err != nil {
return nil, err
}
@@ -306,7 +316,7 @@ func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release, i
Email: rel.Publisher.GetEmail(),
},
Id: fmt.Sprintf("%v: %v", strconv.FormatInt(rel.ID, 10), link.Href),
- Content: content,
+ Content: string(content),
})
}
diff --git a/routers/web/feed/file.go b/routers/web/feed/file.go
index 56a9c54ddcb..1ab768ff27f 100644
--- a/routers/web/feed/file.go
+++ b/routers/web/feed/file.go
@@ -9,9 +9,9 @@ import (
"time"
"code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
"github.com/gorilla/feeds"
)
diff --git a/routers/web/feed/profile.go b/routers/web/feed/profile.go
index ce86727e248..08cbcd9e12b 100644
--- a/routers/web/feed/profile.go
+++ b/routers/web/feed/profile.go
@@ -7,9 +7,9 @@ import (
"time"
activities_model "code.gitea.io/gitea/models/activities"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
+ "code.gitea.io/gitea/services/context"
"github.com/gorilla/feeds"
)
@@ -42,8 +42,10 @@ func showUserFeed(ctx *context.Context, formatType string) {
}
ctxUserDescription, err := markdown.RenderString(&markup.RenderContext{
- Ctx: ctx,
- URLPrefix: ctx.ContextUser.HTMLURL(),
+ Ctx: ctx,
+ Links: markup.Links{
+ Base: ctx.ContextUser.HTMLURL(),
+ },
Metas: map[string]string{
"user": ctx.ContextUser.GetDisplayName(),
},
@@ -54,9 +56,9 @@ func showUserFeed(ctx *context.Context, formatType string) {
}
feed := &feeds.Feed{
- Title: ctx.Tr("home.feed_of", ctx.ContextUser.DisplayName()),
+ Title: ctx.Locale.TrString("home.feed_of", ctx.ContextUser.DisplayName()),
Link: &feeds.Link{Href: ctx.ContextUser.HTMLURL()},
- Description: ctxUserDescription,
+ Description: string(ctxUserDescription),
Created: time.Now(),
}
diff --git a/routers/web/feed/release.go b/routers/web/feed/release.go
index fbfa11c63ec..273f47e3b46 100644
--- a/routers/web/feed/release.go
+++ b/routers/web/feed/release.go
@@ -6,16 +6,18 @@ package feed
import (
"time"
+ "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
"github.com/gorilla/feeds"
)
// shows tags and/or releases on the repo as RSS / Atom feed
func ShowReleaseFeed(ctx *context.Context, repo *repo_model.Repository, isReleasesOnly bool, formatType string) {
- releases, err := repo_model.GetReleasesByRepoID(ctx, ctx.Repo.Repository.ID, repo_model.FindReleasesOptions{
+ releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
IncludeTags: !isReleasesOnly,
+ RepoID: ctx.Repo.Repository.ID,
})
if err != nil {
ctx.ServerError("GetReleasesByRepoID", err)
@@ -26,10 +28,10 @@ func ShowReleaseFeed(ctx *context.Context, repo *repo_model.Repository, isReleas
var link *feeds.Link
if isReleasesOnly {
- title = ctx.Tr("repo.release.releases_for", repo.FullName())
+ title = ctx.Locale.TrString("repo.release.releases_for", repo.FullName())
link = &feeds.Link{Href: repo.HTMLURL() + "/release"}
} else {
- title = ctx.Tr("repo.release.tags_for", repo.FullName())
+ title = ctx.Locale.TrString("repo.release.tags_for", repo.FullName())
link = &feeds.Link{Href: repo.HTMLURL() + "/tags"}
}
diff --git a/routers/web/feed/render.go b/routers/web/feed/render.go
index 8931dae8cce..a41808c24ac 100644
--- a/routers/web/feed/render.go
+++ b/routers/web/feed/render.go
@@ -4,7 +4,7 @@
package feed
import (
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
)
// RenderBranchFeed render format for branch or file
diff --git a/routers/web/feed/repo.go b/routers/web/feed/repo.go
index 5fcad267791..bfcc3a37d6a 100644
--- a/routers/web/feed/repo.go
+++ b/routers/web/feed/repo.go
@@ -8,7 +8,7 @@ import (
activities_model "code.gitea.io/gitea/models/activities"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
"github.com/gorilla/feeds"
)
@@ -27,7 +27,7 @@ func ShowRepoFeed(ctx *context.Context, repo *repo_model.Repository, formatType
}
feed := &feeds.Feed{
- Title: ctx.Tr("home.feed_of", repo.FullName()),
+ Title: ctx.Locale.TrString("home.feed_of", repo.FullName()),
Link: &feeds.Link{Href: repo.HTMLURL()},
Description: repo.Description,
Created: time.Now(),
diff --git a/routers/web/githttp.go b/routers/web/githttp.go
index b2fb5b472f7..5f1dedce763 100644
--- a/routers/web/githttp.go
+++ b/routers/web/githttp.go
@@ -6,11 +6,10 @@ package web
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/repo"
- context_service "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/context"
)
func requireSignIn(ctx *context.Context) {
@@ -28,16 +27,16 @@ func requireSignIn(ctx *context.Context) {
func gitHTTPRouters(m *web.Route) {
m.Group("", func() {
- m.PostOptions("/git-upload-pack", repo.ServiceUploadPack)
- m.PostOptions("/git-receive-pack", repo.ServiceReceivePack)
- m.GetOptions("/info/refs", repo.GetInfoRefs)
- m.GetOptions("/HEAD", repo.GetTextFile("HEAD"))
- m.GetOptions("/objects/info/alternates", repo.GetTextFile("objects/info/alternates"))
- m.GetOptions("/objects/info/http-alternates", repo.GetTextFile("objects/info/http-alternates"))
- m.GetOptions("/objects/info/packs", repo.GetInfoPacks)
- m.GetOptions("/objects/info/{file:[^/]*}", repo.GetTextFile(""))
- m.GetOptions("/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38}}", repo.GetLooseObject)
- m.GetOptions("/objects/pack/pack-{file:[0-9a-f]{40}}.pack", repo.GetPackFile)
- m.GetOptions("/objects/pack/pack-{file:[0-9a-f]{40}}.idx", repo.GetIdxFile)
- }, ignSignInAndCsrf, requireSignIn, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context_service.UserAssignmentWeb())
+ m.Methods("POST,OPTIONS", "/git-upload-pack", repo.ServiceUploadPack)
+ m.Methods("POST,OPTIONS", "/git-receive-pack", repo.ServiceReceivePack)
+ m.Methods("GET,OPTIONS", "/info/refs", repo.GetInfoRefs)
+ m.Methods("GET,OPTIONS", "/HEAD", repo.GetTextFile("HEAD"))
+ m.Methods("GET,OPTIONS", "/objects/info/alternates", repo.GetTextFile("objects/info/alternates"))
+ m.Methods("GET,OPTIONS", "/objects/info/http-alternates", repo.GetTextFile("objects/info/http-alternates"))
+ m.Methods("GET,OPTIONS", "/objects/info/packs", repo.GetInfoPacks)
+ m.Methods("GET,OPTIONS", "/objects/info/{file:[^/]*}", repo.GetTextFile(""))
+ m.Methods("GET,OPTIONS", "/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38,62}}", repo.GetLooseObject)
+ m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.pack", repo.GetPackFile)
+ m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile)
+ }, ignSignInAndCsrf, requireSignIn, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb())
}
diff --git a/routers/web/goget.go b/routers/web/goget.go
index c5b8b6cbc01..8d5612ebfe9 100644
--- a/routers/web/goget.go
+++ b/routers/web/goget.go
@@ -12,9 +12,9 @@ import (
"strings"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
)
func goGet(ctx *context.Context) {
diff --git a/routers/web/healthcheck/check.go b/routers/web/healthcheck/check.go
index ecb73a928fd..85f47613f0d 100644
--- a/routers/web/healthcheck/check.go
+++ b/routers/web/healthcheck/check.go
@@ -121,10 +121,6 @@ func checkDatabase(ctx context.Context, checks checks) status {
// cache checks gitea cache status
func checkCache(checks checks) status {
- if !setting.CacheService.Enabled {
- return pass
- }
-
st := componentStatus{}
if err := cache.GetCache().Ping(); err != nil {
st.Status = fail
diff --git a/routers/web/home.go b/routers/web/home.go
index 2321b00efe7..d4be0931e85 100644
--- a/routers/web/home.go
+++ b/routers/web/home.go
@@ -12,15 +12,15 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/sitemap"
"code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/routers/web/auth"
"code.gitea.io/gitea/routers/web/user"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -71,7 +71,7 @@ func HomeSitemap(ctx *context.Context) {
_, cnt, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{
Type: user_model.UserTypeIndividual,
ListOptions: db.ListOptions{PageSize: 1},
- IsActive: util.OptionalBoolTrue,
+ IsActive: optional.Some(true),
Visible: []structs.VisibleType{structs.VisibleTypePublic},
})
if err != nil {
diff --git a/routers/web/misc/markup.go b/routers/web/misc/markup.go
index c91da9a7f10..2dbbd6fc097 100644
--- a/routers/web/misc/markup.go
+++ b/routers/web/misc/markup.go
@@ -5,10 +5,10 @@
package misc
import (
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/common"
+ "code.gitea.io/gitea/services/context"
)
// Markup render markup document to HTML
diff --git a/routers/web/misc/misc.go b/routers/web/misc/misc.go
index 54c93763f6a..ac5496ce91a 100644
--- a/routers/web/misc/misc.go
+++ b/routers/web/misc/misc.go
@@ -15,7 +15,7 @@ import (
)
func SSHInfo(rw http.ResponseWriter, req *http.Request) {
- if !git.SupportProcReceive {
+ if !git.DefaultFeatures.SupportProcReceive {
rw.WriteHeader(http.StatusNotFound)
return
}
diff --git a/routers/web/misc/swagger.go b/routers/web/misc/swagger.go
index 72c09a37801..5fddfa8885f 100644
--- a/routers/web/misc/swagger.go
+++ b/routers/web/misc/swagger.go
@@ -7,7 +7,7 @@ import (
"net/http"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
)
// tplSwagger swagger page template
diff --git a/routers/web/nodeinfo.go b/routers/web/nodeinfo.go
index 01b71e7086b..f1cc7bf530f 100644
--- a/routers/web/nodeinfo.go
+++ b/routers/web/nodeinfo.go
@@ -7,8 +7,8 @@ import (
"fmt"
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
)
type nodeInfoLinks struct {
diff --git a/routers/web/org/block.go b/routers/web/org/block.go
new file mode 100644
index 00000000000..d40458e2506
--- /dev/null
+++ b/routers/web/org/block.go
@@ -0,0 +1,38 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/modules/base"
+ shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
+)
+
+const (
+ tplSettingsBlockedUsers base.TplName = "org/settings/blocked_users"
+)
+
+func BlockedUsers(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("user.block.list")
+ ctx.Data["PageIsOrgSettings"] = true
+ ctx.Data["PageIsSettingsBlockedUsers"] = true
+
+ shared_user.BlockedUsers(ctx, ctx.ContextUser)
+ if ctx.Written() {
+ return
+ }
+
+ ctx.HTML(http.StatusOK, tplSettingsBlockedUsers)
+}
+
+func BlockedUsersPost(ctx *context.Context) {
+ shared_user.BlockedUsersPost(ctx, ctx.ContextUser)
+ if ctx.Written() {
+ return
+ }
+
+ ctx.Redirect(ctx.ContextUser.OrganisationLink() + "/settings/blocked_users")
+}
diff --git a/routers/web/org/home.go b/routers/web/org/home.go
index a9adfdc03cd..846b1de18ab 100644
--- a/routers/web/org/home.go
+++ b/routers/web/org/home.go
@@ -5,20 +5,21 @@ package org
import (
"net/http"
+ "path"
"strings"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -44,19 +45,6 @@ func Home(ctx *context.Context) {
ctx.Data["PageIsUserProfile"] = true
ctx.Data["Title"] = org.DisplayName()
- if len(org.Description) != 0 {
- desc, err := markdown.RenderString(&markup.RenderContext{
- Ctx: ctx,
- URLPrefix: ctx.Repo.RepoLink,
- Metas: map[string]string{"mode": "document"},
- GitRepo: ctx.Repo.GitRepo,
- }, org.Description)
- if err != nil {
- ctx.ServerError("RenderString", err)
- return
- }
- ctx.Data["RenderedDescription"] = desc
- }
var orderBy db.SearchOrderBy
ctx.Data["SortType"] = ctx.FormString("sort")
@@ -97,6 +85,21 @@ func Home(ctx *context.Context) {
page = 1
}
+ archived := ctx.FormOptionalBool("archived")
+ ctx.Data["IsArchived"] = archived
+
+ fork := ctx.FormOptionalBool("fork")
+ ctx.Data["IsFork"] = fork
+
+ mirror := ctx.FormOptionalBool("mirror")
+ ctx.Data["IsMirror"] = mirror
+
+ template := ctx.FormOptionalBool("template")
+ ctx.Data["IsTemplate"] = template
+
+ private := ctx.FormOptionalBool("private")
+ ctx.Data["IsPrivate"] = private
+
var (
repos []*repo_model.Repository
count int64
@@ -114,6 +117,11 @@ func Home(ctx *context.Context) {
Actor: ctx.Doer,
Language: language,
IncludeDescription: setting.UI.SearchRepoDescription,
+ Archived: archived,
+ Fork: fork,
+ Mirror: mirror,
+ Template: template,
+ IsPrivate: private,
})
if err != nil {
ctx.ServerError("SearchRepository", err)
@@ -131,18 +139,12 @@ func Home(ctx *context.Context) {
return
}
- var isFollowing bool
- if ctx.Doer != nil {
- isFollowing = user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
- }
-
ctx.Data["Repos"] = repos
ctx.Data["Total"] = count
ctx.Data["Members"] = members
ctx.Data["Teams"] = ctx.Org.Teams
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
ctx.Data["PageIsViewRepositories"] = true
- ctx.Data["IsFollowing"] = isFollowing
err = shared_user.LoadHeaderCount(ctx)
if err != nil {
@@ -152,19 +154,19 @@ func Home(ctx *context.Context) {
pager := context.NewPagination(int(count), setting.UI.User.RepoPagingNum, page, 5)
pager.SetDefaultParams(ctx)
- pager.AddParam(ctx, "language", "Language")
+ pager.AddParamString("language", language)
ctx.Data["Page"] = pager
ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0
- profileGitRepo, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer)
+ profileDbRepo, profileGitRepo, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer)
defer profileClose()
- prepareOrgProfileReadme(ctx, profileGitRepo, profileReadmeBlob)
+ prepareOrgProfileReadme(ctx, profileGitRepo, profileDbRepo, profileReadmeBlob)
ctx.HTML(http.StatusOK, tplOrgHome)
}
-func prepareOrgProfileReadme(ctx *context.Context, profileGitRepo *git.Repository, profileReadme *git.Blob) {
+func prepareOrgProfileReadme(ctx *context.Context, profileGitRepo *git.Repository, profileDbRepo *repo_model.Repository, profileReadme *git.Blob) {
if profileGitRepo == nil || profileReadme == nil {
return
}
@@ -175,7 +177,13 @@ func prepareOrgProfileReadme(ctx *context.Context, profileGitRepo *git.Repositor
if profileContent, err := markdown.RenderString(&markup.RenderContext{
Ctx: ctx,
GitRepo: profileGitRepo,
- Metas: map[string]string{"mode": "document"},
+ Links: markup.Links{
+ // Pass repo link to markdown render for the full link of media elements.
+ // The profile of default branch would be shown.
+ Base: profileDbRepo.Link(),
+ BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
+ },
+ Metas: map[string]string{"mode": "document"},
}, bytes); err != nil {
log.Error("failed to RenderString: %v", err)
} else {
diff --git a/routers/web/org/members.go b/routers/web/org/members.go
index 15a615c706f..63ac57cf0dc 100644
--- a/routers/web/org/members.go
+++ b/routers/web/org/members.go
@@ -9,11 +9,12 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/organization"
+ user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -78,40 +79,43 @@ func Members(ctx *context.Context) {
// MembersAction response for operation to a member of organization
func MembersAction(ctx *context.Context) {
- uid := ctx.FormInt64("uid")
- if uid == 0 {
+ member, err := user_model.GetUserByID(ctx, ctx.FormInt64("uid"))
+ if err != nil {
+ log.Error("GetUserByID: %v", err)
+ }
+ if member == nil {
ctx.Redirect(ctx.Org.OrgLink + "/members")
return
}
org := ctx.Org.Organization
- var err error
+
switch ctx.Params(":action") {
case "private":
- if ctx.Doer.ID != uid && !ctx.Org.IsOwner {
+ if ctx.Doer.ID != member.ID && !ctx.Org.IsOwner {
ctx.Error(http.StatusNotFound)
return
}
- err = organization.ChangeOrgUserStatus(ctx, org.ID, uid, false)
+ err = organization.ChangeOrgUserStatus(ctx, org.ID, member.ID, false)
case "public":
- if ctx.Doer.ID != uid && !ctx.Org.IsOwner {
+ if ctx.Doer.ID != member.ID && !ctx.Org.IsOwner {
ctx.Error(http.StatusNotFound)
return
}
- err = organization.ChangeOrgUserStatus(ctx, org.ID, uid, true)
+ err = organization.ChangeOrgUserStatus(ctx, org.ID, member.ID, true)
case "remove":
if !ctx.Org.IsOwner {
ctx.Error(http.StatusNotFound)
return
}
- err = models.RemoveOrgUser(ctx, org.ID, uid)
+ err = models.RemoveOrgUser(ctx, org, member)
if organization.IsErrLastOrgOwner(err) {
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
ctx.JSONRedirect(ctx.Org.OrgLink + "/members")
return
}
case "leave":
- err = models.RemoveOrgUser(ctx, org.ID, ctx.Doer.ID)
+ err = models.RemoveOrgUser(ctx, org, ctx.Doer)
if err == nil {
ctx.Flash.Success(ctx.Tr("form.organization_leave_success", org.DisplayName()))
ctx.JSON(http.StatusOK, map[string]any{
diff --git a/routers/web/org/org.go b/routers/web/org/org.go
index 52f8df8a1c7..f94dd16eaef 100644
--- a/routers/web/org/org.go
+++ b/routers/web/org/org.go
@@ -12,10 +12,10 @@ import (
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
@@ -29,7 +29,7 @@ func Create(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("new_org")
ctx.Data["DefaultOrgVisibilityMode"] = setting.Service.DefaultOrgVisibilityMode
if !ctx.Doer.CanCreateOrganization() {
- ctx.ServerError("Not allowed", errors.New(ctx.Tr("org.form.create_org_not_allowed")))
+ ctx.ServerError("Not allowed", errors.New(ctx.Locale.TrString("org.form.create_org_not_allowed")))
return
}
ctx.HTML(http.StatusOK, tplCreateOrg)
@@ -41,7 +41,7 @@ func CreatePost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("new_org")
if !ctx.Doer.CanCreateOrganization() {
- ctx.ServerError("Not allowed", errors.New(ctx.Tr("org.form.create_org_not_allowed")))
+ ctx.ServerError("Not allowed", errors.New(ctx.Locale.TrString("org.form.create_org_not_allowed")))
return
}
diff --git a/routers/web/org/org_labels.go b/routers/web/org/org_labels.go
index f78bd002742..02eae8052ea 100644
--- a/routers/web/org/org_labels.go
+++ b/routers/web/org/org_labels.go
@@ -8,10 +8,10 @@ import (
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/label"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go
index 439fdf644bb..928676a52b5 100644
--- a/routers/web/org/projects.go
+++ b/routers/web/org/projects.go
@@ -11,17 +11,19 @@ import (
"strconv"
"strings"
+ "code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
project_model "code.gitea.io/gitea/models/project"
attachment_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
@@ -59,10 +61,13 @@ func Projects(ctx *context.Context) {
} else {
projectType = project_model.TypeIndividual
}
- projects, total, err := project_model.FindProjects(ctx, project_model.SearchOptions{
+ projects, total, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{
+ ListOptions: db.ListOptions{
+ Page: page,
+ PageSize: setting.UI.IssuePagingNum,
+ },
OwnerID: ctx.ContextUser.ID,
- Page: page,
- IsClosed: util.OptionalBoolOf(isShowClosed),
+ IsClosed: optional.Some(isShowClosed),
OrderBy: project_model.GetSearchOrderByBySortType(sortType),
Type: projectType,
Title: keyword,
@@ -72,9 +77,9 @@ func Projects(ctx *context.Context) {
return
}
- opTotal, err := project_model.CountProjects(ctx, project_model.SearchOptions{
+ opTotal, err := db.Count[project_model.Project](ctx, project_model.SearchOptions{
OwnerID: ctx.ContextUser.ID,
- IsClosed: util.OptionalBoolOf(!isShowClosed),
+ IsClosed: optional.Some(!isShowClosed),
Type: projectType,
})
if err != nil {
@@ -100,7 +105,7 @@ func Projects(ctx *context.Context) {
}
for _, project := range projects {
- project.RenderedContent = project.Description
+ project.RenderedContent = templates.SanitizeHTML(project.Description) // FIXME: is it right? why not render?
}
err = shared_user.LoadHeaderCount(ctx)
@@ -115,7 +120,7 @@ func Projects(ctx *context.Context) {
}
pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, numPages)
- pager.AddParam(ctx, "state", "State")
+ pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
ctx.Data["Page"] = pager
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
@@ -349,7 +354,7 @@ func ViewProject(ctx *context.Context) {
}
if boards[0].ID == 0 {
- boards[0].Title = ctx.Tr("repo.projects.type.uncategorized")
+ boards[0].Title = ctx.Locale.TrString("repo.projects.type.uncategorized")
}
issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
@@ -373,17 +378,17 @@ func ViewProject(ctx *context.Context) {
linkedPrsMap := make(map[int64][]*issues_model.Issue)
for _, issuesList := range issuesMap {
for _, issue := range issuesList {
- var referencedIds []int64
+ var referencedIDs []int64
for _, comment := range issue.Comments {
if comment.RefIssueID != 0 && comment.RefIsPull {
- referencedIds = append(referencedIds, comment.RefIssueID)
+ referencedIDs = append(referencedIDs, comment.RefIssueID)
}
}
- if len(referencedIds) > 0 {
+ if len(referencedIDs) > 0 {
if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
- IssueIDs: referencedIds,
- IsPull: util.OptionalBoolTrue,
+ IssueIDs: referencedIDs,
+ IsPull: optional.Some(true),
}); err == nil {
linkedPrsMap[issue.ID] = linkedPrs
}
@@ -391,7 +396,7 @@ func ViewProject(ctx *context.Context) {
}
}
- project.RenderedContent = project.Description
+ project.RenderedContent = templates.SanitizeHTML(project.Description) // FIXME: is it right? why not render?
ctx.Data["LinkedPRs"] = linkedPrsMap
ctx.Data["PageIsViewProjects"] = true
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
@@ -675,7 +680,7 @@ func MoveIssues(ctx *context.Context) {
board = &project_model.Board{
ID: 0,
ProjectID: project.ID,
- Title: ctx.Tr("repo.projects.type.uncategorized"),
+ Title: ctx.Locale.TrString("repo.projects.type.uncategorized"),
}
} else {
board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
diff --git a/routers/web/org/projects_test.go b/routers/web/org/projects_test.go
index 8053ab4cf93..f4ccfe1c066 100644
--- a/routers/web/org/projects_test.go
+++ b/routers/web/org/projects_test.go
@@ -7,8 +7,8 @@ import (
"testing"
"code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/contexttest"
"code.gitea.io/gitea/routers/web/org"
+ "code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
)
diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go
index fac83b36128..494ada4323a 100644
--- a/routers/web/org/setting.go
+++ b/routers/web/org/setting.go
@@ -7,7 +7,6 @@ package org
import (
"net/http"
"net/url"
- "strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
@@ -15,13 +14,14 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
user_setting "code.gitea.io/gitea/routers/web/user/setting"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
org_service "code.gitea.io/gitea/services/org"
repo_service "code.gitea.io/gitea/services/repository"
@@ -71,53 +71,50 @@ func SettingsPost(ctx *context.Context) {
}
org := ctx.Org.Organization
- nameChanged := org.Name != form.Name
-
- // Check if organization name has been changed.
- if nameChanged {
- err := user_service.RenameUser(ctx, org.AsUser(), form.Name)
- switch {
- case user_model.IsErrUserAlreadyExist(err):
- ctx.Data["OrgName"] = true
- ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form)
- return
- case db.IsErrNameReserved(err):
- ctx.Data["OrgName"] = true
- ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form)
- return
- case db.IsErrNamePatternNotAllowed(err):
- ctx.Data["OrgName"] = true
- ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
- return
- case err != nil:
- ctx.ServerError("org_service.RenameOrganization", err)
+
+ if org.Name != form.Name {
+ if err := user_service.RenameUser(ctx, org.AsUser(), form.Name); err != nil {
+ if user_model.IsErrUserAlreadyExist(err) {
+ ctx.Data["Err_Name"] = true
+ ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form)
+ } else if db.IsErrNameReserved(err) {
+ ctx.Data["Err_Name"] = true
+ ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form)
+ } else if db.IsErrNamePatternNotAllowed(err) {
+ ctx.Data["Err_Name"] = true
+ ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
+ } else {
+ ctx.ServerError("RenameUser", err)
+ }
return
}
- // reset ctx.org.OrgLink with new name
- ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(form.Name)
- log.Trace("Organization name changed: %s -> %s", org.Name, form.Name)
+ ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(org.Name)
}
- // In case it's just a case change.
- org.Name = form.Name
- org.LowerName = strings.ToLower(form.Name)
+ if form.Email != "" {
+ if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil {
+ ctx.Data["Err_Email"] = true
+ ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsOptions, &form)
+ return
+ }
+ }
+ opts := &user_service.UpdateOptions{
+ FullName: optional.Some(form.FullName),
+ Description: optional.Some(form.Description),
+ Website: optional.Some(form.Website),
+ Location: optional.Some(form.Location),
+ Visibility: optional.Some(form.Visibility),
+ RepoAdminChangeTeamAccess: optional.Some(form.RepoAdminChangeTeamAccess),
+ }
if ctx.Doer.IsAdmin {
- org.MaxRepoCreation = form.MaxRepoCreation
+ opts.MaxRepoCreation = optional.Some(form.MaxRepoCreation)
}
- org.FullName = form.FullName
- org.Email = form.Email
- org.Description = form.Description
- org.Website = form.Website
- org.Location = form.Location
- org.RepoAdminChangeTeamAccess = form.RepoAdminChangeTeamAccess
-
- visibilityChanged := form.Visibility != org.Visibility
- org.Visibility = form.Visibility
+ visibilityChanged := org.Visibility != form.Visibility
- if err := user_model.UpdateUser(ctx, org.AsUser(), false); err != nil {
+ if err := user_service.UpdateUser(ctx, org.AsUser(), opts); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
@@ -215,7 +212,7 @@ func Webhooks(ctx *context.Context) {
ctx.Data["BaseLinkNew"] = ctx.Org.OrgLink + "/settings/hooks"
ctx.Data["Description"] = ctx.Tr("org.settings.hooks_desc")
- ws, err := webhook.ListWebhooksByOpts(ctx, &webhook.ListWebhookOptions{OwnerID: ctx.Org.Organization.ID})
+ ws, err := db.Find[webhook.Webhook](ctx, webhook.ListWebhookOptions{OwnerID: ctx.Org.Organization.ID})
if err != nil {
ctx.ServerError("ListWebhooksByOpts", err)
return
diff --git a/routers/web/org/setting/runners.go b/routers/web/org/setting/runners.go
index c3c771036aa..fe05709237d 100644
--- a/routers/web/org/setting/runners.go
+++ b/routers/web/org/setting/runners.go
@@ -4,7 +4,7 @@
package setting
import (
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
)
func RedirectToDefaultSetting(ctx *context.Context) {
diff --git a/routers/web/org/setting_oauth2.go b/routers/web/org/setting_oauth2.go
index 0045bce4c93..7f855795d36 100644
--- a/routers/web/org/setting_oauth2.go
+++ b/routers/web/org/setting_oauth2.go
@@ -8,11 +8,12 @@ import (
"net/http"
"code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
user_setting "code.gitea.io/gitea/routers/web/user/setting"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -35,7 +36,9 @@ func Applications(ctx *context.Context) {
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsApplications"] = true
- apps, err := auth.GetOAuth2ApplicationsByUserID(ctx, ctx.Org.Organization.ID)
+ apps, err := db.Find[auth.OAuth2Application](ctx, auth.FindOAuth2ApplicationsOptions{
+ OwnerID: ctx.Org.Organization.ID,
+ })
if err != nil {
ctx.ServerError("GetOAuth2ApplicationsByUserID", err)
return
diff --git a/routers/web/org/setting_packages.go b/routers/web/org/setting_packages.go
index 796829d34ef..af9836e42c2 100644
--- a/routers/web/org/setting_packages.go
+++ b/routers/web/org/setting_packages.go
@@ -8,10 +8,10 @@ import (
"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/packages"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
)
const (
diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go
index 9e65c8ba9cd..144d9b1b43d 100644
--- a/routers/web/org/teams.go
+++ b/routers/web/org/teams.go
@@ -5,6 +5,7 @@
package org
import (
+ "errors"
"fmt"
"net/http"
"net/url"
@@ -20,12 +21,11 @@ import (
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/routers/utils"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms"
org_service "code.gitea.io/gitea/services/org"
@@ -78,9 +78,9 @@ func TeamsAction(ctx *context.Context) {
ctx.Error(http.StatusNotFound)
return
}
- err = models.AddTeamMember(ctx, ctx.Org.Team, ctx.Doer.ID)
+ err = models.AddTeamMember(ctx, ctx.Org.Team, ctx.Doer)
case "leave":
- err = models.RemoveTeamMember(ctx, ctx.Org.Team, ctx.Doer.ID)
+ err = models.RemoveTeamMember(ctx, ctx.Org.Team, ctx.Doer)
if err != nil {
if org_model.IsErrLastOrgOwner(err) {
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
@@ -101,13 +101,13 @@ func TeamsAction(ctx *context.Context) {
return
}
- uid := ctx.FormInt64("uid")
- if uid == 0 {
+ user, _ := user_model.GetUserByID(ctx, ctx.FormInt64("uid"))
+ if user == nil {
ctx.Redirect(ctx.Org.OrgLink + "/teams")
return
}
- err = models.RemoveTeamMember(ctx, ctx.Org.Team, uid)
+ err = models.RemoveTeamMember(ctx, ctx.Org.Team, user)
if err != nil {
if org_model.IsErrLastOrgOwner(err) {
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
@@ -127,7 +127,7 @@ func TeamsAction(ctx *context.Context) {
ctx.Error(http.StatusNotFound)
return
}
- uname := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.FormString("uname")))
+ uname := strings.ToLower(ctx.FormString("uname"))
var u *user_model.User
u, err = user_model.GetUserByName(ctx, uname)
if err != nil {
@@ -162,7 +162,7 @@ func TeamsAction(ctx *context.Context) {
if ctx.Org.Team.IsMember(ctx, u.ID) {
ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users"))
} else {
- err = models.AddTeamMember(ctx, ctx.Org.Team, u.ID)
+ err = models.AddTeamMember(ctx, ctx.Org.Team, u)
}
page = "team"
@@ -190,6 +190,8 @@ func TeamsAction(ctx *context.Context) {
if err != nil {
if org_model.IsErrLastOrgOwner(err) {
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
+ } else if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Flash.Error(ctx.Tr("org.teams.members.blocked_user"))
} else {
log.Error("Action(%s): %v", ctx.Params(":action"), err)
ctx.JSON(http.StatusOK, map[string]any{
@@ -591,7 +593,7 @@ func TeamInvitePost(ctx *context.Context) {
return
}
- if err := models.AddTeamMember(ctx, team, ctx.Doer.ID); err != nil {
+ if err := models.AddTeamMember(ctx, team, ctx.Doer); err != nil {
ctx.ServerError("AddTeamMember", err)
return
}
diff --git a/routers/web/passkey.go b/routers/web/passkey.go
new file mode 100644
index 00000000000..0d10a69dfe6
--- /dev/null
+++ b/routers/web/passkey.go
@@ -0,0 +1,24 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package web
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
+)
+
+type passkeyEndpointsType struct {
+ Enroll string `json:"enroll"`
+ Manage string `json:"manage"`
+}
+
+func passkeyEndpoints(ctx *context.Context) {
+ url := setting.AppURL + "user/settings/security"
+ ctx.JSON(http.StatusOK, passkeyEndpointsType{
+ Enroll: url,
+ Manage: url,
+ })
+}
diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go
index 6284d21463f..f27329aa0f0 100644
--- a/routers/web/repo/actions/actions.go
+++ b/routers/web/repo/actions/actions.go
@@ -15,10 +15,11 @@ import (
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers/web/repo"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"github.com/nektos/act/pkg/model"
@@ -60,26 +61,26 @@ func List(ctx *context.Context) {
var workflows []Workflow
if empty, err := ctx.Repo.GitRepo.IsEmpty(); err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("IsEmpty", err)
return
} else if !empty {
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("GetBranchCommit", err)
return
}
entries, err := actions.ListWorkflows(commit)
if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("ListWorkflows", err)
return
}
// Get all runner labels
- opts := actions_model.FindRunnerOptions{
+ runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{
RepoID: ctx.Repo.Repository.ID,
+ IsOnline: optional.Some(true),
WithAvailable: true,
- }
- runners, err := actions_model.FindRunners(ctx, opts)
+ })
if err != nil {
ctx.ServerError("FindRunners", err)
return
@@ -94,12 +95,12 @@ func List(ctx *context.Context) {
workflow := Workflow{Entry: *entry}
content, err := actions.GetContentFromEntry(entry)
if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("GetContentFromEntry", err)
return
}
wf, err := model.ReadWorkflow(bytes.NewReader(content))
if err != nil {
- workflow.ErrMsg = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", err.Error())
+ workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error())
workflows = append(workflows, workflow)
continue
}
@@ -114,7 +115,7 @@ func List(ctx *context.Context) {
continue
}
if !allRunnerLabels.Contains(ro) {
- workflow.ErrMsg = ctx.Locale.Tr("actions.runs.no_matching_runner_helper", ro)
+ workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", ro)
break
}
}
@@ -169,9 +170,9 @@ func List(ctx *context.Context) {
opts.Status = []actions_model.Status{actions_model.Status(status)}
}
- runs, total, err := actions_model.FindRuns(ctx, opts)
+ runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, opts)
if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("FindAndCount", err)
return
}
@@ -179,8 +180,8 @@ func List(ctx *context.Context) {
run.Repo = ctx.Repo.Repository
}
- if err := runs.LoadTriggerUser(ctx); err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ if err := actions_model.RunList(runs).LoadTriggerUser(ctx); err != nil {
+ ctx.ServerError("LoadTriggerUser", err)
return
}
@@ -188,7 +189,7 @@ func List(ctx *context.Context) {
actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID)
if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("GetActors", err)
return
}
ctx.Data["Actors"] = repo.MakeSelfOnTop(ctx.Doer, actors)
@@ -201,6 +202,7 @@ func List(ctx *context.Context) {
pager.AddParamString("actor", fmt.Sprint(actorID))
pager.AddParamString("status", fmt.Sprint(status))
ctx.Data["Page"] = pager
+ ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0
ctx.HTML(http.StatusOK, tplListActions)
}
diff --git a/routers/web/repo/actions/badge.go b/routers/web/repo/actions/badge.go
new file mode 100644
index 00000000000..6fa951826c3
--- /dev/null
+++ b/routers/web/repo/actions/badge.go
@@ -0,0 +1,56 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "path/filepath"
+ "strings"
+
+ actions_model "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/modules/badge"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
+)
+
+func GetWorkflowBadge(ctx *context.Context) {
+ workflowFile := ctx.Params("workflow_name")
+ branch := ctx.Req.URL.Query().Get("branch")
+ if branch == "" {
+ branch = ctx.Repo.Repository.DefaultBranch
+ }
+ branchRef := fmt.Sprintf("refs/heads/%s", branch)
+ event := ctx.Req.URL.Query().Get("event")
+
+ badge, err := getWorkflowBadge(ctx, workflowFile, branchRef, event)
+ if err != nil {
+ ctx.ServerError("GetWorkflowBadge", err)
+ return
+ }
+
+ ctx.Data["Badge"] = badge
+ ctx.RespHeader().Set("Content-Type", "image/svg+xml")
+ ctx.HTML(http.StatusOK, "shared/actions/runner_badge")
+}
+
+func getWorkflowBadge(ctx *context.Context, workflowFile, branchName, event string) (badge.Badge, error) {
+ extension := filepath.Ext(workflowFile)
+ workflowName := strings.TrimSuffix(workflowFile, extension)
+
+ run, err := actions_model.GetWorkflowLatestRun(ctx, ctx.Repo.Repository.ID, workflowFile, branchName, event)
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ return badge.GenerateBadge(workflowName, "no status", badge.DefaultColor), nil
+ }
+ return badge.Badge{}, err
+ }
+
+ color, ok := badge.StatusColorMap[run.Status]
+ if !ok {
+ return badge.GenerateBadge(workflowName, "unknown status", badge.DefaultColor), nil
+ }
+ return badge.GenerateBadge(workflowName, run.Status.String(), color), nil
+}
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index 2c69b136161..3f8030e40d7 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -12,6 +12,7 @@ import (
"io"
"net/http"
"net/url"
+ "strconv"
"strings"
"time"
@@ -21,12 +22,13 @@ import (
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/base"
- context_module "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
actions_service "code.gitea.io/gitea/services/actions"
+ context_module "code.gitea.io/gitea/services/context"
"xorm.io/builder"
)
@@ -57,15 +59,16 @@ type ViewRequest struct {
type ViewResponse struct {
State struct {
Run struct {
- Link string `json:"link"`
- Title string `json:"title"`
- Status string `json:"status"`
- CanCancel bool `json:"canCancel"`
- CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve
- CanRerun bool `json:"canRerun"`
- Done bool `json:"done"`
- Jobs []*ViewJob `json:"jobs"`
- Commit ViewCommit `json:"commit"`
+ Link string `json:"link"`
+ Title string `json:"title"`
+ Status string `json:"status"`
+ CanCancel bool `json:"canCancel"`
+ CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve
+ CanRerun bool `json:"canRerun"`
+ CanDeleteArtifact bool `json:"canDeleteArtifact"`
+ Done bool `json:"done"`
+ Jobs []*ViewJob `json:"jobs"`
+ Commit ViewCommit `json:"commit"`
} `json:"run"`
CurrentJob struct {
Title string `json:"title"`
@@ -146,6 +149,7 @@ func ViewPost(ctx *context_module.Context) {
resp.State.Run.CanCancel = !run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions)
resp.State.Run.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
+ resp.State.Run.CanDeleteArtifact = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
resp.State.Run.Done = run.Status.IsDone()
resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json
resp.State.Run.Status = run.Status.String()
@@ -168,8 +172,8 @@ func ViewPost(ctx *context_module.Context) {
Link: run.RefLink(),
}
resp.State.Run.Commit = ViewCommit{
- LocaleCommit: ctx.Tr("actions.runs.commit"),
- LocalePushedBy: ctx.Tr("actions.runs.pushed_by"),
+ LocaleCommit: ctx.Locale.TrString("actions.runs.commit"),
+ LocalePushedBy: ctx.Locale.TrString("actions.runs.pushed_by"),
ShortSha: base.ShortSha(run.CommitSHA),
Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA),
Pusher: pusher,
@@ -194,7 +198,7 @@ func ViewPost(ctx *context_module.Context) {
resp.State.CurrentJob.Title = current.Name
resp.State.CurrentJob.Detail = current.Status.LocaleString(ctx.Locale)
if run.NeedApproval {
- resp.State.CurrentJob.Detail = ctx.Locale.Tr("actions.need_approval_desc")
+ resp.State.CurrentJob.Detail = ctx.Locale.TrString("actions.need_approval_desc")
}
resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json
resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json
@@ -260,10 +264,14 @@ func ViewPost(ctx *context_module.Context) {
}
// Rerun will rerun jobs in the given run
-// jobIndex = 0 means rerun all jobs
+// If jobIndexStr is a blank string, it means rerun all jobs
func Rerun(ctx *context_module.Context) {
runIndex := ctx.ParamsInt64("run")
- jobIndex := ctx.ParamsInt64("job")
+ jobIndexStr := ctx.Params("job")
+ var jobIndex int64
+ if jobIndexStr != "" {
+ jobIndex, _ = strconv.ParseInt(jobIndexStr, 10, 64)
+ }
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
if err != nil {
@@ -279,12 +287,23 @@ func Rerun(ctx *context_module.Context) {
return
}
+ // reset run's start and stop time when it is done
+ if run.Status.IsDone() {
+ run.PreviousDuration = run.Duration()
+ run.Started = 0
+ run.Stopped = 0
+ if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration"); err != nil {
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+ }
+
job, jobs := getRunJobs(ctx, runIndex, jobIndex)
if ctx.Written() {
return
}
- if jobIndex != 0 {
+ if jobIndexStr != "" {
jobs = []*actions_model.ActionRunJob{job}
}
@@ -512,7 +531,7 @@ func ArtifactsView(ctx *context_module.Context) {
}
for _, art := range artifacts {
status := "completed"
- if art.Status == int64(actions_model.ArtifactStatusExpired) {
+ if art.Status == actions_model.ArtifactStatusExpired {
status = "expired"
}
artifactsResponse.Artifacts = append(artifactsResponse.Artifacts, &ArtifactsViewItem{
@@ -524,6 +543,29 @@ func ArtifactsView(ctx *context_module.Context) {
ctx.JSON(http.StatusOK, artifactsResponse)
}
+func ArtifactsDeleteView(ctx *context_module.Context) {
+ if !ctx.Repo.CanWrite(unit.TypeActions) {
+ ctx.Error(http.StatusForbidden, "no permission")
+ return
+ }
+
+ runIndex := ctx.ParamsInt64("run")
+ artifactName := ctx.Params("artifact_name")
+
+ run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
+ if err != nil {
+ ctx.NotFoundOrServerError("GetRunByIndex", func(err error) bool {
+ return errors.Is(err, util.ErrNotExist)
+ }, err)
+ return
+ }
+ if err = actions_model.SetArtifactNeedDelete(ctx, run.ID, artifactName); err != nil {
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+ ctx.JSON(http.StatusOK, struct{}{})
+}
+
func ArtifactsDownloadView(ctx *context_module.Context) {
runIndex := ctx.ParamsInt64("run")
artifactName := ctx.Params("artifact_name")
@@ -538,7 +580,10 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
return
}
- artifacts, err := actions_model.ListArtifactsByRunIDAndName(ctx, run.ID, artifactName)
+ artifacts, err := db.Find[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{
+ RunID: run.ID,
+ ArtifactName: artifactName,
+ })
if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
@@ -548,8 +593,38 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
return
}
+ // if artifacts status is not uploaded-confirmed, treat it as not found
+ for _, art := range artifacts {
+ if art.Status != int64(actions_model.ArtifactStatusUploadConfirmed) {
+ ctx.Error(http.StatusNotFound, "artifact not found")
+ return
+ }
+ }
+
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName))
+ // Artifacts using the v4 backend are stored as a single combined zip file per artifact on the backend
+ // The v4 backend enshures ContentEncoding is set to "application/zip", which is not the case for the old backend
+ if len(artifacts) == 1 && artifacts[0].ArtifactName+".zip" == artifacts[0].ArtifactPath && artifacts[0].ContentEncoding == "application/zip" {
+ art := artifacts[0]
+ if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect {
+ u, err := storage.ActionsArtifacts.URL(art.StoragePath, art.ArtifactPath)
+ if u != nil && err == nil {
+ ctx.Redirect(u.String())
+ return
+ }
+ }
+ f, err := storage.ActionsArtifacts.Open(art.StoragePath)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+ _, _ = io.Copy(ctx.Resp, f)
+ return
+ }
+
+ // Artifacts using the v1-v3 backend are stored as multiple individual files per artifact on the backend
+ // Those need to be zipped for download
writer := zip.NewWriter(ctx.Resp)
defer writer.Close()
for _, art := range artifacts {
diff --git a/routers/web/repo/activity.go b/routers/web/repo/activity.go
index 3d030edaca0..6f6641cc65e 100644
--- a/routers/web/repo/activity.go
+++ b/routers/web/repo/activity.go
@@ -10,7 +10,7 @@ import (
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -22,6 +22,8 @@ func Activity(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.activity")
ctx.Data["PageIsActivity"] = true
+ ctx.Data["PageIsPulse"] = true
+
ctx.Data["Period"] = ctx.Params("period")
timeUntil := time.Now()
diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go
index 8c322b45e5e..f0c5622aec7 100644
--- a/routers/web/repo/attachment.go
+++ b/routers/web/repo/attachment.go
@@ -9,15 +9,15 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
- "code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/attachment"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/context/upload"
repo_service "code.gitea.io/gitea/services/repository"
)
diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go
index 1f1cca897ef..b088b8387e7 100644
--- a/routers/web/repo/blame.go
+++ b/routers/web/repo/blame.go
@@ -13,13 +13,14 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/charset"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
+ files_service "code.gitea.io/gitea/services/repository/files"
)
type blameRow struct {
@@ -114,24 +115,26 @@ func RefBlame(ctx *context.Context) {
return
}
- commitNames, previousCommits := processBlameParts(ctx, result.Parts)
+ commitNames := processBlameParts(ctx, result.Parts)
if ctx.Written() {
return
}
- renderBlame(ctx, result.Parts, commitNames, previousCommits)
+ renderBlame(ctx, result.Parts, commitNames)
ctx.HTML(http.StatusOK, tplRepoHome)
}
type blameResult struct {
- Parts []git.BlamePart
+ Parts []*git.BlamePart
UsesIgnoreRevs bool
FaultyIgnoreRevsFile bool
}
func performBlame(ctx *context.Context, repoPath string, commit *git.Commit, file string, bypassBlameIgnore bool) (*blameResult, error) {
- blameReader, err := git.CreateBlameReader(ctx, repoPath, commit, file, bypassBlameIgnore)
+ objectFormat := ctx.Repo.GetObjectFormat()
+
+ blameReader, err := git.CreateBlameReader(ctx, objectFormat, repoPath, commit, file, bypassBlameIgnore)
if err != nil {
return nil, err
}
@@ -147,7 +150,7 @@ func performBlame(ctx *context.Context, repoPath string, commit *git.Commit, fil
if len(r.Parts) == 0 && r.UsesIgnoreRevs {
// try again without ignored revs
- blameReader, err = git.CreateBlameReader(ctx, repoPath, commit, file, true)
+ blameReader, err = git.CreateBlameReader(ctx, objectFormat, repoPath, commit, file, true)
if err != nil {
return nil, err
}
@@ -170,7 +173,9 @@ func performBlame(ctx *context.Context, repoPath string, commit *git.Commit, fil
func fillBlameResult(br *git.BlameReader, r *blameResult) error {
r.UsesIgnoreRevs = br.UsesIgnoreRevs()
- r.Parts = make([]git.BlamePart, 0, 5)
+ previousHelper := make(map[string]*git.BlamePart)
+
+ r.Parts = make([]*git.BlamePart, 0, 5)
for {
blamePart, err := br.NextPart()
if err != nil {
@@ -179,18 +184,25 @@ func fillBlameResult(br *git.BlameReader, r *blameResult) error {
if blamePart == nil {
break
}
- r.Parts = append(r.Parts, *blamePart)
+
+ if prev, ok := previousHelper[blamePart.Sha]; ok {
+ if blamePart.PreviousSha == "" {
+ blamePart.PreviousSha = prev.PreviousSha
+ blamePart.PreviousPath = prev.PreviousPath
+ }
+ } else {
+ previousHelper[blamePart.Sha] = blamePart
+ }
+
+ r.Parts = append(r.Parts, blamePart)
}
return nil
}
-func processBlameParts(ctx *context.Context, blameParts []git.BlamePart) (map[string]*user_model.UserCommit, map[string]string) {
+func processBlameParts(ctx *context.Context, blameParts []*git.BlamePart) map[string]*user_model.UserCommit {
// store commit data by SHA to look up avatar info etc
commitNames := make(map[string]*user_model.UserCommit)
- // previousCommits contains links from SHA to parent SHA,
- // if parent also contains the current TreePath.
- previousCommits := make(map[string]string)
// and as blameParts can reference the same commits multiple
// times, we cache the lookup work locally
commits := make([]*git.Commit, 0, len(blameParts))
@@ -214,29 +226,11 @@ func processBlameParts(ctx *context.Context, blameParts []git.BlamePart) (map[st
} else {
ctx.ServerError("Repo.GitRepo.GetCommit", err)
}
- return nil, nil
+ return nil
}
commitCache[sha] = commit
}
- // find parent commit
- if commit.ParentCount() > 0 {
- psha := commit.Parents[0]
- previousCommit, ok := commitCache[psha.String()]
- if !ok {
- previousCommit, _ = commit.Parent(0)
- if previousCommit != nil {
- commitCache[psha.String()] = previousCommit
- }
- }
- // only store parent commit ONCE, if it has the file
- if previousCommit != nil {
- if haz1, _ := previousCommit.HasFile(ctx.Repo.TreePath); haz1 {
- previousCommits[commit.ID.String()] = previousCommit.ID.String()
- }
- }
- }
-
commits = append(commits, commit)
}
@@ -245,37 +239,17 @@ func processBlameParts(ctx *context.Context, blameParts []git.BlamePart) (map[st
commitNames[c.ID.String()] = c
}
- return commitNames, previousCommits
+ return commitNames
}
-func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames map[string]*user_model.UserCommit, previousCommits map[string]string) {
+func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames map[string]*user_model.UserCommit) {
repoLink := ctx.Repo.RepoLink
- language := ""
-
- indexFilename, worktree, deleteTemporaryFile, err := ctx.Repo.GitRepo.ReadTreeToTemporaryIndex(ctx.Repo.CommitID)
- if err == nil {
- defer deleteTemporaryFile()
-
- filename2attribute2info, err := ctx.Repo.GitRepo.CheckAttribute(git.CheckAttributeOpts{
- CachedOnly: true,
- Attributes: []string{"linguist-language", "gitlab-language"},
- Filenames: []string{ctx.Repo.TreePath},
- IndexFile: indexFilename,
- WorkTree: worktree,
- })
- if err != nil {
- log.Error("Unable to load attributes for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
- }
-
- language = filename2attribute2info[ctx.Repo.TreePath]["linguist-language"]
- if language == "" || language == "unspecified" {
- language = filename2attribute2info[ctx.Repo.TreePath]["gitlab-language"]
- }
- if language == "unspecified" {
- language = ""
- }
+ language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
+ if err != nil {
+ log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
}
+
lines := make([]string, 0)
rows := make([]*blameRow, 0)
escapeStatus := &charset.EscapeStatus{}
@@ -295,7 +269,6 @@ func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames m
}
commit := commitNames[part.Sha]
- previousSha := previousCommits[part.Sha]
if index == 0 {
// Count commit number
commitCnt++
@@ -313,8 +286,8 @@ func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames m
br.Avatar = gotemplate.HTML(avatar)
br.RepoLink = repoLink
br.PartSha = part.Sha
- br.PreviousSha = previousSha
- br.PreviousShaURL = fmt.Sprintf("%s/blame/commit/%s/%s", repoLink, url.PathEscape(previousSha), util.PathEscapeSegments(ctx.Repo.TreePath))
+ br.PreviousSha = part.PreviousSha
+ br.PreviousShaURL = fmt.Sprintf("%s/blame/commit/%s/%s", repoLink, url.PathEscape(part.PreviousSha), util.PathEscapeSegments(part.PreviousPath))
br.CommitURL = fmt.Sprintf("%s/commit/%s", repoLink, url.PathEscape(part.Sha))
br.CommitMessage = commit.CommitMessage
br.CommitSince = commitSince
@@ -332,8 +305,7 @@ func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames m
lexerName = lexerNameForLine
}
- br.EscapeStatus, line = charset.EscapeControlHTML(line, ctx.Locale)
- br.Code = gotemplate.HTML(line)
+ br.EscapeStatus, br.Code = charset.EscapeControlHTML(line, ctx.Locale)
rows = append(rows, br)
escapeStatus = escapeStatus.Or(br.EscapeStatus)
}
diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go
index bc72d5f2eca..f879a987866 100644
--- a/routers/web/repo/branch.go
+++ b/routers/web/repo/branch.go
@@ -16,14 +16,15 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
release_service "code.gitea.io/gitea/services/release"
repo_service "code.gitea.io/gitea/services/repository"
@@ -53,7 +54,7 @@ func Branches(ctx *context.Context) {
kw := ctx.FormString("q")
- defaultBranch, branches, branchesCount, err := repo_service.LoadBranches(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, util.OptionalBoolNone, kw, page, pageSize)
+ defaultBranch, branches, branchesCount, err := repo_service.LoadBranches(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, optional.None[bool](), kw, page, pageSize)
if err != nil {
ctx.ServerError("LoadBranches", err)
return
@@ -147,11 +148,13 @@ func RestoreBranchPost(ctx *context.Context) {
return
}
+ objectFormat := git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName)
+
// Don't return error below this
if err := repo_service.PushUpdate(
&repo_module.PushUpdateOptions{
RefFullName: git.RefNameFromBranch(deletedBranch.Name),
- OldCommitID: git.EmptySHA,
+ OldCommitID: objectFormat.EmptyObjectID().String(),
NewCommitID: deletedBranch.CommitID,
PusherID: ctx.Doer.ID,
PusherName: ctx.Doer.Name,
@@ -191,9 +194,9 @@ func CreateBranch(ctx *context.Context) {
}
err = release_service.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, target, form.NewBranchName, "")
} else if ctx.Repo.IsViewBranch {
- err = repo_service.CreateNewBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName, form.NewBranchName)
+ err = repo_service.CreateNewBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.Repo.BranchName, form.NewBranchName)
} else {
- err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.CommitID, form.NewBranchName)
+ err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.Repo.CommitID, form.NewBranchName)
}
if err != nil {
if models.IsErrProtectedTagName(err) {
@@ -224,7 +227,7 @@ func CreateBranch(ctx *context.Context) {
if len(e.Message) == 0 {
ctx.Flash.Error(ctx.Tr("repo.editor.push_rejected_no_message"))
} else {
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.editor.push_rejected"),
"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(e.Message),
diff --git a/routers/web/repo/cherry_pick.go b/routers/web/repo/cherry_pick.go
index 25dd8812198..088f8d889d4 100644
--- a/routers/web/repo/cherry_pick.go
+++ b/routers/web/repo/cherry_pick.go
@@ -12,11 +12,11 @@ import (
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/repository/files"
)
@@ -104,9 +104,9 @@ func CherryPickPost(ctx *context.Context) {
message := strings.TrimSpace(form.CommitSummary)
if message == "" {
if form.Revert {
- message = ctx.Tr("repo.commit.revert-header", sha)
+ message = ctx.Locale.TrString("repo.commit.revert-header", sha)
} else {
- message = ctx.Tr("repo.commit.cherry-pick-header", sha)
+ message = ctx.Locale.TrString("repo.commit.cherry-pick-header", sha)
}
}
diff --git a/routers/web/repo/code_frequency.go b/routers/web/repo/code_frequency.go
new file mode 100644
index 00000000000..c76f492da07
--- /dev/null
+++ b/routers/web/repo/code_frequency.go
@@ -0,0 +1,41 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "errors"
+ "net/http"
+
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/services/context"
+ contributors_service "code.gitea.io/gitea/services/repository"
+)
+
+const (
+ tplCodeFrequency base.TplName = "repo/activity"
+)
+
+// CodeFrequency renders the page to show repository code frequency
+func CodeFrequency(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.activity.navbar.code_frequency")
+
+ ctx.Data["PageIsActivity"] = true
+ ctx.Data["PageIsCodeFrequency"] = true
+ ctx.PageData["repoLink"] = ctx.Repo.RepoLink
+
+ ctx.HTML(http.StatusOK, tplCodeFrequency)
+}
+
+// CodeFrequencyData returns JSON of code frequency data
+func CodeFrequencyData(ctx *context.Context) {
+ if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
+ if errors.Is(err, contributors_service.ErrAwaitGeneration) {
+ ctx.Status(http.StatusAccepted)
+ return
+ }
+ ctx.ServerError("GetCodeFrequencyData", err)
+ } else {
+ ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
+ }
+}
diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go
index 3587d287fc5..d66de782f4d 100644
--- a/routers/web/repo/commit.go
+++ b/routers/web/repo/commit.go
@@ -7,7 +7,9 @@ package repo
import (
"errors"
"fmt"
+ "html/template"
"net/http"
+ "path"
"strings"
asymkey_model "code.gitea.io/gitea/models/asymkey"
@@ -17,11 +19,14 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitgraph"
+ "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/gitdiff"
git_service "code.gitea.io/gitea/services/repository"
)
@@ -158,8 +163,8 @@ func Graph(ctx *context.Context) {
ctx.Data["CommitCount"] = commitsCount
paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5)
- paginator.AddParam(ctx, "mode", "Mode")
- paginator.AddParam(ctx, "hide-pr-refs", "HidePRRefs")
+ paginator.AddParamString("mode", mode)
+ paginator.AddParamString("hide-pr-refs", fmt.Sprint(hidePRRefs))
for _, branch := range branches {
paginator.AddParamString("branch", branch)
}
@@ -198,7 +203,7 @@ func SearchCommits(ctx *context.Context) {
ctx.Data["Keyword"] = query
if all {
- ctx.Data["All"] = "checked"
+ ctx.Data["All"] = true
}
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
@@ -275,7 +280,7 @@ func Diff(ctx *context.Context) {
)
if ctx.Data["PageIsWiki"] != nil {
- gitRepo, err = git.OpenRepository(ctx, ctx.Repo.Repository.WikiPath())
+ gitRepo, err = gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository)
if err != nil {
ctx.ServerError("Repo.GitRepo.GetCommit", err)
return
@@ -294,7 +299,7 @@ func Diff(ctx *context.Context) {
}
return
}
- if len(commitID) != git.SHAFullLength {
+ if len(commitID) != commit.ID.Type().FullLength() {
commitID = commit.ID.String()
}
@@ -370,9 +375,21 @@ func Diff(ctx *context.Context) {
note := &git.Note{}
err = git.GetNote(ctx, ctx.Repo.GitRepo, commitID, note)
if err == nil {
- ctx.Data["Note"] = string(charset.ToUTF8WithFallback(note.Message))
ctx.Data["NoteCommit"] = note.Commit
ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit)
+ ctx.Data["NoteRendered"], err = markup.RenderCommitMessage(&markup.RenderContext{
+ Links: markup.Links{
+ Base: ctx.Repo.RepoLink,
+ BranchPath: path.Join("commit", util.PathEscapeSegments(commitID)),
+ },
+ Metas: ctx.Repo.Repository.ComposeMetas(ctx),
+ GitRepo: ctx.Repo.GitRepo,
+ Ctx: ctx,
+ }, template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{}))))
+ if err != nil {
+ ctx.ServerError("RenderCommitMessage", err)
+ return
+ }
}
ctx.Data["BranchName"], err = commit.GetBranchName()
@@ -388,7 +405,7 @@ func Diff(ctx *context.Context) {
func RawDiff(ctx *context.Context) {
var gitRepo *git.Repository
if ctx.Data["PageIsWiki"] != nil {
- wikiRepo, err := git.OpenRepository(ctx, ctx.Repo.Repository.WikiPath())
+ wikiRepo, err := gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository)
if err != nil {
ctx.ServerError("OpenRepository", err)
return
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index b69af3c61cc..b0570f97c3b 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -25,16 +25,18 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
- "code.gitea.io/gitea/modules/context"
csv_module "code.gitea.io/gitea/modules/csv"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/typesniffer"
- "code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/gitdiff"
)
@@ -125,7 +127,7 @@ func setCsvCompareContext(ctx *context.Context) {
return CsvDiffResult{nil, ""}
}
- errTooLarge := errors.New(ctx.Locale.Tr("repo.error.csv.too_large"))
+ errTooLarge := errors.New(ctx.Locale.TrString("repo.error.csv.too_large"))
csvReaderFromCommit := func(ctx *markup.RenderContext, blob *git.Blob) (*csv.Reader, io.Closer, error) {
if blob == nil {
@@ -142,7 +144,7 @@ func setCsvCompareContext(ctx *context.Context) {
return nil, nil, err
}
- csvReader, err := csv_module.CreateReaderAndDetermineDelimiter(ctx, charset.ToUTF8WithFallbackReader(reader))
+ csvReader, err := csv_module.CreateReaderAndDetermineDelimiter(ctx, charset.ToUTF8WithFallbackReader(reader, charset.ConvertOpts{}))
return csvReader, reader, err
}
@@ -310,13 +312,14 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo {
baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(ci.BaseBranch)
baseIsBranch := ctx.Repo.GitRepo.IsBranchExist(ci.BaseBranch)
baseIsTag := ctx.Repo.GitRepo.IsTagExist(ci.BaseBranch)
+
if !baseIsCommit && !baseIsBranch && !baseIsTag {
// Check if baseBranch is short sha commit hash
if baseCommit, _ := ctx.Repo.GitRepo.GetCommit(ci.BaseBranch); baseCommit != nil {
ci.BaseBranch = baseCommit.ID.String()
ctx.Data["BaseBranch"] = ci.BaseBranch
baseIsCommit = true
- } else if ci.BaseBranch == git.EmptySHA {
+ } else if ci.BaseBranch == ctx.Repo.GetObjectFormat().EmptyObjectID().String() {
if isSameRepo {
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ci.HeadBranch))
} else {
@@ -407,7 +410,7 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo {
ci.HeadRepo = ctx.Repo.Repository
ci.HeadGitRepo = ctx.Repo.GitRepo
} else if has {
- ci.HeadGitRepo, err = git.OpenRepository(ctx, ci.HeadRepo.RepoPath())
+ ci.HeadGitRepo, err = gitrepo.OpenRepository(ctx, ci.HeadRepo)
if err != nil {
ctx.ServerError("OpenRepository", err)
return nil
@@ -687,7 +690,7 @@ func PrepareCompareDiff(
}
func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repository) (branches, tags []string, err error) {
- gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
+ gitRepo, err := gitrepo.OpenRepository(ctx, repo)
if err != nil {
return nil, nil, err
}
@@ -698,7 +701,7 @@ func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repositor
ListOptions: db.ListOptions{
ListAll: true,
},
- IsDeletedBranch: util.OptionalBoolFalse,
+ IsDeletedBranch: optional.Some(false),
})
if err != nil {
return nil, nil, err
@@ -755,7 +758,7 @@ func CompareDiff(ctx *context.Context) {
ListOptions: db.ListOptions{
ListAll: true,
},
- IsDeletedBranch: util.OptionalBoolFalse,
+ IsDeletedBranch: optional.Some(false),
})
if err != nil {
ctx.ServerError("GetBranches", err)
@@ -844,6 +847,7 @@ func CompareDiff(ctx *context.Context) {
}
}
+ ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanWrite(unit.TypeProjects)
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment")
@@ -874,7 +878,7 @@ func ExcerptBlob(ctx *context.Context) {
gitRepo := ctx.Repo.GitRepo
if ctx.FormBool("wiki") {
var err error
- gitRepo, err = git.OpenRepository(ctx, ctx.Repo.Repository.WikiPath())
+ gitRepo, err = gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository)
if err != nil {
ctx.ServerError("OpenRepository", err)
return
diff --git a/routers/web/repo/contributors.go b/routers/web/repo/contributors.go
new file mode 100644
index 00000000000..5fda17469e9
--- /dev/null
+++ b/routers/web/repo/contributors.go
@@ -0,0 +1,44 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "errors"
+ "net/http"
+
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/services/context"
+ contributors_service "code.gitea.io/gitea/services/repository"
+)
+
+const (
+ tplContributors base.TplName = "repo/activity"
+)
+
+// Contributors render the page to show repository contributors graph
+func Contributors(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.activity.navbar.contributors")
+
+ ctx.Data["PageIsActivity"] = true
+ ctx.Data["PageIsContributors"] = true
+
+ ctx.PageData["contributionType"] = "commits"
+
+ ctx.PageData["repoLink"] = ctx.Repo.RepoLink
+
+ ctx.HTML(http.StatusOK, tplContributors)
+}
+
+// ContributorsData renders JSON of contributors along with their weekly commit statistics
+func ContributorsData(ctx *context.Context) {
+ if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
+ if errors.Is(err, contributors_service.ErrAwaitGeneration) {
+ ctx.Status(http.StatusAccepted)
+ return
+ }
+ ctx.ServerError("GetContributorStats", err)
+ } else {
+ ctx.JSON(http.StatusOK, contributorStats)
+ }
+}
diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go
index a9e2e2b2fad..c4a8baecca3 100644
--- a/routers/web/repo/download.go
+++ b/routers/web/repo/download.go
@@ -9,7 +9,6 @@ import (
"time"
git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/lfs"
@@ -17,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/routers/common"
+ "code.gitea.io/gitea/services/context"
)
// ServeBlobOrLFS download a git.Blob redirecting to LFS if necessary
diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go
index 1ad091b70fd..6146ce4ce4a 100644
--- a/routers/web/repo/editor.go
+++ b/routers/web/repo/editor.go
@@ -16,17 +16,17 @@ import (
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/typesniffer"
- "code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/utils"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/forms"
files_service "code.gitea.io/gitea/services/repository/files"
)
@@ -161,13 +161,10 @@ func editFile(ctx *context.Context, isNewFile bool) {
}
d, _ := io.ReadAll(dataRc)
- if err := dataRc.Close(); err != nil {
- log.Error("Error whilst closing blob data: %v", err)
- }
buf = append(buf, d...)
- if content, err := charset.ToUTF8WithErr(buf); err != nil {
- log.Error("ToUTF8WithErr: %v", err)
+ if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil {
+ log.Error("ToUTF8: %v", err)
ctx.Data["FileContent"] = string(buf)
} else {
ctx.Data["FileContent"] = content
@@ -262,9 +259,9 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
message := strings.TrimSpace(form.CommitSummary)
if len(message) == 0 {
if isNewFile {
- message = ctx.Tr("repo.editor.add", form.TreePath)
+ message = ctx.Locale.TrString("repo.editor.add", form.TreePath)
} else {
- message = ctx.Tr("repo.editor.update", form.TreePath)
+ message = ctx.Locale.TrString("repo.editor.update", form.TreePath)
}
}
form.CommitMessage = strings.TrimSpace(form.CommitMessage)
@@ -287,7 +284,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
Operation: operation,
FromTreePath: ctx.Repo.TreePath,
TreePath: form.TreePath,
- ContentReader: strings.NewReader(form.Content),
+ ContentReader: strings.NewReader(strings.ReplaceAll(form.Content, "\r", "")),
},
},
Signoff: form.Signoff,
@@ -344,7 +341,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
if len(errPushRej.Message) == 0 {
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form)
} else {
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.editor.push_rejected"),
"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(errPushRej.Message),
@@ -356,7 +353,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
ctx.RenderWithErr(flashError, tplEditFile, &form)
}
} else {
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.editor.fail_to_update_file", form.TreePath),
"Summary": ctx.Tr("repo.editor.fail_to_update_file_summary"),
"Details": utils.SanitizeFlashErrorString(err.Error()),
@@ -415,7 +412,7 @@ func DiffPreviewPost(ctx *context.Context) {
}
if diff.NumFiles == 0 {
- ctx.PlainText(http.StatusOK, ctx.Tr("repo.editor.no_changes_to_show"))
+ ctx.PlainText(http.StatusOK, ctx.Locale.TrString("repo.editor.no_changes_to_show"))
return
}
ctx.Data["File"] = diff.Files[0]
@@ -482,7 +479,7 @@ func DeleteFilePost(ctx *context.Context) {
message := strings.TrimSpace(form.CommitSummary)
if len(message) == 0 {
- message = ctx.Tr("repo.editor.delete", ctx.Repo.TreePath)
+ message = ctx.Locale.TrString("repo.editor.delete", ctx.Repo.TreePath)
}
form.CommitMessage = strings.TrimSpace(form.CommitMessage)
if len(form.CommitMessage) > 0 {
@@ -545,7 +542,7 @@ func DeleteFilePost(ctx *context.Context) {
if len(errPushRej.Message) == 0 {
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form)
} else {
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.editor.push_rejected"),
"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(errPushRej.Message),
@@ -691,7 +688,7 @@ func UploadFilePost(ctx *context.Context) {
if dir == "" {
dir = "/"
}
- message = ctx.Tr("repo.editor.upload_files_to_dir", dir)
+ message = ctx.Locale.TrString("repo.editor.upload_files_to_dir", dir)
}
form.CommitMessage = strings.TrimSpace(form.CommitMessage)
@@ -745,7 +742,7 @@ func UploadFilePost(ctx *context.Context) {
if len(errPushRej.Message) == 0 {
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplUploadFile, &form)
} else {
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.editor.push_rejected"),
"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(errPushRej.Message),
diff --git a/routers/web/repo/editor_test.go b/routers/web/repo/editor_test.go
index 67fb277d5c2..313fcfe33a2 100644
--- a/routers/web/repo/editor_test.go
+++ b/routers/web/repo/editor_test.go
@@ -7,8 +7,9 @@ import (
"testing"
"code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/contexttest"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/gitrepo"
+ "code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
)
@@ -66,7 +67,7 @@ func TestGetClosestParentWithFiles(t *testing.T) {
repo := ctx.Repo.Repository
branch := repo.DefaultBranch
- gitRepo, _ := git.OpenRepository(git.DefaultContext, repo.RepoPath())
+ gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo)
defer gitRepo.Close()
commit, _ := gitRepo.GetBranchCommit(branch)
var expectedTreePath string // Should return the root dir, empty string, since there are no subdirs in this repo
diff --git a/routers/web/repo/find.go b/routers/web/repo/find.go
index daefe59c8f2..07b37227988 100644
--- a/routers/web/repo/find.go
+++ b/routers/web/repo/find.go
@@ -7,7 +7,7 @@ import (
"net/http"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
)
const (
diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go
new file mode 100644
index 00000000000..60e37476eef
--- /dev/null
+++ b/routers/web/repo/fork.go
@@ -0,0 +1,238 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "errors"
+ "net/http"
+ "net/url"
+
+ "code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
+ "code.gitea.io/gitea/models/organization"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/forms"
+ repo_service "code.gitea.io/gitea/services/repository"
+)
+
+const (
+ tplFork base.TplName = "repo/pulls/fork"
+)
+
+func getForkRepository(ctx *context.Context) *repo_model.Repository {
+ forkRepo := ctx.Repo.Repository
+ if ctx.Written() {
+ return nil
+ }
+
+ if forkRepo.IsEmpty {
+ log.Trace("Empty repository %-v", forkRepo)
+ ctx.NotFound("getForkRepository", nil)
+ return nil
+ }
+
+ if err := forkRepo.LoadOwner(ctx); err != nil {
+ ctx.ServerError("LoadOwner", err)
+ return nil
+ }
+
+ ctx.Data["repo_name"] = forkRepo.Name
+ ctx.Data["description"] = forkRepo.Description
+ ctx.Data["IsPrivate"] = forkRepo.IsPrivate || forkRepo.Owner.Visibility == structs.VisibleTypePrivate
+ canForkToUser := forkRepo.OwnerID != ctx.Doer.ID && !repo_model.HasForkedRepo(ctx, ctx.Doer.ID, forkRepo.ID)
+
+ ctx.Data["ForkRepo"] = forkRepo
+
+ ownedOrgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx, ctx.Doer.ID)
+ if err != nil {
+ ctx.ServerError("GetOrgsCanCreateRepoByUserID", err)
+ return nil
+ }
+ var orgs []*organization.Organization
+ for _, org := range ownedOrgs {
+ if forkRepo.OwnerID != org.ID && !repo_model.HasForkedRepo(ctx, org.ID, forkRepo.ID) {
+ orgs = append(orgs, org)
+ }
+ }
+
+ traverseParentRepo := forkRepo
+ for {
+ if ctx.Doer.ID == traverseParentRepo.OwnerID {
+ canForkToUser = false
+ } else {
+ for i, org := range orgs {
+ if org.ID == traverseParentRepo.OwnerID {
+ orgs = append(orgs[:i], orgs[i+1:]...)
+ break
+ }
+ }
+ }
+
+ if !traverseParentRepo.IsFork {
+ break
+ }
+ traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID)
+ if err != nil {
+ ctx.ServerError("GetRepositoryByID", err)
+ return nil
+ }
+ }
+
+ ctx.Data["CanForkToUser"] = canForkToUser
+ ctx.Data["Orgs"] = orgs
+
+ if canForkToUser {
+ ctx.Data["ContextUser"] = ctx.Doer
+ } else if len(orgs) > 0 {
+ ctx.Data["ContextUser"] = orgs[0]
+ } else {
+ ctx.Data["CanForkRepo"] = false
+ ctx.Flash.Error(ctx.Tr("repo.fork_no_valid_owners"), true)
+ return nil
+ }
+
+ branches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
+ RepoID: ctx.Repo.Repository.ID,
+ ListOptions: db.ListOptions{
+ ListAll: true,
+ },
+ IsDeletedBranch: optional.Some(false),
+ // Add it as the first option
+ ExcludeBranchNames: []string{ctx.Repo.Repository.DefaultBranch},
+ })
+ if err != nil {
+ ctx.ServerError("FindBranchNames", err)
+ return nil
+ }
+ ctx.Data["Branches"] = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
+
+ return forkRepo
+}
+
+// Fork render repository fork page
+func Fork(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("new_fork")
+
+ if ctx.Doer.CanForkRepo() {
+ ctx.Data["CanForkRepo"] = true
+ } else {
+ maxCreationLimit := ctx.Doer.MaxCreationLimit()
+ msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
+ ctx.Flash.Error(msg, true)
+ }
+
+ getForkRepository(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ ctx.HTML(http.StatusOK, tplFork)
+}
+
+// ForkPost response for forking a repository
+func ForkPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.CreateRepoForm)
+ ctx.Data["Title"] = ctx.Tr("new_fork")
+ ctx.Data["CanForkRepo"] = true
+
+ ctxUser := checkContextUser(ctx, form.UID)
+ if ctx.Written() {
+ return
+ }
+
+ forkRepo := getForkRepository(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ ctx.Data["ContextUser"] = ctxUser
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, tplFork)
+ return
+ }
+
+ var err error
+ traverseParentRepo := forkRepo
+ for {
+ if ctxUser.ID == traverseParentRepo.OwnerID {
+ ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
+ return
+ }
+ repo := repo_model.GetForkedRepo(ctx, ctxUser.ID, traverseParentRepo.ID)
+ if repo != nil {
+ ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
+ return
+ }
+ if !traverseParentRepo.IsFork {
+ break
+ }
+ traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID)
+ if err != nil {
+ ctx.ServerError("GetRepositoryByID", err)
+ return
+ }
+ }
+
+ // Check if user is allowed to create repo's on the organization.
+ if ctxUser.IsOrganization() {
+ isAllowedToFork, err := organization.OrgFromUser(ctxUser).CanCreateOrgRepo(ctx, ctx.Doer.ID)
+ if err != nil {
+ ctx.ServerError("CanCreateOrgRepo", err)
+ return
+ } else if !isAllowedToFork {
+ ctx.Error(http.StatusForbidden)
+ return
+ }
+ }
+
+ repo, err := repo_service.ForkRepository(ctx, ctx.Doer, ctxUser, repo_service.ForkRepoOptions{
+ BaseRepo: forkRepo,
+ Name: form.RepoName,
+ Description: form.Description,
+ SingleBranch: form.ForkSingleBranch,
+ })
+ if err != nil {
+ ctx.Data["Err_RepoName"] = true
+ switch {
+ case repo_model.IsErrReachLimitOfRepo(err):
+ maxCreationLimit := ctxUser.MaxCreationLimit()
+ msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
+ ctx.RenderWithErr(msg, tplFork, &form)
+ case repo_model.IsErrRepoAlreadyExist(err):
+ ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
+ case repo_model.IsErrRepoFilesAlreadyExist(err):
+ switch {
+ case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
+ ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplFork, form)
+ case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
+ ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplFork, form)
+ case setting.Repository.AllowDeleteOfUnadoptedRepositories:
+ ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplFork, form)
+ default:
+ ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplFork, form)
+ }
+ case db.IsErrNameReserved(err):
+ ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form)
+ case db.IsErrNamePatternNotAllowed(err):
+ ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form)
+ case errors.Is(err, user_model.ErrBlockedUser):
+ ctx.RenderWithErr(ctx.Tr("repo.fork.blocked_user"), tplFork, form)
+ default:
+ ctx.ServerError("ForkPost", err)
+ }
+ return
+ }
+
+ log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
+ ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
+}
diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go
index 6ff385f9890..8fb6d930688 100644
--- a/routers/web/repo/githttp.go
+++ b/routers/web/repo/githttp.go
@@ -11,7 +11,7 @@ import (
"fmt"
"net/http"
"os"
- "path"
+ "path/filepath"
"regexp"
"strconv"
"strings"
@@ -24,13 +24,13 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
repo_service "code.gitea.io/gitea/services/repository"
"github.com/go-chi/cors"
@@ -90,11 +90,10 @@ func httpBase(ctx *context.Context) *serviceHandler {
isWiki := false
unitType := unit.TypeCode
- var wikiRepoName string
+
if strings.HasSuffix(reponame, ".wiki") {
isWiki = true
unitType = unit.TypeWiki
- wikiRepoName = reponame
reponame = reponame[:len(reponame)-5]
}
@@ -107,16 +106,16 @@ func httpBase(ctx *context.Context) *serviceHandler {
repoExist := true
repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, reponame)
if err != nil {
- if repo_model.IsErrRepoNotExist(err) {
- if redirectRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, reponame); err == nil {
- context.RedirectToRepo(ctx.Base, redirectRepoID)
- return nil
- }
- repoExist = false
- } else {
+ if !repo_model.IsErrRepoNotExist(err) {
ctx.ServerError("GetRepositoryByName", err)
return nil
}
+
+ if redirectRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, reponame); err == nil {
+ context.RedirectToRepo(ctx.Base, redirectRepoID)
+ return nil
+ }
+ repoExist = false
}
// Don't allow pushing if the repo is archived
@@ -184,7 +183,7 @@ func httpBase(ctx *context.Context) *serviceHandler {
if repoExist {
// Because of special ref "refs/for" .. , need delay write permission check
- if git.SupportProcReceive {
+ if git.DefaultFeatures.SupportProcReceive {
accessMode = perm.AccessModeRead
}
@@ -292,22 +291,9 @@ func httpBase(ctx *context.Context) *serviceHandler {
environ = append(environ, repo_module.EnvRepoID+fmt.Sprintf("=%d", repo.ID))
- w := ctx.Resp
- r := ctx.Req
- cfg := &serviceConfig{
- UploadPack: true,
- ReceivePack: true,
- Env: environ,
- }
-
- r.URL.Path = strings.ToLower(r.URL.Path) // blue: In case some repo name has upper case name
+ ctx.Req.URL.Path = strings.ToLower(ctx.Req.URL.Path) // blue: In case some repo name has upper case name
- dir := repo_model.RepoPath(username, reponame)
- if isWiki {
- dir = repo_model.RepoPath(username, wikiRepoName)
- }
-
- return &serviceHandler{cfg, w, r, dir, cfg.Env}
+ return &serviceHandler{repo, isWiki, environ}
}
var (
@@ -329,7 +315,7 @@ func dummyInfoRefs(ctx *context.Context) {
}
}()
- if err := git.InitRepository(ctx, tmpDir, true); err != nil {
+ if err := git.InitRepository(ctx, tmpDir, true, git.Sha1ObjectFormat.Name()); err != nil {
log.Error("Failed to init bare repo for git-receive-pack cache: %v", err)
return
}
@@ -352,32 +338,31 @@ func dummyInfoRefs(ctx *context.Context) {
_, _ = ctx.Write(infoRefsCache)
}
-type serviceConfig struct {
- UploadPack bool
- ReceivePack bool
- Env []string
-}
-
type serviceHandler struct {
- cfg *serviceConfig
- w http.ResponseWriter
- r *http.Request
- dir string
+ repo *repo_model.Repository
+ isWiki bool
environ []string
}
-func (h *serviceHandler) setHeaderNoCache() {
- h.w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
- h.w.Header().Set("Pragma", "no-cache")
- h.w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
+func (h *serviceHandler) getRepoDir() string {
+ if h.isWiki {
+ return h.repo.WikiPath()
+ }
+ return h.repo.RepoPath()
+}
+
+func setHeaderNoCache(ctx *context.Context) {
+ ctx.Resp.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
+ ctx.Resp.Header().Set("Pragma", "no-cache")
+ ctx.Resp.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
}
-func (h *serviceHandler) setHeaderCacheForever() {
+func setHeaderCacheForever(ctx *context.Context) {
now := time.Now().Unix()
expires := now + 31536000
- h.w.Header().Set("Date", fmt.Sprintf("%d", now))
- h.w.Header().Set("Expires", fmt.Sprintf("%d", expires))
- h.w.Header().Set("Cache-Control", "public, max-age=31536000")
+ ctx.Resp.Header().Set("Date", fmt.Sprintf("%d", now))
+ ctx.Resp.Header().Set("Expires", fmt.Sprintf("%d", expires))
+ ctx.Resp.Header().Set("Cache-Control", "public, max-age=31536000")
}
func containsParentDirectorySeparator(v string) bool {
@@ -394,71 +379,71 @@ func containsParentDirectorySeparator(v string) bool {
func isSlashRune(r rune) bool { return r == '/' || r == '\\' }
-func (h *serviceHandler) sendFile(contentType, file string) {
+func (h *serviceHandler) sendFile(ctx *context.Context, contentType, file string) {
if containsParentDirectorySeparator(file) {
log.Error("request file path contains invalid path: %v", file)
- h.w.WriteHeader(http.StatusBadRequest)
+ ctx.Resp.WriteHeader(http.StatusBadRequest)
return
}
- reqFile := path.Join(h.dir, file)
+ reqFile := filepath.Join(h.getRepoDir(), file)
fi, err := os.Stat(reqFile)
if os.IsNotExist(err) {
- h.w.WriteHeader(http.StatusNotFound)
+ ctx.Resp.WriteHeader(http.StatusNotFound)
return
}
- h.w.Header().Set("Content-Type", contentType)
- h.w.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size()))
- h.w.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat))
- http.ServeFile(h.w, h.r, reqFile)
+ ctx.Resp.Header().Set("Content-Type", contentType)
+ ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size()))
+ ctx.Resp.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat))
+ http.ServeFile(ctx.Resp, ctx.Req, reqFile)
}
// one or more key=value pairs separated by colons
var safeGitProtocolHeader = regexp.MustCompile(`^[0-9a-zA-Z]+=[0-9a-zA-Z]+(:[0-9a-zA-Z]+=[0-9a-zA-Z]+)*$`)
-func prepareGitCmdWithAllowedService(service string, h *serviceHandler) (*git.Command, error) {
- if service == "receive-pack" && h.cfg.ReceivePack {
- return git.NewCommand(h.r.Context(), "receive-pack"), nil
+func prepareGitCmdWithAllowedService(ctx *context.Context, service string) (*git.Command, error) {
+ if service == "receive-pack" {
+ return git.NewCommand(ctx, "receive-pack"), nil
}
- if service == "upload-pack" && h.cfg.UploadPack {
- return git.NewCommand(h.r.Context(), "upload-pack"), nil
+ if service == "upload-pack" {
+ return git.NewCommand(ctx, "upload-pack"), nil
}
return nil, fmt.Errorf("service %q is not allowed", service)
}
-func serviceRPC(h *serviceHandler, service string) {
+func serviceRPC(ctx *context.Context, h *serviceHandler, service string) {
defer func() {
- if err := h.r.Body.Close(); err != nil {
+ if err := ctx.Req.Body.Close(); err != nil {
log.Error("serviceRPC: Close: %v", err)
}
}()
expectedContentType := fmt.Sprintf("application/x-git-%s-request", service)
- if h.r.Header.Get("Content-Type") != expectedContentType {
- log.Error("Content-Type (%q) doesn't match expected: %q", h.r.Header.Get("Content-Type"), expectedContentType)
- h.w.WriteHeader(http.StatusUnauthorized)
+ if ctx.Req.Header.Get("Content-Type") != expectedContentType {
+ log.Error("Content-Type (%q) doesn't match expected: %q", ctx.Req.Header.Get("Content-Type"), expectedContentType)
+ ctx.Resp.WriteHeader(http.StatusUnauthorized)
return
}
- cmd, err := prepareGitCmdWithAllowedService(service, h)
+ cmd, err := prepareGitCmdWithAllowedService(ctx, service)
if err != nil {
log.Error("Failed to prepareGitCmdWithService: %v", err)
- h.w.WriteHeader(http.StatusUnauthorized)
+ ctx.Resp.WriteHeader(http.StatusUnauthorized)
return
}
- h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service))
+ ctx.Resp.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service))
- reqBody := h.r.Body
+ reqBody := ctx.Req.Body
// Handle GZIP.
- if h.r.Header.Get("Content-Encoding") == "gzip" {
+ if ctx.Req.Header.Get("Content-Encoding") == "gzip" {
reqBody, err = gzip.NewReader(reqBody)
if err != nil {
log.Error("Fail to create gzip reader: %v", err)
- h.w.WriteHeader(http.StatusInternalServerError)
+ ctx.Resp.WriteHeader(http.StatusInternalServerError)
return
}
}
@@ -466,23 +451,23 @@ func serviceRPC(h *serviceHandler, service string) {
// set this for allow pre-receive and post-receive execute
h.environ = append(h.environ, "SSH_ORIGINAL_COMMAND="+service)
- if protocol := h.r.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
+ if protocol := ctx.Req.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
}
var stderr bytes.Buffer
- cmd.AddArguments("--stateless-rpc").AddDynamicArguments(h.dir)
- cmd.SetDescription(fmt.Sprintf("%s %s %s [repo_path: %s]", git.GitExecutable, service, "--stateless-rpc", h.dir))
+ cmd.AddArguments("--stateless-rpc").AddDynamicArguments(h.getRepoDir())
+ cmd.SetDescription(fmt.Sprintf("%s %s %s [repo_path: %s]", git.GitExecutable, service, "--stateless-rpc", h.getRepoDir()))
if err := cmd.Run(&git.RunOpts{
- Dir: h.dir,
+ Dir: h.getRepoDir(),
Env: append(os.Environ(), h.environ...),
- Stdout: h.w,
+ Stdout: ctx.Resp,
Stdin: reqBody,
Stderr: &stderr,
UseContextTimeout: true,
}); err != nil {
if err.Error() != "signal: killed" {
- log.Error("Fail to serve RPC(%s) in %s: %v - %s", service, h.dir, err, stderr.String())
+ log.Error("Fail to serve RPC(%s) in %s: %v - %s", service, h.getRepoDir(), err, stderr.String())
}
return
}
@@ -492,7 +477,7 @@ func serviceRPC(h *serviceHandler, service string) {
func ServiceUploadPack(ctx *context.Context) {
h := httpBase(ctx)
if h != nil {
- serviceRPC(h, "upload-pack")
+ serviceRPC(ctx, h, "upload-pack")
}
}
@@ -500,12 +485,12 @@ func ServiceUploadPack(ctx *context.Context) {
func ServiceReceivePack(ctx *context.Context) {
h := httpBase(ctx)
if h != nil {
- serviceRPC(h, "receive-pack")
+ serviceRPC(ctx, h, "receive-pack")
}
}
-func getServiceType(r *http.Request) string {
- serviceType := r.FormValue("service")
+func getServiceType(ctx *context.Context) string {
+ serviceType := ctx.Req.FormValue("service")
if !strings.HasPrefix(serviceType, "git-") {
return ""
}
@@ -534,28 +519,28 @@ func GetInfoRefs(ctx *context.Context) {
if h == nil {
return
}
- h.setHeaderNoCache()
- service := getServiceType(h.r)
- cmd, err := prepareGitCmdWithAllowedService(service, h)
+ setHeaderNoCache(ctx)
+ service := getServiceType(ctx)
+ cmd, err := prepareGitCmdWithAllowedService(ctx, service)
if err == nil {
- if protocol := h.r.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
+ if protocol := ctx.Req.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
}
h.environ = append(os.Environ(), h.environ...)
- refs, _, err := cmd.AddArguments("--stateless-rpc", "--advertise-refs", ".").RunStdBytes(&git.RunOpts{Env: h.environ, Dir: h.dir})
+ refs, _, err := cmd.AddArguments("--stateless-rpc", "--advertise-refs", ".").RunStdBytes(&git.RunOpts{Env: h.environ, Dir: h.getRepoDir()})
if err != nil {
log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
}
- h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service))
- h.w.WriteHeader(http.StatusOK)
- _, _ = h.w.Write(packetWrite("# service=git-" + service + "\n"))
- _, _ = h.w.Write([]byte("0000"))
- _, _ = h.w.Write(refs)
+ ctx.Resp.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service))
+ ctx.Resp.WriteHeader(http.StatusOK)
+ _, _ = ctx.Resp.Write(packetWrite("# service=git-" + service + "\n"))
+ _, _ = ctx.Resp.Write([]byte("0000"))
+ _, _ = ctx.Resp.Write(refs)
} else {
- updateServerInfo(ctx, h.dir)
- h.sendFile("text/plain; charset=utf-8", "info/refs")
+ updateServerInfo(ctx, h.getRepoDir())
+ h.sendFile(ctx, "text/plain; charset=utf-8", "info/refs")
}
}
@@ -564,12 +549,12 @@ func GetTextFile(p string) func(*context.Context) {
return func(ctx *context.Context) {
h := httpBase(ctx)
if h != nil {
- h.setHeaderNoCache()
+ setHeaderNoCache(ctx)
file := ctx.Params("file")
if file != "" {
- h.sendFile("text/plain", "objects/info/"+file)
+ h.sendFile(ctx, "text/plain", "objects/info/"+file)
} else {
- h.sendFile("text/plain", p)
+ h.sendFile(ctx, "text/plain", p)
}
}
}
@@ -579,8 +564,8 @@ func GetTextFile(p string) func(*context.Context) {
func GetInfoPacks(ctx *context.Context) {
h := httpBase(ctx)
if h != nil {
- h.setHeaderCacheForever()
- h.sendFile("text/plain; charset=utf-8", "objects/info/packs")
+ setHeaderCacheForever(ctx)
+ h.sendFile(ctx, "text/plain; charset=utf-8", "objects/info/packs")
}
}
@@ -588,8 +573,8 @@ func GetInfoPacks(ctx *context.Context) {
func GetLooseObject(ctx *context.Context) {
h := httpBase(ctx)
if h != nil {
- h.setHeaderCacheForever()
- h.sendFile("application/x-git-loose-object", fmt.Sprintf("objects/%s/%s",
+ setHeaderCacheForever(ctx)
+ h.sendFile(ctx, "application/x-git-loose-object", fmt.Sprintf("objects/%s/%s",
ctx.Params("head"), ctx.Params("hash")))
}
}
@@ -598,8 +583,8 @@ func GetLooseObject(ctx *context.Context) {
func GetPackFile(ctx *context.Context) {
h := httpBase(ctx)
if h != nil {
- h.setHeaderCacheForever()
- h.sendFile("application/x-git-packed-objects", "objects/pack/pack-"+ctx.Params("file")+".pack")
+ setHeaderCacheForever(ctx)
+ h.sendFile(ctx, "application/x-git-packed-objects", "objects/pack/pack-"+ctx.Params("file")+".pack")
}
}
@@ -607,7 +592,7 @@ func GetPackFile(ctx *context.Context) {
func GetIdxFile(ctx *context.Context) {
h := httpBase(ctx)
if h != nil {
- h.setHeaderCacheForever()
- h.sendFile("application/x-git-packed-objects-toc", "objects/pack/pack-"+ctx.Params("file")+".idx")
+ setHeaderCacheForever(ctx)
+ h.sendFile(ctx, "application/x-git-packed-objects-toc", "objects/pack/pack-"+ctx.Params("file")+".idx")
}
}
diff --git a/routers/web/repo/helper.go b/routers/web/repo/helper.go
index a98abe566f8..5e1e116018e 100644
--- a/routers/web/repo/helper.go
+++ b/routers/web/repo/helper.go
@@ -8,8 +8,8 @@ import (
"sort"
"code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/services/context"
)
func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User {
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 94300da8683..a0a500f0b23 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -9,6 +9,7 @@ import (
stdCtx "context"
"errors"
"fmt"
+ "html/template"
"math/big"
"net/http"
"net/url"
@@ -31,28 +32,32 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/emoji"
"code.gitea.io/gitea/modules/git"
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
issue_template "code.gitea.io/gitea/modules/issue/template"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
+ "code.gitea.io/gitea/modules/optional"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/templates/vars"
"code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/utils"
asymkey_service "code.gitea.io/gitea/services/asymkey"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms"
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
+ user_service "code.gitea.io/gitea/services/user"
)
const (
@@ -136,7 +141,7 @@ func MustAllowPulls(ctx *context.Context) {
}
}
-func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption util.OptionalBool) {
+func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption optional.Option[bool]) {
var err error
viewType := ctx.FormString("type")
sortType := ctx.FormString("sort")
@@ -237,10 +242,18 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
}
}
- isShowClosed := ctx.FormString("state") == "closed"
- // if open issues are zero and close don't, use closed as default
+ var isShowClosed optional.Option[bool]
+ switch ctx.FormString("state") {
+ case "closed":
+ isShowClosed = optional.Some(true)
+ case "all":
+ isShowClosed = optional.None[bool]()
+ default:
+ isShowClosed = optional.Some(false)
+ }
+ // if there are closed issues and no open issues, default to showing all issues
if len(ctx.FormString("state")) == 0 && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 {
- isShowClosed = true
+ isShowClosed = optional.None[bool]()
}
if repo.IsTimetrackerEnabled(ctx) {
@@ -260,10 +273,13 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
}
var total int
- if !isShowClosed {
- total = int(issueStats.OpenCount)
- } else {
+ switch {
+ case isShowClosed.Value():
total = int(issueStats.ClosedCount)
+ case !isShowClosed.Has():
+ total = int(issueStats.OpenCount + issueStats.ClosedCount)
+ default:
+ total = int(issueStats.OpenCount)
}
pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
@@ -282,7 +298,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
ReviewedID: reviewedID,
MilestoneIDs: mileIDs,
ProjectID: projectID,
- IsClosed: util.OptionalBoolOf(isShowClosed),
+ IsClosed: isShowClosed,
IsPull: isPullOption,
LabelIDs: labelIDs,
SortType: sortType,
@@ -308,15 +324,15 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
return
}
- // Get posters.
- for i := range issues {
- // Check read status
- if !ctx.IsSigned {
- issues[i].IsRead = true
- } else if err = issues[i].GetIsRead(ctx, ctx.Doer.ID); err != nil {
- ctx.ServerError("GetIsRead", err)
+ if ctx.IsSigned {
+ if err := issues.LoadIsRead(ctx, ctx.Doer.ID); err != nil {
+ ctx.ServerError("LoadIsRead", err)
return
}
+ } else {
+ for i := range issues {
+ issues[i].IsRead = true
+ }
}
commitStatuses, lastStatus, err := pull_service.GetIssuesAllCommitStatus(ctx, issues)
@@ -416,7 +432,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
return
}
- pinned, err := issues_model.GetPinnedIssues(ctx, repo.ID, isPullOption.IsTrue())
+ pinned, err := issues_model.GetPinnedIssues(ctx, repo.ID, isPullOption.Value())
if err != nil {
ctx.ServerError("GetPinnedIssues", err)
return
@@ -428,6 +444,9 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
ctx.Data["OpenCount"] = issueStats.OpenCount
ctx.Data["ClosedCount"] = issueStats.ClosedCount
linkStr := "%s?q=%s&type=%s&sort=%s&state=%s&labels=%s&milestone=%d&project=%d&assignee=%d&poster=%d&archived=%t"
+ ctx.Data["AllStatesLink"] = fmt.Sprintf(linkStr, ctx.Link,
+ url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "all", url.QueryEscape(selectLabels),
+ mentionedID, projectID, assigneeID, posterID, archived)
ctx.Data["OpenLink"] = fmt.Sprintf(linkStr, ctx.Link,
url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "open", url.QueryEscape(selectLabels),
mentionedID, projectID, assigneeID, posterID, archived)
@@ -442,25 +461,27 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
ctx.Data["ProjectID"] = projectID
ctx.Data["AssigneeID"] = assigneeID
ctx.Data["PosterID"] = posterID
- ctx.Data["IsShowClosed"] = isShowClosed
ctx.Data["Keyword"] = keyword
- if isShowClosed {
+ switch {
+ case isShowClosed.Value():
ctx.Data["State"] = "closed"
- } else {
+ case !isShowClosed.Has():
+ ctx.Data["State"] = "all"
+ default:
ctx.Data["State"] = "open"
}
ctx.Data["ShowArchivedLabels"] = archived
- pager.AddParam(ctx, "q", "Keyword")
- pager.AddParam(ctx, "type", "ViewType")
- pager.AddParam(ctx, "sort", "SortType")
- pager.AddParam(ctx, "state", "State")
- pager.AddParam(ctx, "labels", "SelectLabels")
- pager.AddParam(ctx, "milestone", "MilestoneID")
- pager.AddParam(ctx, "project", "ProjectID")
- pager.AddParam(ctx, "assignee", "AssigneeID")
- pager.AddParam(ctx, "poster", "PosterID")
- pager.AddParam(ctx, "archived", "ShowArchivedLabels")
+ pager.AddParamString("q", keyword)
+ pager.AddParamString("type", viewType)
+ pager.AddParamString("sort", sortType)
+ pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
+ pager.AddParamString("labels", fmt.Sprint(selectLabels))
+ pager.AddParamString("milestone", fmt.Sprint(milestoneID))
+ pager.AddParamString("project", fmt.Sprint(projectID))
+ pager.AddParamString("assignee", fmt.Sprint(assigneeID))
+ pager.AddParamString("poster", fmt.Sprint(posterID))
+ pager.AddParamString("archived", fmt.Sprint(archived))
ctx.Data["Page"] = pager
}
@@ -493,7 +514,7 @@ func Issues(ctx *context.Context) {
ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
}
- issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), util.OptionalBoolOf(isPullList))
+ issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), optional.Some(isPullList))
if ctx.Written() {
return
}
@@ -510,9 +531,8 @@ func Issues(ctx *context.Context) {
func renderMilestones(ctx *context.Context) {
// Get milestones
- milestones, _, err := issues_model.GetMilestones(ctx, issues_model.GetMilestonesOption{
+ milestones, err := db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
RepoID: ctx.Repo.Repository.ID,
- State: api.StateAll,
})
if err != nil {
ctx.ServerError("GetAllRepoMilestones", err)
@@ -534,17 +554,17 @@ func renderMilestones(ctx *context.Context) {
// RetrieveRepoMilestonesAndAssignees find all the milestones and assignees of a repository
func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.Repository) {
var err error
- ctx.Data["OpenMilestones"], _, err = issues_model.GetMilestones(ctx, issues_model.GetMilestonesOption{
- RepoID: repo.ID,
- State: api.StateOpen,
+ ctx.Data["OpenMilestones"], err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
+ RepoID: repo.ID,
+ IsClosed: optional.Some(false),
})
if err != nil {
ctx.ServerError("GetMilestones", err)
return
}
- ctx.Data["ClosedMilestones"], _, err = issues_model.GetMilestones(ctx, issues_model.GetMilestonesOption{
- RepoID: repo.ID,
- State: api.StateClosed,
+ ctx.Data["ClosedMilestones"], err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
+ RepoID: repo.ID,
+ IsClosed: optional.Some(true),
})
if err != nil {
ctx.ServerError("GetMilestones", err)
@@ -568,52 +588,63 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
if repo.Owner.IsOrganization() {
repoOwnerType = project_model.TypeOrganization
}
- var err error
- projects, _, err := project_model.FindProjects(ctx, project_model.SearchOptions{
- RepoID: repo.ID,
- Page: -1,
- IsClosed: util.OptionalBoolFalse,
- Type: project_model.TypeRepository,
- })
- if err != nil {
- ctx.ServerError("GetProjects", err)
- return
- }
- projects2, _, err := project_model.FindProjects(ctx, project_model.SearchOptions{
- OwnerID: repo.OwnerID,
- Page: -1,
- IsClosed: util.OptionalBoolFalse,
- Type: repoOwnerType,
- })
- if err != nil {
- ctx.ServerError("GetProjects", err)
- return
- }
- ctx.Data["OpenProjects"] = append(projects, projects2...)
+ projectsUnit := repo.MustGetUnit(ctx, unit.TypeProjects)
- projects, _, err = project_model.FindProjects(ctx, project_model.SearchOptions{
- RepoID: repo.ID,
- Page: -1,
- IsClosed: util.OptionalBoolTrue,
- Type: project_model.TypeRepository,
- })
- if err != nil {
- ctx.ServerError("GetProjects", err)
- return
+ var openProjects []*project_model.Project
+ var closedProjects []*project_model.Project
+ var err error
+
+ if projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) {
+ openProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
+ ListOptions: db.ListOptionsAll,
+ RepoID: repo.ID,
+ IsClosed: optional.Some(false),
+ Type: project_model.TypeRepository,
+ })
+ if err != nil {
+ ctx.ServerError("GetProjects", err)
+ return
+ }
+ closedProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
+ ListOptions: db.ListOptionsAll,
+ RepoID: repo.ID,
+ IsClosed: optional.Some(true),
+ Type: project_model.TypeRepository,
+ })
+ if err != nil {
+ ctx.ServerError("GetProjects", err)
+ return
+ }
}
- projects2, _, err = project_model.FindProjects(ctx, project_model.SearchOptions{
- OwnerID: repo.OwnerID,
- Page: -1,
- IsClosed: util.OptionalBoolTrue,
- Type: repoOwnerType,
- })
- if err != nil {
- ctx.ServerError("GetProjects", err)
- return
+
+ if projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeOwner) {
+ openProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
+ ListOptions: db.ListOptionsAll,
+ OwnerID: repo.OwnerID,
+ IsClosed: optional.Some(false),
+ Type: repoOwnerType,
+ })
+ if err != nil {
+ ctx.ServerError("GetProjects", err)
+ return
+ }
+ openProjects = append(openProjects, openProjects2...)
+ closedProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
+ ListOptions: db.ListOptionsAll,
+ OwnerID: repo.OwnerID,
+ IsClosed: optional.Some(true),
+ Type: repoOwnerType,
+ })
+ if err != nil {
+ ctx.ServerError("GetProjects", err)
+ return
+ }
+ closedProjects = append(closedProjects, closedProjects2...)
}
- ctx.Data["ClosedProjects"] = append(projects, projects2...)
+ ctx.Data["OpenProjects"] = openProjects
+ ctx.Data["ClosedProjects"] = closedProjects
}
// repoReviewerSelection items to bee shown
@@ -696,16 +727,12 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
tmp.ItemID = -review.ReviewerTeamID
}
- if ctx.Repo.IsAdmin() {
- // Admin can dismiss or re-request any review requests
+ if canChooseReviewer {
+ // Users who can choose reviewers can also remove review requests
tmp.CanChange = true
} else if ctx.Doer != nil && ctx.Doer.ID == review.ReviewerID && review.Type == issues_model.ReviewTypeRequest {
// A user can refuse review requests
tmp.CanChange = true
- } else if (canChooseReviewer || (ctx.Doer != nil && ctx.Doer.ID == issue.PosterID)) && review.Type != issues_model.ReviewTypeRequest &&
- ctx.Doer.ID != review.ReviewerID {
- // The poster of the PR, a manager, or official reviewers can re-request review from other reviewers
- tmp.CanChange = true
}
pullReviews = append(pullReviews, tmp)
@@ -978,17 +1005,17 @@ func NewIssue(ctx *context.Context) {
}
ctx.Data["Tags"] = tags
- _, templateErrs := issue_service.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
+ ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates)
for k, v := range errs {
- templateErrs[k] = v
+ ret.TemplateErrors[k] = v
}
if ctx.Written() {
return
}
- if len(templateErrs) > 0 {
- ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
+ if len(ret.TemplateErrors) > 0 {
+ ctx.Flash.Warning(renderErrorOfTemplates(ctx, ret.TemplateErrors), true)
}
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypeIssues)
@@ -1002,7 +1029,7 @@ func NewIssue(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplIssueNew)
}
-func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) string {
+func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) template.HTML {
var files []string
for k := range errs {
files = append(files, k)
@@ -1014,14 +1041,14 @@ func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) string
lines = append(lines, fmt.Sprintf("%s: %v", file, errs[file]))
}
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.issues.choose.ignore_invalid_templates"),
"Summary": ctx.Tr("repo.issues.choose.invalid_templates", len(errs)),
"Details": utils.SanitizeFlashErrorString(strings.Join(lines, "\n")),
})
if err != nil {
log.Debug("render flash error: %v", err)
- flashError = ctx.Tr("repo.issues.choose.ignore_invalid_templates")
+ flashError = ctx.Locale.Tr("repo.issues.choose.ignore_invalid_templates")
}
return flashError
}
@@ -1031,11 +1058,11 @@ func NewIssueChooseTemplate(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
ctx.Data["PageIsIssueList"] = true
- issueTemplates, errs := issue_service.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
- ctx.Data["IssueTemplates"] = issueTemplates
+ ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
+ ctx.Data["IssueTemplates"] = ret.IssueTemplates
- if len(errs) > 0 {
- ctx.Flash.Warning(renderErrorOfTemplates(ctx, errs), true)
+ if len(ret.TemplateErrors) > 0 {
+ ctx.Flash.Warning(renderErrorOfTemplates(ctx, ret.TemplateErrors), true)
}
if !issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) {
@@ -1197,6 +1224,14 @@ func NewIssuePost(ctx *context.Context) {
return
}
+ if projectID > 0 {
+ if !ctx.Repo.CanRead(unit.TypeProjects) {
+ // User must also be able to see the project.
+ ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects")
+ return
+ }
+ }
+
if setting.Attachment.Enabled {
attachments = form.Files
}
@@ -1229,27 +1264,17 @@ func NewIssuePost(ctx *context.Context) {
Ref: form.Ref,
}
- if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
+ if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs, projectID); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
- return
+ } else if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.JSONError(ctx.Tr("repo.issues.new.blocked_user"))
+ } else {
+ ctx.ServerError("NewIssue", err)
}
- ctx.ServerError("NewIssue", err)
return
}
- if projectID > 0 {
- if !ctx.Repo.CanRead(unit.TypeProjects) {
- // User must also be able to see the project.
- ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects")
- return
- }
- if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil {
- ctx.ServerError("ChangeProjectAssign", err)
- return
- }
- }
-
log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
if ctx.FormString("redirect_after_creation") == "project" && projectID > 0 {
ctx.JSONRedirect(ctx.Repo.RepoLink + "/projects/" + strconv.FormatInt(projectID, 10))
@@ -1319,7 +1344,7 @@ func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *use
return roleDescriptor, err
} else if hasMergedPR {
roleDescriptor.RoleInRepo = issues_model.RoleRepoContributor
- } else {
+ } else if issue.IsPull {
// only display first time contributor in the first opening pull request
roleDescriptor.RoleInRepo = issues_model.RoleRepoFirstTimeContributor
}
@@ -1424,7 +1449,7 @@ func ViewIssue(ctx *context.Context) {
return
}
- ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
+ ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, emoji.ReplaceAliases(issue.Title))
iw := new(issues_model.IssueWatch)
if ctx.Doer != nil {
@@ -1437,12 +1462,13 @@ func ViewIssue(ctx *context.Context) {
}
}
ctx.Data["IssueWatch"] = iw
-
issue.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
- URLPrefix: ctx.Repo.RepoLink,
- Metas: ctx.Repo.Repository.ComposeMetas(ctx),
- GitRepo: ctx.Repo.GitRepo,
- Ctx: ctx,
+ Links: markup.Links{
+ Base: ctx.Repo.RepoLink,
+ },
+ Metas: ctx.Repo.Repository.ComposeMetas(ctx),
+ GitRepo: ctx.Repo.GitRepo,
+ Ctx: ctx,
}, issue.Content)
if err != nil {
ctx.ServerError("RenderString", err)
@@ -1509,18 +1535,9 @@ func ViewIssue(ctx *context.Context) {
}
if issue.IsPull {
- canChooseReviewer := ctx.Repo.CanWrite(unit.TypePullRequests)
+ canChooseReviewer := false
if ctx.Doer != nil && ctx.IsSigned {
- if !canChooseReviewer {
- canChooseReviewer = ctx.Doer.ID == issue.PosterID
- }
- if !canChooseReviewer {
- canChooseReviewer, err = issues_model.IsOfficialReviewer(ctx, issue, ctx.Doer)
- if err != nil {
- ctx.ServerError("IsOfficialReviewer", err)
- return
- }
- }
+ canChooseReviewer = issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, repo, issue)
}
RetrieveRepoReviewers(ctx, repo, issue, canChooseReviewer)
@@ -1587,25 +1604,27 @@ func ViewIssue(ctx *context.Context) {
// Render comments and and fetch participants.
participants[0] = issue.Poster
+
+ if err := issue.Comments.LoadAttachmentsByIssue(ctx); err != nil {
+ ctx.ServerError("LoadAttachmentsByIssue", err)
+ return
+ }
+ if err := issue.Comments.LoadPosters(ctx); err != nil {
+ ctx.ServerError("LoadPosters", err)
+ return
+ }
+
for _, comment = range issue.Comments {
comment.Issue = issue
- if err := comment.LoadPoster(ctx); err != nil {
- ctx.ServerError("LoadPoster", err)
- return
- }
-
if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview {
- if err := comment.LoadAttachments(ctx); err != nil {
- ctx.ServerError("LoadAttachments", err)
- return
- }
-
comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
- URLPrefix: ctx.Repo.RepoLink,
- Metas: ctx.Repo.Repository.ComposeMetas(ctx),
- GitRepo: ctx.Repo.GitRepo,
- Ctx: ctx,
+ Links: markup.Links{
+ Base: ctx.Repo.RepoLink,
+ },
+ Metas: ctx.Repo.Repository.ComposeMetas(ctx),
+ GitRepo: ctx.Repo.GitRepo,
+ Ctx: ctx,
}, comment.Content)
if err != nil {
ctx.ServerError("RenderString", err)
@@ -1637,7 +1656,7 @@ func ViewIssue(ctx *context.Context) {
}
ghostMilestone := &issues_model.Milestone{
ID: -1,
- Name: ctx.Tr("repo.issues.deleted_milestone"),
+ Name: ctx.Locale.TrString("repo.issues.deleted_milestone"),
}
if comment.OldMilestoneID > 0 && comment.OldMilestone == nil {
comment.OldMilestone = ghostMilestone
@@ -1646,7 +1665,6 @@ func ViewIssue(ctx *context.Context) {
comment.Milestone = ghostMilestone
}
} else if comment.Type == issues_model.CommentTypeProject {
-
if err = comment.LoadProject(ctx); err != nil {
ctx.ServerError("LoadProject", err)
return
@@ -1654,7 +1672,7 @@ func ViewIssue(ctx *context.Context) {
ghostProject := &project_model.Project{
ID: -1,
- Title: ctx.Tr("repo.issues.deleted_project"),
+ Title: ctx.Locale.TrString("repo.issues.deleted_project"),
}
if comment.OldProjectID > 0 && comment.OldProject == nil {
@@ -1679,10 +1697,12 @@ func ViewIssue(ctx *context.Context) {
}
} else if comment.Type.HasContentSupport() {
comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
- URLPrefix: ctx.Repo.RepoLink,
- Metas: ctx.Repo.Repository.ComposeMetas(ctx),
- GitRepo: ctx.Repo.GitRepo,
- Ctx: ctx,
+ Links: markup.Links{
+ Base: ctx.Repo.RepoLink,
+ },
+ Metas: ctx.Repo.Repository.ComposeMetas(ctx),
+ GitRepo: ctx.Repo.GitRepo,
+ Ctx: ctx,
}, comment.Content)
if err != nil {
ctx.ServerError("RenderString", err)
@@ -1747,7 +1767,7 @@ func ViewIssue(ctx *context.Context) {
// so "|" is used as delimeter to mark the new format
if comment.Content[0] != '|' {
// handle old time comments that have formatted text stored
- comment.RenderedContent = comment.Content
+ comment.RenderedContent = templates.SanitizeHTML(comment.Content)
comment.Content = ""
} else {
// else it's just a duration in seconds to pass on to the frontend
@@ -1842,6 +1862,8 @@ func ViewIssue(ctx *context.Context) {
mergeStyle = repo_model.MergeStyleRebaseMerge
} else if prConfig.AllowSquash {
mergeStyle = repo_model.MergeStyleSquash
+ } else if prConfig.AllowFastForwardOnly {
+ mergeStyle = repo_model.MergeStyleFastForwardOnly
} else if prConfig.AllowManualMerge {
mergeStyle = repo_model.MergeStyleManuallyMerged
}
@@ -1963,7 +1985,7 @@ func ViewIssue(ctx *context.Context) {
return
}
- ctx.Data["BlockingDependencies"], ctx.Data["BlockingByDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blocking)
+ ctx.Data["BlockingDependencies"], ctx.Data["BlockingDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blocking)
if ctx.Written() {
return
}
@@ -2019,43 +2041,43 @@ func ViewIssue(ctx *context.Context) {
}
ctx.Data["Tags"] = tags
+ ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
+ return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
+ }
+
ctx.HTML(http.StatusOK, tplIssueView)
}
// checkBlockedByIssues return canRead and notPermitted
func checkBlockedByIssues(ctx *context.Context, blockers []*issues_model.DependencyInfo) (canRead, notPermitted []*issues_model.DependencyInfo) {
- var (
- lastRepoID int64
- lastPerm access_model.Permission
- )
- for i, blocker := range blockers {
+ repoPerms := make(map[int64]access_model.Permission)
+ repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission
+ for _, blocker := range blockers {
// Get the permissions for this repository
- perm := lastPerm
- if lastRepoID != blocker.Repository.ID {
- if blocker.Repository.ID == ctx.Repo.Repository.ID {
- perm = ctx.Repo.Permission
- } else {
- var err error
- perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer)
- if err != nil {
- ctx.ServerError("GetUserRepoPermission", err)
- return nil, nil
- }
+ // If the repo ID exists in the map, return the exist permissions
+ // else get the permission and add it to the map
+ var perm access_model.Permission
+ existPerm, ok := repoPerms[blocker.RepoID]
+ if ok {
+ perm = existPerm
+ } else {
+ var err error
+ perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer)
+ if err != nil {
+ ctx.ServerError("GetUserRepoPermission", err)
+ return nil, nil
}
- lastRepoID = blocker.Repository.ID
+ repoPerms[blocker.RepoID] = perm
}
-
- // check permission
- if !perm.CanReadIssuesOrPulls(blocker.Issue.IsPull) {
- blockers[len(notPermitted)], blockers[i] = blocker, blockers[len(notPermitted)]
- notPermitted = blockers[:len(notPermitted)+1]
+ if perm.CanReadIssuesOrPulls(blocker.Issue.IsPull) {
+ canRead = append(canRead, blocker)
+ } else {
+ notPermitted = append(notPermitted, blocker)
}
}
- blockers = blockers[len(notPermitted):]
- sortDependencyInfo(blockers)
+ sortDependencyInfo(canRead)
sortDependencyInfo(notPermitted)
-
- return blockers, notPermitted
+ return canRead, notPermitted
}
func sortDependencyInfo(blockers []*issues_model.DependencyInfo) {
@@ -2226,7 +2248,11 @@ func UpdateIssueContent(ctx *context.Context) {
}
if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, ctx.Req.FormValue("content")); err != nil {
- ctx.ServerError("ChangeContent", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.JSONError(ctx.Tr("repo.issues.edit.blocked_user"))
+ } else {
+ ctx.ServerError("ChangeContent", err)
+ }
return
}
@@ -2239,10 +2265,12 @@ func UpdateIssueContent(ctx *context.Context) {
}
content, err := markdown.RenderString(&markup.RenderContext{
- URLPrefix: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ?
- Metas: ctx.Repo.Repository.ComposeMetas(ctx),
- GitRepo: ctx.Repo.GitRepo,
- Ctx: ctx,
+ Links: markup.Links{
+ Base: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ?
+ },
+ Metas: ctx.Repo.Repository.ComposeMetas(ctx),
+ GitRepo: ctx.Repo.GitRepo,
+ Ctx: ctx,
}, issue.Content)
if err != nil {
ctx.ServerError("RenderString", err)
@@ -2487,14 +2515,14 @@ func SearchIssues(ctx *context.Context) {
return
}
- var isClosed util.OptionalBool
+ var isClosed optional.Option[bool]
switch ctx.FormString("state") {
case "closed":
- isClosed = util.OptionalBoolTrue
+ isClosed = optional.Some(true)
case "all":
- isClosed = util.OptionalBoolNone
+ isClosed = optional.None[bool]()
default:
- isClosed = util.OptionalBoolFalse
+ isClosed = optional.Some(false)
}
var (
@@ -2507,7 +2535,7 @@ func SearchIssues(ctx *context.Context) {
Private: false,
AllPublic: true,
TopicOnly: false,
- Collaborate: util.OptionalBoolNone,
+ Collaborate: optional.None[bool](),
// This needs to be a column that is not nil in fixtures or
// MySQL will return different results when sorting by null in some cases
OrderBy: db.SearchOrderByAlphabetically,
@@ -2530,7 +2558,7 @@ func SearchIssues(ctx *context.Context) {
opts.OwnerID = owner.ID
opts.AllLimited = false
opts.AllPublic = false
- opts.Collaborate = util.OptionalBoolFalse
+ opts.Collaborate = optional.Some(false)
}
if ctx.FormString("team") != "" {
if ctx.FormString("owner") == "" {
@@ -2569,14 +2597,12 @@ func SearchIssues(ctx *context.Context) {
keyword = ""
}
- var isPull util.OptionalBool
+ isPull := optional.None[bool]()
switch ctx.FormString("type") {
case "pulls":
- isPull = util.OptionalBoolTrue
+ isPull = optional.Some(true)
case "issues":
- isPull = util.OptionalBoolFalse
- default:
- isPull = util.OptionalBoolNone
+ isPull = optional.Some(false)
}
var includedAnyLabels []int64
@@ -2608,9 +2634,9 @@ func SearchIssues(ctx *context.Context) {
}
}
- var projectID *int64
+ projectID := optional.None[int64]()
if v := ctx.FormInt64("project"); v > 0 {
- projectID = &v
+ projectID = optional.Some(v)
}
// this api is also used in UI,
@@ -2639,28 +2665,28 @@ func SearchIssues(ctx *context.Context) {
}
if since != 0 {
- searchOpt.UpdatedAfterUnix = &since
+ searchOpt.UpdatedAfterUnix = optional.Some(since)
}
if before != 0 {
- searchOpt.UpdatedBeforeUnix = &before
+ searchOpt.UpdatedBeforeUnix = optional.Some(before)
}
if ctx.IsSigned {
ctxUserID := ctx.Doer.ID
if ctx.FormBool("created") {
- searchOpt.PosterID = &ctxUserID
+ searchOpt.PosterID = optional.Some(ctxUserID)
}
if ctx.FormBool("assigned") {
- searchOpt.AssigneeID = &ctxUserID
+ searchOpt.AssigneeID = optional.Some(ctxUserID)
}
if ctx.FormBool("mentioned") {
- searchOpt.MentionID = &ctxUserID
+ searchOpt.MentionID = optional.Some(ctxUserID)
}
if ctx.FormBool("review_requested") {
- searchOpt.ReviewRequestedID = &ctxUserID
+ searchOpt.ReviewRequestedID = optional.Some(ctxUserID)
}
if ctx.FormBool("reviewed") {
- searchOpt.ReviewedID = &ctxUserID
+ searchOpt.ReviewedID = optional.Some(ctxUserID)
}
}
@@ -2711,14 +2737,14 @@ func ListIssues(ctx *context.Context) {
return
}
- var isClosed util.OptionalBool
+ var isClosed optional.Option[bool]
switch ctx.FormString("state") {
case "closed":
- isClosed = util.OptionalBoolTrue
+ isClosed = optional.Some(true)
case "all":
- isClosed = util.OptionalBoolNone
+ isClosed = optional.None[bool]()
default:
- isClosed = util.OptionalBoolFalse
+ isClosed = optional.Some(false)
}
keyword := ctx.FormTrim("q")
@@ -2765,19 +2791,17 @@ func ListIssues(ctx *context.Context) {
}
}
- var projectID *int64
+ projectID := optional.None[int64]()
if v := ctx.FormInt64("project"); v > 0 {
- projectID = &v
+ projectID = optional.Some(v)
}
- var isPull util.OptionalBool
+ isPull := optional.None[bool]()
switch ctx.FormString("type") {
case "pulls":
- isPull = util.OptionalBoolTrue
+ isPull = optional.Some(true)
case "issues":
- isPull = util.OptionalBoolFalse
- default:
- isPull = util.OptionalBoolNone
+ isPull = optional.Some(false)
}
// FIXME: we should be more efficient here
@@ -2807,10 +2831,10 @@ func ListIssues(ctx *context.Context) {
SortBy: issue_indexer.SortByCreatedDesc,
}
if since != 0 {
- searchOpt.UpdatedAfterUnix = &since
+ searchOpt.UpdatedAfterUnix = optional.Some(since)
}
if before != 0 {
- searchOpt.UpdatedBeforeUnix = &before
+ searchOpt.UpdatedBeforeUnix = optional.Some(before)
}
if len(labelIDs) == 1 && labelIDs[0] == 0 {
searchOpt.NoLabelOnly = true
@@ -2831,13 +2855,13 @@ func ListIssues(ctx *context.Context) {
}
if createdByID > 0 {
- searchOpt.PosterID = &createdByID
+ searchOpt.PosterID = optional.Some(createdByID)
}
if assignedByID > 0 {
- searchOpt.AssigneeID = &assignedByID
+ searchOpt.AssigneeID = optional.Some(assignedByID)
}
if mentionedByID > 0 {
- searchOpt.MentionID = &mentionedByID
+ searchOpt.MentionID = optional.Some(mentionedByID)
}
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
@@ -3086,7 +3110,11 @@ func NewComment(ctx *context.Context) {
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
if err != nil {
- ctx.ServerError("CreateIssueComment", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
+ } else {
+ ctx.ServerError("CreateIssueComment", err)
+ }
return
}
@@ -3106,6 +3134,11 @@ func UpdateCommentContent(ctx *context.Context) {
return
}
+ if comment.Issue.RepoID != ctx.Repo.Repository.ID {
+ ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
+ return
+ }
+
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
ctx.Error(http.StatusForbidden)
return
@@ -3125,7 +3158,11 @@ func UpdateCommentContent(ctx *context.Context) {
return
}
if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
- ctx.ServerError("UpdateComment", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
+ } else {
+ ctx.ServerError("UpdateComment", err)
+ }
return
}
@@ -3143,10 +3180,12 @@ func UpdateCommentContent(ctx *context.Context) {
}
content, err := markdown.RenderString(&markup.RenderContext{
- URLPrefix: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ?
- Metas: ctx.Repo.Repository.ComposeMetas(ctx),
- GitRepo: ctx.Repo.GitRepo,
- Ctx: ctx,
+ Links: markup.Links{
+ Base: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ?
+ },
+ Metas: ctx.Repo.Repository.ComposeMetas(ctx),
+ GitRepo: ctx.Repo.GitRepo,
+ Ctx: ctx,
}, comment.Content)
if err != nil {
ctx.ServerError("RenderString", err)
@@ -3172,6 +3211,11 @@ func DeleteComment(ctx *context.Context) {
return
}
+ if comment.Issue.RepoID != ctx.Repo.Repository.ID {
+ ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
+ return
+ }
+
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
ctx.Error(http.StatusForbidden)
return
@@ -3226,9 +3270,9 @@ func ChangeIssueReaction(ctx *context.Context) {
switch ctx.Params(":action") {
case "react":
- reaction, err := issues_model.CreateIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Content)
+ reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Content)
if err != nil {
- if issues_model.IsErrForbiddenIssueReaction(err) {
+ if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
ctx.ServerError("ChangeIssueReaction", err)
return
}
@@ -3270,7 +3314,7 @@ func ChangeIssueReaction(ctx *context.Context) {
return
}
- html, err := ctx.RenderToString(tplReactions, map[string]any{
+ html, err := ctx.RenderToHTML(tplReactions, map[string]any{
"ctxData": ctx.Data,
"ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index),
"Reactions": issue.Reactions.GroupByType(),
@@ -3298,6 +3342,11 @@ func ChangeCommentReaction(ctx *context.Context) {
return
}
+ if comment.Issue.RepoID != ctx.Repo.Repository.ID {
+ ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
+ return
+ }
+
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull)) {
if log.IsTrace() {
if ctx.IsSigned {
@@ -3328,9 +3377,9 @@ func ChangeCommentReaction(ctx *context.Context) {
switch ctx.Params(":action") {
case "react":
- reaction, err := issues_model.CreateCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Content)
+ reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Content)
if err != nil {
- if issues_model.IsErrForbiddenIssueReaction(err) {
+ if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
ctx.ServerError("ChangeIssueReaction", err)
return
}
@@ -3372,7 +3421,7 @@ func ChangeCommentReaction(ctx *context.Context) {
return
}
- html, err := ctx.RenderToString(tplReactions, map[string]any{
+ html, err := ctx.RenderToHTML(tplReactions, map[string]any{
"ctxData": ctx.Data,
"ActionURL": fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID),
"Reactions": comment.Reactions.GroupByType(),
@@ -3441,6 +3490,21 @@ func GetCommentAttachments(ctx *context.Context) {
return
}
+ if err := comment.LoadIssue(ctx); err != nil {
+ ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
+ return
+ }
+
+ if comment.Issue.RepoID != ctx.Repo.Repository.ID {
+ ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
+ return
+ }
+
+ if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
+ ctx.NotFound("CanReadIssuesOrPulls", issues_model.ErrCommentNotExist{})
+ return
+ }
+
if !comment.Type.HasAttachmentSupport() {
ctx.ServerError("GetCommentAttachments", fmt.Errorf("comment type %v does not support attachments", comment.Type))
return
@@ -3500,8 +3564,8 @@ func updateAttachments(ctx *context.Context, item any, files []string) error {
return err
}
-func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) string {
- attachHTML, err := ctx.RenderToString(tplAttachment, map[string]any{
+func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) template.HTML {
+ attachHTML, err := ctx.RenderToHTML(tplAttachment, map[string]any{
"ctxData": ctx.Data,
"Attachments": attachments,
"Content": content,
diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go
index 5c378fe9d79..1ec497658f9 100644
--- a/routers/web/repo/issue_content_history.go
+++ b/routers/web/repo/issue_content_history.go
@@ -11,11 +11,11 @@ import (
"code.gitea.io/gitea/models/avatars"
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/services/context"
"github.com/sergi/go-diff/diffmatchpatch"
)
@@ -56,12 +56,12 @@ func GetContentHistoryList(ctx *context.Context) {
for _, item := range items {
var actionText string
if item.IsDeleted {
- actionTextDeleted := ctx.Locale.Tr("repo.issues.content_history.deleted")
+ actionTextDeleted := ctx.Locale.TrString("repo.issues.content_history.deleted")
actionText = "" + actionTextDeleted + ""
} else if item.IsFirstCreated {
- actionText = ctx.Locale.Tr("repo.issues.content_history.created")
+ actionText = ctx.Locale.TrString("repo.issues.content_history.created")
} else {
- actionText = ctx.Locale.Tr("repo.issues.content_history.edited")
+ actionText = ctx.Locale.TrString("repo.issues.content_history.edited")
}
username := item.UserName
@@ -94,7 +94,7 @@ func canSoftDeleteContentHistory(ctx *context.Context, issue *issues_model.Issue
// CanWrite means the doer can manage the issue/PR list
if ctx.Repo.IsOwner() || ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
canSoftDelete = true
- } else {
+ } else if ctx.Doer != nil {
// for read-only users, they could still post issues or comments,
// they should be able to delete the history related to their own issue/comment, a case is:
// 1. the user posts some sensitive data
@@ -122,7 +122,7 @@ func GetContentHistoryDetail(ctx *context.Context) {
}
historyID := ctx.FormInt64("history_id")
- history, prevHistory, err := issues_model.GetIssueContentHistoryAndPrev(ctx, historyID)
+ history, prevHistory, err := issues_model.GetIssueContentHistoryAndPrev(ctx, issue.ID, historyID)
if err != nil {
ctx.JSON(http.StatusNotFound, map[string]any{
"message": "Can not find the content history",
@@ -186,6 +186,10 @@ func SoftDeleteContentHistory(ctx *context.Context) {
if ctx.Written() {
return
}
+ if ctx.Doer == nil {
+ ctx.NotFound("Require SignIn", nil)
+ return
+ }
commentID := ctx.FormInt64("comment_id")
historyID := ctx.FormInt64("history_id")
@@ -193,15 +197,29 @@ func SoftDeleteContentHistory(ctx *context.Context) {
var comment *issues_model.Comment
var history *issues_model.ContentHistory
var err error
+
+ if history, err = issues_model.GetIssueContentHistoryByID(ctx, historyID); err != nil {
+ log.Error("can not get issue content history %v. err=%v", historyID, err)
+ return
+ }
+ if history.IssueID != issue.ID {
+ ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
+ return
+ }
if commentID != 0 {
+ if history.CommentID != commentID {
+ ctx.NotFound("CompareCommentID", issues_model.ErrCommentNotExist{})
+ return
+ }
+
if comment, err = issues_model.GetCommentByID(ctx, commentID); err != nil {
log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
return
}
- }
- if history, err = issues_model.GetIssueContentHistoryByID(ctx, historyID); err != nil {
- log.Error("can not get issue content history %v. err=%v", historyID, err)
- return
+ if comment.IssueID != issue.ID {
+ ctx.NotFound("CompareIssueID", issues_model.ErrCommentNotExist{})
+ return
+ }
}
canSoftDelete := canSoftDeleteContentHistory(ctx, issue, comment, history)
diff --git a/routers/web/repo/issue_dependency.go b/routers/web/repo/issue_dependency.go
index 022ec3ae3e2..e3b85ee638e 100644
--- a/routers/web/repo/issue_dependency.go
+++ b/routers/web/repo/issue_dependency.go
@@ -8,8 +8,8 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
)
// AddDependency adds new dependencies
diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go
index dd3e2803b4e..81bee4dbb53 100644
--- a/routers/web/repo/issue_label.go
+++ b/routers/web/repo/issue_label.go
@@ -10,12 +10,11 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/label"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
issue_service "code.gitea.io/gitea/services/issue"
)
@@ -112,12 +111,11 @@ func NewLabel(ctx *context.Context) {
}
l := &issues_model.Label{
- RepoID: ctx.Repo.Repository.ID,
- Name: form.Title,
- Exclusive: form.Exclusive,
- Description: form.Description,
- Color: form.Color,
- ArchivedUnix: timeutil.TimeStamp(0),
+ RepoID: ctx.Repo.Repository.ID,
+ Name: form.Title,
+ Exclusive: form.Exclusive,
+ Description: form.Description,
+ Color: form.Color,
}
if err := issues_model.NewLabel(ctx, l); err != nil {
ctx.ServerError("NewLabel", err)
diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go
index e0d49e44e18..93fc72300b3 100644
--- a/routers/web/repo/issue_label_test.go
+++ b/routers/web/repo/issue_label_test.go
@@ -10,10 +10,10 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/contexttest"
"code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/contexttest"
"code.gitea.io/gitea/services/forms"
"github.com/stretchr/testify/assert"
@@ -123,7 +123,7 @@ func TestDeleteLabel(t *testing.T) {
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
unittest.AssertNotExistsBean(t, &issues_model.Label{ID: 2})
unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{LabelID: 2})
- assert.Equal(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg)
+ assert.EqualValues(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg)
}
func TestUpdateIssueLabel_Clear(t *testing.T) {
diff --git a/routers/web/repo/issue_lock.go b/routers/web/repo/issue_lock.go
index f83109d9b3f..1d5fc8a5f39 100644
--- a/routers/web/repo/issue_lock.go
+++ b/routers/web/repo/issue_lock.go
@@ -5,8 +5,8 @@ package repo
import (
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
diff --git a/routers/web/repo/issue_pin.go b/routers/web/repo/issue_pin.go
index f853f72335e..365c812681a 100644
--- a/routers/web/repo/issue_pin.go
+++ b/routers/web/repo/issue_pin.go
@@ -7,9 +7,9 @@ import (
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/services/context"
)
// IssuePinOrUnpin pin or unpin a Issue
@@ -90,6 +90,12 @@ func IssuePinMove(ctx *context.Context) {
return
}
+ if issue.RepoID != ctx.Repo.Repository.ID {
+ ctx.Status(http.StatusNotFound)
+ log.Error("Issue does not belong to this repository")
+ return
+ }
+
err = issue.MovePin(ctx, form.Position)
if err != nil {
ctx.Status(http.StatusInternalServerError)
diff --git a/routers/web/repo/issue_stopwatch.go b/routers/web/repo/issue_stopwatch.go
index ab9fe3e69d9..70d42b27c0a 100644
--- a/routers/web/repo/issue_stopwatch.go
+++ b/routers/web/repo/issue_stopwatch.go
@@ -9,8 +9,8 @@ import (
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/eventsource"
+ "code.gitea.io/gitea/services/context"
)
// IssueStopwatch creates or stops a stopwatch for the given issue.
diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go
index c9bf861b844..241e4340499 100644
--- a/routers/web/repo/issue_timetrack.go
+++ b/routers/web/repo/issue_timetrack.go
@@ -9,9 +9,9 @@ import (
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
diff --git a/routers/web/repo/issue_watch.go b/routers/web/repo/issue_watch.go
index 1cb5cc7162d..8b033f3b17a 100644
--- a/routers/web/repo/issue_watch.go
+++ b/routers/web/repo/issue_watch.go
@@ -8,8 +8,13 @@ import (
"strconv"
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/services/context"
+)
+
+const (
+ tplWatching base.TplName = "repo/issue/view_content/watching"
)
// IssueWatch sets issue watching
@@ -52,5 +57,7 @@ func IssueWatch(ctx *context.Context) {
return
}
- ctx.Redirect(issue.Link())
+ ctx.Data["Issue"] = issue
+ ctx.Data["IssueWatch"] = &issues_model.IssueWatch{IsWatching: watch}
+ ctx.HTML(http.StatusOK, tplWatching)
}
diff --git a/routers/web/repo/middlewares.go b/routers/web/repo/middlewares.go
index b50e96be3c4..420931c5fb7 100644
--- a/routers/web/repo/middlewares.go
+++ b/routers/web/repo/middlewares.go
@@ -9,14 +9,15 @@ import (
system_model "code.gitea.io/gitea/models/system"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/optional"
+ "code.gitea.io/gitea/services/context"
+ user_service "code.gitea.io/gitea/services/user"
)
// SetEditorconfigIfExists set editor config as render variable
func SetEditorconfigIfExists(ctx *context.Context) {
if ctx.Repo.Repository.IsEmpty {
- ctx.Data["Editorconfig"] = nil
return
}
@@ -56,8 +57,12 @@ func SetDiffViewStyle(ctx *context.Context) {
}
ctx.Data["IsSplitStyle"] = style == "split"
- if err := user_model.UpdateUserDiffViewStyle(ctx, ctx.Doer, style); err != nil {
- ctx.ServerError("ErrUpdateDiffViewStyle", err)
+
+ opts := &user_service.UpdateOptions{
+ DiffViewStyle: optional.Some(style),
+ }
+ if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
+ ctx.ServerError("UpdateUser", err)
}
}
@@ -93,23 +98,15 @@ func SetWhitespaceBehavior(ctx *context.Context) {
// 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
+ showOutdatedCommentsValue = "true"
if ctx.IsSigned {
- showOutdatedCommentsValue, _ = user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyShowOutdatedComments, "false")
- } else {
- // not logged in user -> use the default value
- showOutdatedCommentsValue = "false"
+ showOutdatedCommentsValue, _ = user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyShowOutdatedComments, showOutdatedCommentsValue)
}
- } else {
+ } else if ctx.IsSigned {
// valid value -> update user setting if user is logged in
- if ctx.IsSigned {
- _ = user_model.SetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyShowOutdatedComments, showOutdatedCommentsValue)
- }
+ _ = user_model.SetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyShowOutdatedComments, showOutdatedCommentsValue)
}
-
- showOutdatedComments, _ := strconv.ParseBool(showOutdatedCommentsValue)
- ctx.Data["ShowOutdatedComments"] = showOutdatedComments
+ ctx.Data["ShowOutdatedComments"], _ = strconv.ParseBool(showOutdatedCommentsValue)
}
diff --git a/routers/web/repo/migrate.go b/routers/web/repo/migrate.go
index b70901d5f28..97b0c425ea3 100644
--- a/routers/web/repo/migrate.go
+++ b/routers/web/repo/migrate.go
@@ -15,13 +15,13 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/migrations"
"code.gitea.io/gitea/services/task"
diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go
index 0e6f6307474..95a4fe60cc1 100644
--- a/routers/web/repo/milestone.go
+++ b/routers/web/repo/milestone.go
@@ -12,14 +12,13 @@ import (
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/issue"
@@ -46,18 +45,13 @@ func Milestones(ctx *context.Context) {
page = 1
}
- state := structs.StateOpen
- if isShowClosed {
- state = structs.StateClosed
- }
-
- miles, total, err := issues_model.GetMilestones(ctx, issues_model.GetMilestonesOption{
+ miles, total, err := db.FindAndCount[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
ListOptions: db.ListOptions{
Page: page,
PageSize: setting.UI.IssuePagingNum,
},
RepoID: ctx.Repo.Repository.ID,
- State: state,
+ IsClosed: optional.Some(isShowClosed),
SortType: sortType,
Name: keyword,
})
@@ -80,17 +74,19 @@ func Milestones(ctx *context.Context) {
url.QueryEscape(keyword), url.QueryEscape(sortType))
if ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
- if err := miles.LoadTotalTrackedTimes(ctx); err != nil {
+ if err := issues_model.MilestoneList(miles).LoadTotalTrackedTimes(ctx); err != nil {
ctx.ServerError("LoadTotalTrackedTimes", err)
return
}
}
for _, m := range miles {
m.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
- URLPrefix: ctx.Repo.RepoLink,
- Metas: ctx.Repo.Repository.ComposeMetas(ctx),
- GitRepo: ctx.Repo.GitRepo,
- Ctx: ctx,
+ Links: markup.Links{
+ Base: ctx.Repo.RepoLink,
+ },
+ Metas: ctx.Repo.Repository.ComposeMetas(ctx),
+ GitRepo: ctx.Repo.GitRepo,
+ Ctx: ctx,
}, m.Content)
if err != nil {
ctx.ServerError("RenderString", err)
@@ -110,8 +106,8 @@ func Milestones(ctx *context.Context) {
ctx.Data["IsShowClosed"] = isShowClosed
pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, 5)
- pager.AddParam(ctx, "state", "State")
- pager.AddParam(ctx, "q", "Keyword")
+ pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
+ pager.AddParamString("q", keyword)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplMilestone)
@@ -281,10 +277,12 @@ func MilestoneIssuesAndPulls(ctx *context.Context) {
}
milestone.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
- URLPrefix: ctx.Repo.RepoLink,
- Metas: ctx.Repo.Repository.ComposeMetas(ctx),
- GitRepo: ctx.Repo.GitRepo,
- Ctx: ctx,
+ Links: markup.Links{
+ Base: ctx.Repo.RepoLink,
+ },
+ Metas: ctx.Repo.Repository.ComposeMetas(ctx),
+ GitRepo: ctx.Repo.GitRepo,
+ Ctx: ctx,
}, milestone.Content)
if err != nil {
ctx.ServerError("RenderString", err)
@@ -294,10 +292,10 @@ func MilestoneIssuesAndPulls(ctx *context.Context) {
ctx.Data["Title"] = milestone.Name
ctx.Data["Milestone"] = milestone
- issues(ctx, milestoneID, projectID, util.OptionalBoolNone)
+ issues(ctx, milestoneID, projectID, optional.None[bool]())
- ret, _ := issue.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
- ctx.Data["NewIssueChooseTemplate"] = len(ret) > 0
+ ret := issue.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
+ ctx.Data["NewIssueChooseTemplate"] = len(ret.IssueTemplates) > 0
ctx.Data["CanWriteIssues"] = ctx.Repo.CanWriteIssuesOrPulls(false)
ctx.Data["CanWritePulls"] = ctx.Repo.CanWriteIssuesOrPulls(true)
diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go
index ac9e64d774e..57e578da373 100644
--- a/routers/web/repo/packages.go
+++ b/routers/web/repo/packages.go
@@ -10,9 +10,9 @@ import (
"code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -37,7 +37,7 @@ func Packages(ctx *context.Context) {
RepoID: ctx.Repo.Repository.ID,
Type: packages.Type(packageType),
Name: packages.SearchValue{Value: query},
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
})
if err != nil {
ctx.ServerError("SearchLatestVersions", err)
@@ -70,8 +70,8 @@ func Packages(ctx *context.Context) {
ctx.Data["RepositoryAccessMap"] = map[int64]bool{ctx.Repo.Repository.ID: true} // There is only the current repository
pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5)
- pager.AddParam(ctx, "q", "Query")
- pager.AddParam(ctx, "type", "PackageType")
+ pager.AddParamString("q", query)
+ pager.AddParamString("type", packageType)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplPackagesList)
diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go
index c04435cf1b4..0dee02dd9ca 100644
--- a/routers/web/repo/patch.go
+++ b/routers/web/repo/patch.go
@@ -10,10 +10,10 @@ import (
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/repository/files"
)
@@ -79,7 +79,7 @@ func NewDiffPatchPost(ctx *context.Context) {
// `message` will be both the summary and message combined
message := strings.TrimSpace(form.CommitSummary)
if len(message) == 0 {
- message = ctx.Tr("repo.editor.patch")
+ message = ctx.Locale.TrString("repo.editor.patch")
}
form.CommitMessage = strings.TrimSpace(form.CommitMessage)
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index 6417024f8ba..2cba5c0970b 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -10,19 +10,20 @@ import (
"net/url"
"strings"
+ "code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/perm"
project_model "code.gitea.io/gitea/models/project"
- attachment_model "code.gitea.io/gitea/models/repo"
+ repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
@@ -32,16 +33,17 @@ const (
tplProjectsView base.TplName = "repo/projects/view"
)
-// MustEnableProjects check if projects are enabled in settings
-func MustEnableProjects(ctx *context.Context) {
+// MustEnableRepoProjects check if repo projects are enabled in settings
+func MustEnableRepoProjects(ctx *context.Context) {
if unit.TypeProjects.UnitGlobalDisabled() {
ctx.NotFound("EnableKanbanBoard", nil)
return
}
if ctx.Repo.Repository != nil {
- if !ctx.Repo.CanRead(unit.TypeProjects) {
- ctx.NotFound("MustEnableProjects", nil)
+ projectsUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeProjects)
+ if !ctx.Repo.CanRead(unit.TypeProjects) || !projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) {
+ ctx.NotFound("MustEnableRepoProjects", nil)
return
}
}
@@ -71,10 +73,13 @@ func Projects(ctx *context.Context) {
total = repo.NumClosedProjects
}
- projects, count, err := project_model.FindProjects(ctx, project_model.SearchOptions{
+ projects, count, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{
+ ListOptions: db.ListOptions{
+ PageSize: setting.UI.IssuePagingNum,
+ Page: page,
+ },
RepoID: repo.ID,
- Page: page,
- IsClosed: util.OptionalBoolOf(isShowClosed),
+ IsClosed: optional.Some(isShowClosed),
OrderBy: project_model.GetSearchOrderByBySortType(sortType),
Type: project_model.TypeRepository,
Title: keyword,
@@ -86,10 +91,12 @@ func Projects(ctx *context.Context) {
for i := range projects {
projects[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{
- URLPrefix: ctx.Repo.RepoLink,
- Metas: ctx.Repo.Repository.ComposeMetas(ctx),
- GitRepo: ctx.Repo.GitRepo,
- Ctx: ctx,
+ Links: markup.Links{
+ Base: ctx.Repo.RepoLink,
+ },
+ Metas: ctx.Repo.Repository.ComposeMetas(ctx),
+ GitRepo: ctx.Repo.GitRepo,
+ Ctx: ctx,
}, projects[i].Description)
if err != nil {
ctx.ServerError("RenderString", err)
@@ -111,7 +118,7 @@ func Projects(ctx *context.Context) {
}
pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, numPages)
- pager.AddParam(ctx, "state", "State")
+ pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
ctx.Data["Page"] = pager
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
@@ -309,7 +316,7 @@ func ViewProject(ctx *context.Context) {
}
if boards[0].ID == 0 {
- boards[0].Title = ctx.Tr("repo.projects.type.uncategorized")
+ boards[0].Title = ctx.Locale.TrString("repo.projects.type.uncategorized")
}
issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
@@ -319,10 +326,10 @@ func ViewProject(ctx *context.Context) {
}
if project.CardType != project_model.CardTypeTextOnly {
- issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment)
+ issuesAttachmentMap := make(map[int64][]*repo_model.Attachment)
for _, issuesList := range issuesMap {
for _, issue := range issuesList {
- if issueAttachment, err := attachment_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil {
+ if issueAttachment, err := repo_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil {
issuesAttachmentMap[issue.ID] = issueAttachment
}
}
@@ -333,17 +340,17 @@ func ViewProject(ctx *context.Context) {
linkedPrsMap := make(map[int64][]*issues_model.Issue)
for _, issuesList := range issuesMap {
for _, issue := range issuesList {
- var referencedIds []int64
+ var referencedIDs []int64
for _, comment := range issue.Comments {
if comment.RefIssueID != 0 && comment.RefIsPull {
- referencedIds = append(referencedIds, comment.RefIssueID)
+ referencedIDs = append(referencedIDs, comment.RefIssueID)
}
}
- if len(referencedIds) > 0 {
+ if len(referencedIDs) > 0 {
if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
- IssueIDs: referencedIds,
- IsPull: util.OptionalBoolTrue,
+ IssueIDs: referencedIDs,
+ IsPull: optional.Some(true),
}); err == nil {
linkedPrsMap[issue.ID] = linkedPrs
}
@@ -353,10 +360,12 @@ func ViewProject(ctx *context.Context) {
ctx.Data["LinkedPRs"] = linkedPrsMap
project.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
- URLPrefix: ctx.Repo.RepoLink,
- Metas: ctx.Repo.Repository.ComposeMetas(ctx),
- GitRepo: ctx.Repo.GitRepo,
- Ctx: ctx,
+ Links: markup.Links{
+ Base: ctx.Repo.RepoLink,
+ },
+ Metas: ctx.Repo.Repository.ComposeMetas(ctx),
+ GitRepo: ctx.Repo.GitRepo,
+ Ctx: ctx,
}, project.Description)
if err != nil {
ctx.ServerError("RenderString", err)
@@ -464,7 +473,7 @@ func AddBoardToProjectPost(ctx *context.Context) {
return
}
- project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
+ project, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
if err != nil {
if project_model.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
@@ -625,7 +634,7 @@ func MoveIssues(ctx *context.Context) {
board = &project_model.Board{
ID: 0,
ProjectID: project.ID,
- Title: ctx.Tr("repo.projects.type.uncategorized"),
+ Title: ctx.Locale.TrString("repo.projects.type.uncategorized"),
}
} else {
board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
diff --git a/routers/web/repo/projects_test.go b/routers/web/repo/projects_test.go
index 6698d47028b..479f8c55a22 100644
--- a/routers/web/repo/projects_test.go
+++ b/routers/web/repo/projects_test.go
@@ -7,7 +7,7 @@ import (
"testing"
"code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/contexttest"
+ "code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
)
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index ec109ed665c..447781602d8 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -10,7 +10,6 @@ import (
"fmt"
"html"
"net/http"
- "net/url"
"strconv"
"strings"
"time"
@@ -20,36 +19,36 @@ import (
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
pull_model "code.gitea.io/gitea/models/pull"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/emoji"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/gitrepo"
issue_template "code.gitea.io/gitea/modules/issue/template"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/utils"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/automerge"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/gitdiff"
notify_service "code.gitea.io/gitea/services/notify"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
+ user_service "code.gitea.io/gitea/services/user"
"github.com/gobwas/glob"
)
const (
- tplFork base.TplName = "repo/pulls/fork"
tplCompareDiff base.TplName = "repo/diff/compare"
tplPullCommits base.TplName = "repo/pulls/commits"
tplPullFiles base.TplName = "repo/pulls/files"
@@ -108,213 +107,6 @@ func getRepository(ctx *context.Context, repoID int64) *repo_model.Repository {
return repo
}
-func getForkRepository(ctx *context.Context) *repo_model.Repository {
- forkRepo := getRepository(ctx, ctx.ParamsInt64(":repoid"))
- if ctx.Written() {
- return nil
- }
-
- if forkRepo.IsEmpty {
- log.Trace("Empty repository %-v", forkRepo)
- ctx.NotFound("getForkRepository", nil)
- return nil
- }
-
- if err := forkRepo.LoadOwner(ctx); err != nil {
- ctx.ServerError("LoadOwner", err)
- return nil
- }
-
- ctx.Data["repo_name"] = forkRepo.Name
- ctx.Data["description"] = forkRepo.Description
- ctx.Data["IsPrivate"] = forkRepo.IsPrivate || forkRepo.Owner.Visibility == structs.VisibleTypePrivate
- canForkToUser := forkRepo.OwnerID != ctx.Doer.ID && !repo_model.HasForkedRepo(ctx, ctx.Doer.ID, forkRepo.ID)
-
- ctx.Data["ForkRepo"] = forkRepo
-
- ownedOrgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx, ctx.Doer.ID)
- if err != nil {
- ctx.ServerError("GetOrgsCanCreateRepoByUserID", err)
- return nil
- }
- var orgs []*organization.Organization
- for _, org := range ownedOrgs {
- if forkRepo.OwnerID != org.ID && !repo_model.HasForkedRepo(ctx, org.ID, forkRepo.ID) {
- orgs = append(orgs, org)
- }
- }
-
- traverseParentRepo := forkRepo
- for {
- if ctx.Doer.ID == traverseParentRepo.OwnerID {
- canForkToUser = false
- } else {
- for i, org := range orgs {
- if org.ID == traverseParentRepo.OwnerID {
- orgs = append(orgs[:i], orgs[i+1:]...)
- break
- }
- }
- }
-
- if !traverseParentRepo.IsFork {
- break
- }
- traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID)
- if err != nil {
- ctx.ServerError("GetRepositoryByID", err)
- return nil
- }
- }
-
- ctx.Data["CanForkToUser"] = canForkToUser
- ctx.Data["Orgs"] = orgs
-
- if canForkToUser {
- ctx.Data["ContextUser"] = ctx.Doer
- } else if len(orgs) > 0 {
- ctx.Data["ContextUser"] = orgs[0]
- } else {
- ctx.Data["CanForkRepo"] = false
- ctx.Flash.Error(ctx.Tr("repo.fork_no_valid_owners"), true)
- return nil
- }
-
- branches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
- RepoID: ctx.Repo.Repository.ID,
- ListOptions: db.ListOptions{
- ListAll: true,
- },
- IsDeletedBranch: util.OptionalBoolFalse,
- // Add it as the first option
- ExcludeBranchNames: []string{ctx.Repo.Repository.DefaultBranch},
- })
- if err != nil {
- ctx.ServerError("FindBranchNames", err)
- return nil
- }
- ctx.Data["Branches"] = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
-
- return forkRepo
-}
-
-// Fork render repository fork page
-func Fork(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("new_fork")
-
- if ctx.Doer.CanForkRepo() {
- ctx.Data["CanForkRepo"] = true
- } else {
- maxCreationLimit := ctx.Doer.MaxCreationLimit()
- msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
- ctx.Flash.Error(msg, true)
- }
-
- getForkRepository(ctx)
- if ctx.Written() {
- return
- }
-
- ctx.HTML(http.StatusOK, tplFork)
-}
-
-// ForkPost response for forking a repository
-func ForkPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.CreateRepoForm)
- ctx.Data["Title"] = ctx.Tr("new_fork")
- ctx.Data["CanForkRepo"] = true
-
- ctxUser := checkContextUser(ctx, form.UID)
- if ctx.Written() {
- return
- }
-
- forkRepo := getForkRepository(ctx)
- if ctx.Written() {
- return
- }
-
- ctx.Data["ContextUser"] = ctxUser
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplFork)
- return
- }
-
- var err error
- traverseParentRepo := forkRepo
- for {
- if ctxUser.ID == traverseParentRepo.OwnerID {
- ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
- return
- }
- repo := repo_model.GetForkedRepo(ctx, ctxUser.ID, traverseParentRepo.ID)
- if repo != nil {
- ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
- return
- }
- if !traverseParentRepo.IsFork {
- break
- }
- traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID)
- if err != nil {
- ctx.ServerError("GetRepositoryByID", err)
- return
- }
- }
-
- // Check if user is allowed to create repo's on the organization.
- if ctxUser.IsOrganization() {
- isAllowedToFork, err := organization.OrgFromUser(ctxUser).CanCreateOrgRepo(ctx, ctx.Doer.ID)
- if err != nil {
- ctx.ServerError("CanCreateOrgRepo", err)
- return
- } else if !isAllowedToFork {
- ctx.Error(http.StatusForbidden)
- return
- }
- }
-
- repo, err := repo_service.ForkRepository(ctx, ctx.Doer, ctxUser, repo_service.ForkRepoOptions{
- BaseRepo: forkRepo,
- Name: form.RepoName,
- Description: form.Description,
- SingleBranch: form.ForkSingleBranch,
- })
- if err != nil {
- ctx.Data["Err_RepoName"] = true
- switch {
- case repo_model.IsErrReachLimitOfRepo(err):
- maxCreationLimit := ctxUser.MaxCreationLimit()
- msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
- ctx.RenderWithErr(msg, tplFork, &form)
- case repo_model.IsErrRepoAlreadyExist(err):
- ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
- case repo_model.IsErrRepoFilesAlreadyExist(err):
- switch {
- case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplFork, form)
- case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplFork, form)
- case setting.Repository.AllowDeleteOfUnadoptedRepositories:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplFork, form)
- default:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplFork, form)
- }
- case db.IsErrNameReserved(err):
- ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form)
- case db.IsErrNamePatternNotAllowed(err):
- ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form)
- default:
- ctx.ServerError("ForkPost", err)
- }
- return
- }
-
- log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
- ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
-}
-
func getPullInfo(ctx *context.Context) (issue *issues_model.Issue, ok bool) {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
@@ -333,7 +125,7 @@ func getPullInfo(ctx *context.Context) (issue *issues_model.Issue, ok bool) {
ctx.ServerError("LoadRepo", err)
return nil, false
}
- ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
+ ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, emoji.ReplaceAliases(issue.Title))
ctx.Data["Issue"] = issue
if !issue.IsPull {
@@ -530,7 +322,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
if pull.BaseRepoID == ctx.Repo.Repository.ID && ctx.Repo.GitRepo != nil {
baseGitRepo = ctx.Repo.GitRepo
} else {
- baseGitRepo, err := git.OpenRepository(ctx, pull.BaseRepo.RepoPath())
+ baseGitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo)
if err != nil {
ctx.ServerError("OpenRepository", err)
return nil
@@ -582,7 +374,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
var headBranchSha string
// HeadRepo may be missing
if pull.HeadRepo != nil {
- headGitRepo, err := git.OpenRepository(ctx, pull.HeadRepo.RepoPath())
+ headGitRepo, err := gitrepo.OpenRepository(ctx, pull.HeadRepo)
if err != nil {
ctx.ServerError("OpenRepository", err)
return nil
@@ -651,9 +443,35 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
}
if pb != nil && pb.EnableStatusCheck {
+
+ var missingRequiredChecks []string
+ for _, requiredContext := range pb.StatusCheckContexts {
+ contextFound := false
+ matchesRequiredContext := createRequiredContextMatcher(requiredContext)
+ for _, presentStatus := range commitStatuses {
+ if matchesRequiredContext(presentStatus.Context) {
+ contextFound = true
+ break
+ }
+ }
+
+ if !contextFound {
+ missingRequiredChecks = append(missingRequiredChecks, requiredContext)
+ }
+ }
+ ctx.Data["MissingRequiredChecks"] = missingRequiredChecks
+
ctx.Data["is_context_required"] = func(context string) bool {
for _, c := range pb.StatusCheckContexts {
- if gp, err := glob.Compile(c); err == nil && gp.Match(context) {
+ if c == context {
+ return true
+ }
+ if gp, err := glob.Compile(c); err != nil {
+ // All newly created status_check_contexts are checked to ensure they are valid glob expressions before being stored in the database.
+ // But some old status_check_context created before glob was introduced may be invalid glob expressions.
+ // So log the error here for debugging.
+ log.Error("compile glob %q: %v", c, err)
+ } else if gp.Match(context) {
return true
}
}
@@ -711,10 +529,22 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
return compareInfo
}
+func createRequiredContextMatcher(requiredContext string) func(string) bool {
+ if gp, err := glob.Compile(requiredContext); err == nil {
+ return func(contextToCheck string) bool {
+ return gp.Match(contextToCheck)
+ }
+ }
+
+ return func(contextToCheck string) bool {
+ return requiredContext == contextToCheck
+ }
+}
+
type pullCommitList struct {
Commits []pull_service.CommitInfo `json:"commits"`
LastReviewCommitSha string `json:"last_review_commit_sha"`
- Locale map[string]string `json:"locale"`
+ Locale map[string]any `json:"locale"`
}
// GetPullCommits get all commits for given pull request
@@ -732,7 +562,7 @@ func GetPullCommits(ctx *context.Context) {
}
// Get the needed locale
- resp.Locale = map[string]string{
+ resp.Locale = map[string]any{
"lang": ctx.Locale.Language(),
"show_all_commits": ctx.Tr("repo.pulls.show_all_commits"),
"stats_num_commits": ctx.TrN(len(commits), "repo.activity.git_stats_commit_1", "repo.activity.git_stats_commit_n", len(commits)),
@@ -929,6 +759,19 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
return
}
+ for _, file := range diff.Files {
+ for _, section := range file.Sections {
+ for _, line := range section.Lines {
+ for _, comment := range line.Comments {
+ if err := comment.LoadAttachments(ctx); err != nil {
+ ctx.ServerError("LoadAttachments", err)
+ return
+ }
+ }
+ }
+ }
+ }
+
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch)
if err != nil {
ctx.ServerError("LoadProtectedBranch", err)
@@ -1011,6 +854,10 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
}
upload.AddUploadContext(ctx, "comment")
+ ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
+ return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
+ }
+
ctx.HTML(http.StatusOK, tplPullFiles)
}
@@ -1075,7 +922,7 @@ func UpdatePullRequest(ctx *context.Context) {
if err = pull_service.Update(ctx, issue.PullRequest, ctx.Doer, message, rebase); err != nil {
if models.IsErrMergeConflicts(err) {
conflictError := err.(models.ErrMergeConflicts)
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.pulls.merge_conflict"),
"Summary": ctx.Tr("repo.pulls.merge_conflict_summary"),
"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut),
@@ -1089,7 +936,7 @@ func UpdatePullRequest(ctx *context.Context) {
return
} else if models.IsErrRebaseConflicts(err) {
conflictError := err.(models.ErrRebaseConflicts)
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)),
"Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"),
"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut),
@@ -1141,30 +988,28 @@ func MergePullRequest(ctx *context.Context) {
switch {
case errors.Is(err, pull_service.ErrIsClosed):
if issue.IsPull {
- ctx.Flash.Error(ctx.Tr("repo.pulls.is_closed"))
+ ctx.JSONError(ctx.Tr("repo.pulls.is_closed"))
} else {
- ctx.Flash.Error(ctx.Tr("repo.issues.closed_title"))
+ ctx.JSONError(ctx.Tr("repo.issues.closed_title"))
}
case errors.Is(err, pull_service.ErrUserNotAllowedToMerge):
- ctx.Flash.Error(ctx.Tr("repo.pulls.update_not_allowed"))
+ ctx.JSONError(ctx.Tr("repo.pulls.update_not_allowed"))
case errors.Is(err, pull_service.ErrHasMerged):
- ctx.Flash.Error(ctx.Tr("repo.pulls.has_merged"))
+ ctx.JSONError(ctx.Tr("repo.pulls.has_merged"))
case errors.Is(err, pull_service.ErrIsWorkInProgress):
- ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_wip"))
+ ctx.JSONError(ctx.Tr("repo.pulls.no_merge_wip"))
case errors.Is(err, pull_service.ErrNotMergableState):
- ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_not_ready"))
+ ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready"))
case models.IsErrDisallowedToMerge(err):
- ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_not_ready"))
+ ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready"))
case asymkey_service.IsErrWontSign(err):
- ctx.Flash.Error(err.Error()) // has no translation ...
+ ctx.JSONError(err.Error()) // has no translation ...
case errors.Is(err, pull_service.ErrDependenciesLeft):
- ctx.Flash.Error(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
+ ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
default:
ctx.ServerError("WebCheck", err)
- return
}
- ctx.Redirect(issue.Link())
return
}
@@ -1172,18 +1017,18 @@ func MergePullRequest(ctx *context.Context) {
if manuallyMerged {
if err := pull_service.MergedManually(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, form.MergeCommitID); err != nil {
switch {
-
case models.IsErrInvalidMergeStyle(err):
- ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option"))
+ ctx.JSONError(ctx.Tr("repo.pulls.invalid_merge_option"))
case strings.Contains(err.Error(), "Wrong commit ID"):
- ctx.Flash.Error(ctx.Tr("repo.pulls.wrong_commit_id"))
+ ctx.JSONError(ctx.Tr("repo.pulls.wrong_commit_id"))
default:
ctx.ServerError("MergedManually", err)
- return
}
+
+ return
}
- ctx.Redirect(issue.Link())
+ ctx.JSONRedirect(issue.Link())
return
}
@@ -1213,18 +1058,17 @@ func MergePullRequest(ctx *context.Context) {
} else if scheduled {
// nothing more to do ...
ctx.Flash.Success(ctx.Tr("repo.pulls.auto_merge_newly_scheduled"))
- ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, pr.Index))
+ ctx.JSONRedirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, pr.Index))
return
}
}
if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
if models.IsErrInvalidMergeStyle(err) {
- ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option"))
- ctx.Redirect(issue.Link())
+ ctx.JSONError(ctx.Tr("repo.pulls.invalid_merge_option"))
} else if models.IsErrMergeConflicts(err) {
conflictError := err.(models.ErrMergeConflicts)
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.editor.merge_conflict"),
"Summary": ctx.Tr("repo.editor.merge_conflict_summary"),
"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut),
@@ -1234,10 +1078,10 @@ func MergePullRequest(ctx *context.Context) {
return
}
ctx.Flash.Error(flashError)
- ctx.Redirect(issue.Link())
+ ctx.JSONRedirect(issue.Link())
} else if models.IsErrRebaseConflicts(err) {
conflictError := err.(models.ErrRebaseConflicts)
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)),
"Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"),
"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut),
@@ -1247,19 +1091,19 @@ func MergePullRequest(ctx *context.Context) {
return
}
ctx.Flash.Error(flashError)
- ctx.Redirect(issue.Link())
+ ctx.JSONRedirect(issue.Link())
} else if models.IsErrMergeUnrelatedHistories(err) {
log.Debug("MergeUnrelatedHistories error: %v", err)
ctx.Flash.Error(ctx.Tr("repo.pulls.unrelated_histories"))
- ctx.Redirect(issue.Link())
+ ctx.JSONRedirect(issue.Link())
} else if git.IsErrPushOutOfDate(err) {
log.Debug("MergePushOutOfDate error: %v", err)
ctx.Flash.Error(ctx.Tr("repo.pulls.merge_out_of_date"))
- ctx.Redirect(issue.Link())
+ ctx.JSONRedirect(issue.Link())
} else if models.IsErrSHADoesNotMatch(err) {
log.Debug("MergeHeadOutOfDate error: %v", err)
ctx.Flash.Error(ctx.Tr("repo.pulls.head_out_of_date"))
- ctx.Redirect(issue.Link())
+ ctx.JSONRedirect(issue.Link())
} else if git.IsErrPushRejected(err) {
log.Debug("MergePushRejected error: %v", err)
pushrejErr := err.(*git.ErrPushRejected)
@@ -1267,7 +1111,7 @@ func MergePullRequest(ctx *context.Context) {
if len(message) == 0 {
ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected_no_message"))
} else {
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.pulls.push_rejected"),
"Summary": ctx.Tr("repo.pulls.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(pushrejErr.Message),
@@ -1278,7 +1122,7 @@ func MergePullRequest(ctx *context.Context) {
}
ctx.Flash.Error(flashError)
}
- ctx.Redirect(issue.Link())
+ ctx.JSONRedirect(issue.Link())
} else {
ctx.ServerError("Merge", err)
}
@@ -1287,7 +1131,7 @@ func MergePullRequest(ctx *context.Context) {
log.Trace("Pull request merged: %d", pr.ID)
if err := stopTimerIfAvailable(ctx, ctx.Doer, issue); err != nil {
- ctx.ServerError("CreateOrStopIssueStopwatch", err)
+ ctx.ServerError("stopTimerIfAvailable", err)
return
}
@@ -1301,7 +1145,7 @@ func MergePullRequest(ctx *context.Context) {
return
}
if exist {
- ctx.Redirect(issue.Link())
+ ctx.JSONRedirect(issue.Link())
return
}
@@ -1309,9 +1153,9 @@ func MergePullRequest(ctx *context.Context) {
if ctx.Repo != nil && ctx.Repo.Repository != nil && pr.HeadRepoID == ctx.Repo.Repository.ID && ctx.Repo.GitRepo != nil {
headRepo = ctx.Repo.GitRepo
} else {
- headRepo, err = git.OpenRepository(ctx, pr.HeadRepo.RepoPath())
+ headRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo)
if err != nil {
- ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.RepoPath()), err)
+ ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.FullName()), err)
return
}
defer headRepo.Close()
@@ -1319,7 +1163,7 @@ func MergePullRequest(ctx *context.Context) {
deleteBranch(ctx, pr, headRepo)
}
- ctx.Redirect(issue.Link())
+ ctx.JSONRedirect(issue.Link())
}
// CancelAutoMergePullRequest cancels a scheduled pr
@@ -1379,7 +1223,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
return
}
- labelIDs, assigneeIDs, milestoneID, _ := ValidateRepoMetas(ctx, *form, true)
+ labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, true)
if ctx.Written() {
return
}
@@ -1432,7 +1276,6 @@ func CompareAndPullRequestPost(ctx *context.Context) {
if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
- return
} else if git.IsErrPushRejected(err) {
pushrejErr := err.(*git.ErrPushRejected)
message := pushrejErr.Message
@@ -1440,7 +1283,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
ctx.JSONError(ctx.Tr("repo.pulls.push_rejected_no_message"))
return
}
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.pulls.push_rejected"),
"Summary": ctx.Tr("repo.pulls.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(pushrejErr.Message),
@@ -1449,14 +1292,32 @@ func CompareAndPullRequestPost(ctx *context.Context) {
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.JSONError(flashError)
+ } else if errors.Is(err, user_model.ErrBlockedUser) {
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
+ "Message": ctx.Tr("repo.pulls.push_rejected"),
+ "Summary": ctx.Tr("repo.pulls.new.blocked_user"),
+ })
+ if err != nil {
+ ctx.ServerError("CompareAndPullRequest.HTMLString", err)
+ return
+ }
+ ctx.JSONError(flashError)
}
- ctx.ServerError("NewPullRequest", err)
return
}
+ if projectID > 0 {
+ if !ctx.Repo.CanWrite(unit.TypeProjects) {
+ ctx.Error(http.StatusBadRequest, "user hasn't the permission to write to projects")
+ return
+ }
+ if err := issues_model.ChangeProjectAssign(ctx, pullIssue, ctx.Doer, projectID); err != nil {
+ ctx.ServerError("ChangeProjectAssign", err)
+ return
+ }
+ }
+
log.Trace("Pull request created: %d/%d", repo.ID, pullIssue.ID)
ctx.JSONRedirect(pullIssue.Link())
}
@@ -1521,9 +1382,9 @@ func CleanUpPullRequest(ctx *context.Context) {
gitBaseRepo = ctx.Repo.GitRepo
} else {
// If not just open it
- gitBaseRepo, err = git.OpenRepository(ctx, pr.BaseRepo.RepoPath())
+ gitBaseRepo, err = gitrepo.OpenRepository(ctx, pr.BaseRepo)
if err != nil {
- ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.BaseRepo.RepoPath()), err)
+ ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.BaseRepo.FullName()), err)
return
}
defer gitBaseRepo.Close()
@@ -1536,9 +1397,9 @@ func CleanUpPullRequest(ctx *context.Context) {
gitRepo = ctx.Repo.GitRepo
} else if pr.BaseRepoID != pr.HeadRepoID {
// Otherwise just load it up
- gitRepo, err = git.OpenRepository(ctx, pr.HeadRepo.RepoPath())
+ gitRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo)
if err != nil {
- ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.RepoPath()), err)
+ ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.FullName()), err)
return
}
defer gitRepo.Close()
@@ -1571,6 +1432,12 @@ func CleanUpPullRequest(ctx *context.Context) {
func deleteBranch(ctx *context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository) {
fullBranchName := pr.HeadRepo.FullName() + ":" + pr.HeadBranch
+
+ if err := pull_service.RetargetChildrenOnMerge(ctx, ctx.Doer, pr); err != nil {
+ ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
+ return
+ }
+
if err := repo_service.DeleteBranch(ctx, ctx.Doer, pr.HeadRepo, gitRepo, pr.HeadBranch); err != nil {
switch {
case git.IsErrBranchNotExist(err):
diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go
index 1359af9d3bf..5385ebfc97a 100644
--- a/routers/web/repo/pull_review.go
+++ b/routers/web/repo/pull_review.go
@@ -10,19 +10,24 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
pull_model "code.gitea.io/gitea/models/pull"
+ user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/forms"
pull_service "code.gitea.io/gitea/services/pull"
+ user_service "code.gitea.io/gitea/services/user"
)
const (
- tplConversation base.TplName = "repo/diff/conversation"
- tplNewComment base.TplName = "repo/diff/new_comment"
+ tplDiffConversation base.TplName = "repo/diff/conversation"
+ tplConversationOutdated base.TplName = "repo/diff/conversation_outdated"
+ tplTimelineConversation base.TplName = "repo/issue/view_content/conversation"
+ tplNewComment base.TplName = "repo/diff/new_comment"
)
// RenderNewCodeCommentForm will render the form for creating a new review comment
@@ -48,6 +53,8 @@ func RenderNewCodeCommentForm(ctx *context.Context) {
return
}
ctx.Data["AfterCommitID"] = pullHeadCommitID
+ ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
+ upload.AddUploadContext(ctx, "comment")
ctx.HTML(http.StatusOK, tplNewComment)
}
@@ -73,6 +80,11 @@ func CreateCodeComment(ctx *context.Context) {
signedLine *= -1
}
+ var attachments []string
+ if setting.Attachment.Enabled {
+ attachments = form.Files
+ }
+
comment, err := pull_service.CreateCodeComment(ctx,
ctx.Doer,
ctx.Repo.GitRepo,
@@ -83,6 +95,7 @@ func CreateCodeComment(ctx *context.Context) {
!form.SingleReview,
form.Reply,
form.LatestCommitID,
+ attachments,
)
if err != nil {
ctx.ServerError("CreateCodeComment", err)
@@ -97,11 +110,7 @@ func CreateCodeComment(ctx *context.Context) {
log.Trace("Comment created: %-v #%d[%d] Comment[%d]", ctx.Repo.Repository, issue.Index, issue.ID, comment.ID)
- if form.Origin == "diff" {
- renderConversation(ctx, comment)
- return
- }
- ctx.Redirect(comment.Link(ctx))
+ renderConversation(ctx, comment, form.Origin)
}
// UpdateResolveConversation add or remove an Conversation resolved mark
@@ -152,22 +161,37 @@ func UpdateResolveConversation(ctx *context.Context) {
return
}
- if origin == "diff" {
- renderConversation(ctx, comment)
- return
- }
- ctx.JSONOK()
+ renderConversation(ctx, comment, origin)
}
-func renderConversation(ctx *context.Context, comment *issues_model.Comment) {
- comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line, ctx.Data["ShowOutdatedComments"].(bool))
+func renderConversation(ctx *context.Context, comment *issues_model.Comment, origin string) {
+ ctx.Data["PageIsPullFiles"] = origin == "diff"
+
+ showOutdatedComments := origin == "timeline" || ctx.Data["ShowOutdatedComments"].(bool)
+ comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line, showOutdatedComments)
if err != nil {
ctx.ServerError("FetchCodeCommentsByLine", err)
return
}
- ctx.Data["PageIsPullFiles"] = true
+ if len(comments) == 0 {
+ // if the comments are empty (deleted, outdated, etc), it's better to tell the users that it is outdated
+ ctx.HTML(http.StatusOK, tplConversationOutdated)
+ return
+ }
+
+ if err := comments.LoadAttachments(ctx); err != nil {
+ ctx.ServerError("LoadAttachments", err)
+ return
+ }
+
+ ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
+ upload.AddUploadContext(ctx, "comment")
+
ctx.Data["comments"] = comments
- ctx.Data["CanMarkConversation"] = true
+ if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, comment.Issue, ctx.Doer); err != nil {
+ ctx.ServerError("CanMarkConversation", err)
+ return
+ }
ctx.Data["Issue"] = comment.Issue
if err = comment.Issue.LoadPullRequest(ctx); err != nil {
ctx.ServerError("comment.Issue.LoadPullRequest", err)
@@ -179,7 +203,17 @@ func renderConversation(ctx *context.Context, comment *issues_model.Comment) {
return
}
ctx.Data["AfterCommitID"] = pullHeadCommitID
- ctx.HTML(http.StatusOK, tplConversation)
+ ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
+ return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
+ }
+
+ if origin == "diff" {
+ ctx.HTML(http.StatusOK, tplDiffConversation)
+ } else if origin == "timeline" {
+ ctx.HTML(http.StatusOK, tplTimelineConversation)
+ } else {
+ ctx.Error(http.StatusBadRequest, "Unknown origin: "+origin)
+ }
}
// SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
@@ -209,9 +243,9 @@ func SubmitReview(ctx *context.Context) {
if issue.IsPoster(ctx.Doer.ID) {
var translated string
if reviewType == issues_model.ReviewTypeApprove {
- translated = ctx.Tr("repo.issues.review.self.approval")
+ translated = ctx.Locale.TrString("repo.issues.review.self.approval")
} else {
- translated = ctx.Tr("repo.issues.review.self.rejection")
+ translated = ctx.Locale.TrString("repo.issues.review.self.rejection")
}
ctx.Flash.Error(translated)
diff --git a/routers/web/repo/pull_review_test.go b/routers/web/repo/pull_review_test.go
new file mode 100644
index 00000000000..5f035f1eb04
--- /dev/null
+++ b/routers/web/repo/pull_review_test.go
@@ -0,0 +1,76 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http/httptest"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/contexttest"
+ "code.gitea.io/gitea/services/pull"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRenderConversation(t *testing.T) {
+ unittest.PrepareTestEnv(t)
+
+ pr, _ := issues_model.GetPullRequestByID(db.DefaultContext, 2)
+ _ = pr.LoadIssue(db.DefaultContext)
+ _ = pr.Issue.LoadPoster(db.DefaultContext)
+ _ = pr.Issue.LoadRepo(db.DefaultContext)
+
+ run := func(name string, cb func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder)) {
+ t.Run(name, func(t *testing.T) {
+ ctx, resp := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
+ contexttest.LoadUser(t, ctx, pr.Issue.PosterID)
+ contexttest.LoadRepo(t, ctx, pr.BaseRepoID)
+ contexttest.LoadGitRepo(t, ctx)
+ defer ctx.Repo.GitRepo.Close()
+ cb(t, ctx, resp)
+ })
+ }
+
+ var preparedComment *issues_model.Comment
+ run("prepare", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) {
+ comment, err := pull.CreateCodeComment(ctx, pr.Issue.Poster, ctx.Repo.GitRepo, pr.Issue, 1, "content", "", false, 0, pr.HeadCommitID, nil)
+ if !assert.NoError(t, err) {
+ return
+ }
+ comment.Invalidated = true
+ err = issues_model.UpdateCommentInvalidate(ctx, comment)
+ if !assert.NoError(t, err) {
+ return
+ }
+ preparedComment = comment
+ })
+ if !assert.NotNil(t, preparedComment) {
+ return
+ }
+ run("diff with outdated", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) {
+ ctx.Data["ShowOutdatedComments"] = true
+ renderConversation(ctx, preparedComment, "diff")
+ assert.Contains(t, resp.Body.String(), `
diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl
index 7eb9d086e61..6bdda07e487 100644
--- a/templates/admin/config.tmpl
+++ b/templates/admin/config.tmpl
@@ -151,8 +151,6 @@
- {{ctx.Locale.Tr "admin.config.picture_config"}}
-
-
-
-
{{ctx.Locale.Tr "admin.config.git_config"}}
diff --git a/templates/admin/config_settings.tmpl b/templates/admin/config_settings.tmpl
new file mode 100644
index 00000000000..22ad5c24ac0
--- /dev/null
+++ b/templates/admin/config_settings.tmpl
@@ -0,0 +1,42 @@
+{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin config")}}
+
+ {{ctx.Locale.Tr "admin.config.picture_config"}}
+
+
+
+
+ {{ctx.Locale.Tr "repository"}}
+
+
@@ -63,6 +63,10 @@
{{ctx.Locale.Tr "admin.dashboard.sync_repo_branches"}}
+
+
@@ -71,69 +75,9 @@
{{ctx.Locale.Tr "admin.dashboard.sync_repo_tags"}}
+
+
{{ctx.Locale.Tr "admin.dashboard.system_status"}}
-
-
+ {{/* TODO: make these stats work in multi-server deployments, likely needs per-server stats in DB */}}
+
", repoLink, p.Pusher.UserName, commitDesc, branchLink) // for each commit, generate a new line text for i, commit := range p.Commits { - text += fmt.Sprintf("%s: %s - %s", MatrixLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name) + text += fmt.Sprintf("%s: %s - %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name) // add linebreak to each commit but the last if i < len(p.Commits)-1 { text += "
" @@ -163,41 +182,41 @@ func (m *MatrixPayload) Push(p *api.PushPayload) (api.Payloader, error) { } - return getMatrixPayload(text, p.Commits, m.MsgType), nil + return m.newPayload(text, p.Commits...) } -// PullRequest implements PayloadConvertor PullRequest method -func (m *MatrixPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { - text, _, _, _ := getPullRequestPayloadInfo(p, MatrixLinkFormatter, true) +// PullRequest implements payloadConvertor PullRequest method +func (m matrixConvertor) PullRequest(p *api.PullRequestPayload) (MatrixPayload, error) { + text, _, _, _ := getPullRequestPayloadInfo(p, htmlLinkFormatter, true) - return getMatrixPayload(text, nil, m.MsgType), nil + return m.newPayload(text) } -// Review implements PayloadConvertor Review method -func (m *MatrixPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) { - senderLink := MatrixLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName) +// Review implements payloadConvertor Review method +func (m matrixConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (MatrixPayload, error) { + senderLink := htmlLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName) title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title) - titleLink := MatrixLinkFormatter(p.PullRequest.HTMLURL, title) - repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName) + titleLink := htmlLinkFormatter(p.PullRequest.HTMLURL, title) + repoLink := htmlLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName) var text string switch p.Action { case api.HookIssueReviewed: action, err := parseHookPullRequestEventType(event) if err != nil { - return nil, err + return MatrixPayload{}, err } text = fmt.Sprintf("[%s] Pull request review %s: %s by %s", repoLink, action, titleLink, senderLink) } - return getMatrixPayload(text, nil, m.MsgType), nil + return m.newPayload(text) } -// Repository implements PayloadConvertor Repository method -func (m *MatrixPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { - senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) - repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName) +// Repository implements payloadConvertor Repository method +func (m matrixConvertor) Repository(p *api.RepositoryPayload) (MatrixPayload, error) { + senderLink := htmlLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) + repoLink := htmlLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName) var text string switch p.Action { @@ -206,47 +225,22 @@ func (m *MatrixPayload) Repository(p *api.RepositoryPayload) (api.Payloader, err case api.HookRepoDeleted: text = fmt.Sprintf("[%s] Repository deleted by %s", repoLink, senderLink) } - - return getMatrixPayload(text, nil, m.MsgType), nil + return m.newPayload(text) } -func (m *MatrixPayload) Package(p *api.PackagePayload) (api.Payloader, error) { - senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) - repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName) +func (m matrixConvertor) Package(p *api.PackagePayload) (MatrixPayload, error) { + senderLink := htmlLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) + packageLink := htmlLinkFormatter(p.Package.HTMLURL, p.Package.Name) var text string switch p.Action { case api.HookPackageCreated: - text = fmt.Sprintf("[%s] Package published by %s", repoLink, senderLink) + text = fmt.Sprintf("[%s] Package published by %s", packageLink, senderLink) case api.HookPackageDeleted: - text = fmt.Sprintf("[%s] Package deleted by %s", repoLink, senderLink) - } - - return getMatrixPayload(text, nil, m.MsgType), nil -} - -// GetMatrixPayload converts a Matrix webhook into a MatrixPayload -func GetMatrixPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) { - s := new(MatrixPayload) - - matrix := &MatrixMeta{} - if err := json.Unmarshal([]byte(meta), &matrix); err != nil { - return s, errors.New("GetMatrixPayload meta json:" + err.Error()) + text = fmt.Sprintf("[%s] Package deleted by %s", packageLink, senderLink) } - s.MsgType = messageTypeText[matrix.MessageType] - - return convertPayloader(s, p, event) -} - -func getMatrixPayload(text string, commits []*api.PayloadCommit, msgType string) *MatrixPayload { - p := MatrixPayload{} - p.FormattedBody = text - p.Body = getMessageBody(text) - p.Format = "org.matrix.custom.html" - p.MsgType = msgType - p.Commits = commits - return &p + return m.newPayload(text) } var urlRegex = regexp.MustCompile(`]*?href="([^">]*?)">(.*?)`) @@ -271,3 +265,16 @@ func getMatrixTxnID(payload []byte) (string, error) { return hex.EncodeToString(h.Sum(nil)), nil } + +// MatrixLinkToRef Matrix-formatter link to a repo ref +func MatrixLinkToRef(repoURL, ref string) string { + refName := git.RefName(ref).ShortName() + switch { + case strings.HasPrefix(ref, git.BranchPrefix): + return htmlLinkFormatter(repoURL+"/src/branch/"+util.PathEscapeSegments(refName), refName) + case strings.HasPrefix(ref, git.TagPrefix): + return htmlLinkFormatter(repoURL+"/src/tag/"+util.PathEscapeSegments(refName), refName) + default: + return htmlLinkFormatter(repoURL+"/src/commit/"+util.PathEscapeSegments(refName), refName) + } +} diff --git a/services/webhook/matrix_test.go b/services/webhook/matrix_test.go index 8c710942280..058f8e3c5f9 100644 --- a/services/webhook/matrix_test.go +++ b/services/webhook/matrix_test.go @@ -4,8 +4,11 @@ package webhook import ( + "context" "testing" + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -14,204 +17,213 @@ import ( ) func TestMatrixPayload(t *testing.T) { + mc := matrixConvertor{ + MsgType: "m.text", + } + t.Run("Create", func(t *testing.T) { p := createTestPayload() - d := new(MatrixPayload) - pl, err := d.Create(p) + pl, err := mc.Create(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):[test](http://localhost:3000/test/repo/src/branch/test)] branch created by user1", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo:test] branch created by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):[test](http://localhost:3000/test/repo/src/branch/test)] branch created by user1", pl.Body) + assert.Equal(t, `[test/repo:test] branch created by user1`, pl.FormattedBody) }) t.Run("Delete", func(t *testing.T) { p := deleteTestPayload() - d := new(MatrixPayload) - pl, err := d.Delete(p) + pl, err := mc.Delete(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):test] branch deleted by user1", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo:test] branch deleted by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):test] branch deleted by user1", pl.Body) + assert.Equal(t, `[test/repo:test] branch deleted by user1`, pl.FormattedBody) }) t.Run("Fork", func(t *testing.T) { p := forkTestPayload() - d := new(MatrixPayload) - pl, err := d.Fork(p) + pl, err := mc.Fork(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[test/repo2](http://localhost:3000/test/repo2) is forked to [test/repo](http://localhost:3000/test/repo)", pl.(*MatrixPayload).Body) - assert.Equal(t, `test/repo2 is forked to test/repo`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[test/repo2](http://localhost:3000/test/repo2) is forked to [test/repo](http://localhost:3000/test/repo)", pl.Body) + assert.Equal(t, `test/repo2 is forked to test/repo`, pl.FormattedBody) }) t.Run("Push", func(t *testing.T) { p := pushTestPayload() - d := new(MatrixPayload) - pl, err := d.Push(p) + pl, err := mc.Push(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] user1 pushed 2 commits to test:
2020558: commit message - user1
2020558: commit message - user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1", pl.Body) + assert.Equal(t, `[test/repo] user1 pushed 2 commits to test:
2020558: commit message - user1
2020558: commit message - user1`, pl.FormattedBody) }) t.Run("Issue", func(t *testing.T) { p := issueTestPayload() - d := new(MatrixPayload) p.Action = api.HookIssueOpened - pl, err := d.Issue(p) + pl, err := mc.Issue(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue opened: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Issue opened: #2 crash by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue opened: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] Issue opened: #2 crash by user1`, pl.FormattedBody) p.Action = api.HookIssueClosed - pl, err = d.Issue(p) + pl, err = mc.Issue(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue closed: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Issue closed: #2 crash by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue closed: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] Issue closed: #2 crash by user1`, pl.FormattedBody) }) t.Run("IssueComment", func(t *testing.T) { p := issueCommentTestPayload() - d := new(MatrixPayload) - pl, err := d.IssueComment(p) + pl, err := mc.IssueComment(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on issue [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] New comment on issue #2 crash by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on issue [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] New comment on issue #2 crash by user1`, pl.FormattedBody) }) t.Run("PullRequest", func(t *testing.T) { p := pullRequestTestPayload() - d := new(MatrixPayload) - pl, err := d.PullRequest(p) + pl, err := mc.PullRequest(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request opened: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Pull request opened: #12 Fix bug by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request opened: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] Pull request opened: #12 Fix bug by user1`, pl.FormattedBody) }) t.Run("PullRequestComment", func(t *testing.T) { p := pullRequestCommentTestPayload() - d := new(MatrixPayload) - pl, err := d.IssueComment(p) + pl, err := mc.IssueComment(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on pull request [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] New comment on pull request #12 Fix bug by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on pull request [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] New comment on pull request #12 Fix bug by user1`, pl.FormattedBody) }) t.Run("Review", func(t *testing.T) { p := pullRequestTestPayload() p.Action = api.HookIssueReviewed - d := new(MatrixPayload) - pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved) + pl, err := mc.Review(p, webhook_module.HookEventPullRequestReviewApproved) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Pull request review approved: #12 Fix bug by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] Pull request review approved: #12 Fix bug by user1`, pl.FormattedBody) }) t.Run("Repository", func(t *testing.T) { p := repositoryTestPayload() - d := new(MatrixPayload) - pl, err := d.Repository(p) + pl, err := mc.Repository(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, `[[test/repo](http://localhost:3000/test/repo)] Repository created by [user1](https://try.gitea.io/user1)`, pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Repository created by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, `[[test/repo](http://localhost:3000/test/repo)] Repository created by [user1](https://try.gitea.io/user1)`, pl.Body) + assert.Equal(t, `[test/repo] Repository created by user1`, pl.FormattedBody) + }) + + t.Run("Package", func(t *testing.T) { + p := packageTestPayload() + + pl, err := mc.Package(p) + require.NoError(t, err) + require.NotNil(t, pl) + + assert.Equal(t, `[[GiteaContainer](http://localhost:3000/user1/-/packages/container/GiteaContainer/latest)] Package published by [user1](https://try.gitea.io/user1)`, pl.Body) + assert.Equal(t, `[GiteaContainer] Package published by user1`, pl.FormattedBody) }) t.Run("Wiki", func(t *testing.T) { p := wikiTestPayload() - d := new(MatrixPayload) p.Action = api.HookWikiCreated - pl, err := d.Wiki(p) + pl, err := mc.Wiki(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New wiki page '[index](http://localhost:3000/test/repo/wiki/index)' (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] New wiki page 'index' (Wiki change comment) by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New wiki page '[index](http://localhost:3000/test/repo/wiki/index)' (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] New wiki page 'index' (Wiki change comment) by user1`, pl.FormattedBody) p.Action = api.HookWikiEdited - pl, err = d.Wiki(p) + pl, err = mc.Wiki(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' edited (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Wiki page 'index' edited (Wiki change comment) by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' edited (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] Wiki page 'index' edited (Wiki change comment) by user1`, pl.FormattedBody) p.Action = api.HookWikiDeleted - pl, err = d.Wiki(p) + pl, err = mc.Wiki(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' deleted by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Wiki page 'index' deleted by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' deleted by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] Wiki page 'index' deleted by user1`, pl.FormattedBody) }) t.Run("Release", func(t *testing.T) { p := pullReleaseTestPayload() - d := new(MatrixPayload) - pl, err := d.Release(p) + pl, err := mc.Release(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Release created: [v1.0](http://localhost:3000/test/repo/releases/tag/v1.0) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Release created: v1.0 by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Release created: [v1.0](http://localhost:3000/test/repo/releases/tag/v1.0) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] Release created: v1.0 by user1`, pl.FormattedBody) }) } func TestMatrixJSONPayload(t *testing.T) { p := pushTestPayload() - - pl, err := new(MatrixPayload).Push(p) + data, err := p.JSONPayload() require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - json, err := pl.JSONPayload() + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.MATRIX, + URL: "https://matrix.example.com/_matrix/client/r0/rooms/ROOM_ID/send/m.room.message", + Meta: `{"message_type":0}`, // text + } + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := newMatrixRequest(context.Background(), hook, task) + require.NotNil(t, req) + require.NotNil(t, reqBody) require.NoError(t, err) - assert.NotEmpty(t, json) + + assert.Equal(t, "PUT", req.Method) + assert.Equal(t, "/_matrix/client/r0/rooms/ROOM_ID/send/m.room.message/6db5dc1e282529a8c162c7fe93dd2667494eeb51", req.URL.Path) + assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256")) + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + var body MatrixPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1", body.Body) } func Test_getTxnID(t *testing.T) { diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go index f58da3fe1cd..99d01061844 100644 --- a/services/webhook/msteams.go +++ b/services/webhook/msteams.go @@ -4,12 +4,14 @@ package webhook import ( + "context" "fmt" + "net/http" "net/url" "strings" + webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -56,19 +58,8 @@ type ( } ) -// JSONPayload Marshals the MSTeamsPayload to json -func (m *MSTeamsPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(m, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - -var _ PayloadConvertor = &MSTeamsPayload{} - // Create implements PayloadConvertor Create method -func (m *MSTeamsPayload) Create(p *api.CreatePayload) (api.Payloader, error) { +func (m msteamsConvertor) Create(p *api.CreatePayload) (MSTeamsPayload, error) { // created tag/branch refName := git.RefName(p.Ref).ShortName() title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName) @@ -85,7 +76,7 @@ func (m *MSTeamsPayload) Create(p *api.CreatePayload) (api.Payloader, error) { } // Delete implements PayloadConvertor Delete method -func (m *MSTeamsPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { +func (m msteamsConvertor) Delete(p *api.DeletePayload) (MSTeamsPayload, error) { // deleted tag/branch refName := git.RefName(p.Ref).ShortName() title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName) @@ -102,7 +93,7 @@ func (m *MSTeamsPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { } // Fork implements PayloadConvertor Fork method -func (m *MSTeamsPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { +func (m msteamsConvertor) Fork(p *api.ForkPayload) (MSTeamsPayload, error) { title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName) return createMSTeamsPayload( @@ -117,7 +108,7 @@ func (m *MSTeamsPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { } // Push implements PayloadConvertor Push method -func (m *MSTeamsPayload) Push(p *api.PushPayload) (api.Payloader, error) { +func (m msteamsConvertor) Push(p *api.PushPayload) (MSTeamsPayload, error) { var ( branchName = git.RefName(p.Ref).ShortName() commitDesc string @@ -160,7 +151,7 @@ func (m *MSTeamsPayload) Push(p *api.PushPayload) (api.Payloader, error) { } // Issue implements PayloadConvertor Issue method -func (m *MSTeamsPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { +func (m msteamsConvertor) Issue(p *api.IssuePayload) (MSTeamsPayload, error) { title, _, attachmentText, color := getIssuesPayloadInfo(p, noneLinkFormatter, false) return createMSTeamsPayload( @@ -175,7 +166,7 @@ func (m *MSTeamsPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { } // IssueComment implements PayloadConvertor IssueComment method -func (m *MSTeamsPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { +func (m msteamsConvertor) IssueComment(p *api.IssueCommentPayload) (MSTeamsPayload, error) { title, _, color := getIssueCommentPayloadInfo(p, noneLinkFormatter, false) return createMSTeamsPayload( @@ -190,7 +181,7 @@ func (m *MSTeamsPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader } // PullRequest implements PayloadConvertor PullRequest method -func (m *MSTeamsPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { +func (m msteamsConvertor) PullRequest(p *api.PullRequestPayload) (MSTeamsPayload, error) { title, _, attachmentText, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false) return createMSTeamsPayload( @@ -205,14 +196,14 @@ func (m *MSTeamsPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, } // Review implements PayloadConvertor Review method -func (m *MSTeamsPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) { +func (m msteamsConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (MSTeamsPayload, error) { var text, title string var color int switch p.Action { case api.HookIssueReviewed: action, err := parseHookPullRequestEventType(event) if err != nil { - return nil, err + return MSTeamsPayload{}, err } title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) @@ -242,7 +233,7 @@ func (m *MSTeamsPayload) Review(p *api.PullRequestPayload, event webhook_module. } // Repository implements PayloadConvertor Repository method -func (m *MSTeamsPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { +func (m msteamsConvertor) Repository(p *api.RepositoryPayload) (MSTeamsPayload, error) { var title, url string var color int switch p.Action { @@ -267,7 +258,7 @@ func (m *MSTeamsPayload) Repository(p *api.RepositoryPayload) (api.Payloader, er } // Wiki implements PayloadConvertor Wiki method -func (m *MSTeamsPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { +func (m msteamsConvertor) Wiki(p *api.WikiPayload) (MSTeamsPayload, error) { title, color, _ := getWikiPayloadInfo(p, noneLinkFormatter, false) return createMSTeamsPayload( @@ -282,7 +273,7 @@ func (m *MSTeamsPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { } // Release implements PayloadConvertor Release method -func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { +func (m msteamsConvertor) Release(p *api.ReleasePayload) (MSTeamsPayload, error) { title, color := getReleasePayloadInfo(p, noneLinkFormatter, false) return createMSTeamsPayload( @@ -296,7 +287,7 @@ func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { ), nil } -func (m *MSTeamsPayload) Package(p *api.PackagePayload) (api.Payloader, error) { +func (m msteamsConvertor) Package(p *api.PackagePayload) (MSTeamsPayload, error) { title, color := getPackagePayloadInfo(p, noneLinkFormatter, false) return createMSTeamsPayload( @@ -310,23 +301,19 @@ func (m *MSTeamsPayload) Package(p *api.PackagePayload) (api.Payloader, error) { ), nil } -// GetMSTeamsPayload converts a MSTeams webhook into a MSTeamsPayload -func GetMSTeamsPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) { - return convertPayloader(new(MSTeamsPayload), p, event) -} - -func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) *MSTeamsPayload { - facts := []MSTeamsFact{ - { +func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) MSTeamsPayload { + facts := make([]MSTeamsFact, 0, 2) + if r != nil { + facts = append(facts, MSTeamsFact{ Name: "Repository:", Value: r.FullName, - }, + }) } if fact != nil { facts = append(facts, *fact) } - return &MSTeamsPayload{ + return MSTeamsPayload{ Type: "MessageCard", Context: "https://schema.org/extensions", ThemeColor: fmt.Sprintf("%x", color), @@ -355,3 +342,11 @@ func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTar }, } } + +type msteamsConvertor struct{} + +var _ payloadConvertor[MSTeamsPayload] = msteamsConvertor{} + +func newMSTeamsRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + return newJSONRequest(msteamsConvertor{}, w, t, true) +} diff --git a/services/webhook/msteams_test.go b/services/webhook/msteams_test.go index 990a535df5c..01e08b918e5 100644 --- a/services/webhook/msteams_test.go +++ b/services/webhook/msteams_test.go @@ -4,8 +4,11 @@ package webhook import ( + "context" "testing" + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -14,22 +17,20 @@ import ( ) func TestMSTeamsPayload(t *testing.T) { + mc := msteamsConvertor{} t.Run("Create", func(t *testing.T) { p := createTestPayload() - d := new(MSTeamsPayload) - pl, err := d.Create(p) + pl, err := mc.Create(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - - assert.Equal(t, "[test/repo] branch test created", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] branch test created", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + + assert.Equal(t, "[test/repo] branch test created", pl.Title) + assert.Equal(t, "[test/repo] branch test created", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repo.FullName, fact.Value) } else if fact.Name == "branch:" { @@ -38,27 +39,24 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Delete", func(t *testing.T) { p := deleteTestPayload() - d := new(MSTeamsPayload) - pl, err := d.Delete(p) + pl, err := mc.Delete(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - - assert.Equal(t, "[test/repo] branch test deleted", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] branch test deleted", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + + assert.Equal(t, "[test/repo] branch test deleted", pl.Title) + assert.Equal(t, "[test/repo] branch test deleted", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repo.FullName, fact.Value) } else if fact.Name == "branch:" { @@ -67,27 +65,24 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Fork", func(t *testing.T) { p := forkTestPayload() - d := new(MSTeamsPayload) - pl, err := d.Fork(p) + pl, err := mc.Fork(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - - assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + + assert.Equal(t, "test/repo2 is forked to test/repo", pl.Title) + assert.Equal(t, "test/repo2 is forked to test/repo", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repo.FullName, fact.Value) } else if fact.Name == "Forkee:" { @@ -96,27 +91,24 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Push", func(t *testing.T) { p := pushTestPayload() - d := new(MSTeamsPayload) - pl, err := d.Push(p) + pl, err := mc.Push(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - - assert.Equal(t, "[test/repo:test] 2 new commits", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo:test] 2 new commits", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + + assert.Equal(t, "[test/repo:test] 2 new commits", pl.Title) + assert.Equal(t, "[test/repo:test] 2 new commits", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repo.FullName, fact.Value) } else if fact.Name == "Commit count:" { @@ -125,28 +117,25 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Issue", func(t *testing.T) { p := issueTestPayload() - d := new(MSTeamsPayload) p.Action = api.HookIssueOpened - pl, err := d.Issue(p) + pl, err := mc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - - assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "issue body", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + + assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.Title) + assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "issue body", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else if fact.Name == "Issue #:" { @@ -155,23 +144,21 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.PotentialAction[0].Targets[0].URI) p.Action = api.HookIssueClosed - pl, err = d.Issue(p) + pl, err = mc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - - assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + + assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.Title) + assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else if fact.Name == "Issue #:" { @@ -180,27 +167,24 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.PotentialAction[0].Targets[0].URI) }) t.Run("IssueComment", func(t *testing.T) { p := issueCommentTestPayload() - d := new(MSTeamsPayload) - pl, err := d.IssueComment(p) + pl, err := mc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - - assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "more info needed", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + + assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.Title) + assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "more info needed", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else if fact.Name == "Issue #:" { @@ -209,27 +193,24 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.PotentialAction[0].Targets[0].URI) }) t.Run("PullRequest", func(t *testing.T) { p := pullRequestTestPayload() - d := new(MSTeamsPayload) - pl, err := d.PullRequest(p) + pl, err := mc.PullRequest(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - - assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "fixes bug #2", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + + assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.Title) + assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "fixes bug #2", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else if fact.Name == "Pull request #:" { @@ -238,27 +219,24 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.PotentialAction[0].Targets[0].URI) }) t.Run("PullRequestComment", func(t *testing.T) { p := pullRequestCommentTestPayload() - d := new(MSTeamsPayload) - pl, err := d.IssueComment(p) + pl, err := mc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - - assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "changes requested", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + + assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.Title) + assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "changes requested", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else if fact.Name == "Issue #:" { @@ -267,28 +245,25 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Review", func(t *testing.T) { p := pullRequestTestPayload() p.Action = api.HookIssueReviewed - d := new(MSTeamsPayload) - pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved) + pl, err := mc.Review(p, webhook_module.HookEventPullRequestReviewApproved) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - - assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "good job", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + + assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.Title) + assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "good job", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else if fact.Name == "Pull request #:" { @@ -297,128 +272,139 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Repository", func(t *testing.T) { p := repositoryTestPayload() - d := new(MSTeamsPayload) - pl, err := d.Repository(p) + pl, err := mc.Repository(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - - assert.Equal(t, "[test/repo] Repository created", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Repository created", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 1) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + + assert.Equal(t, "[test/repo] Repository created", pl.Title) + assert.Equal(t, "[test/repo] Repository created", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 1) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo", pl.PotentialAction[0].Targets[0].URI) + }) + + t.Run("Package", func(t *testing.T) { + p := packageTestPayload() + + pl, err := mc.Package(p) + require.NoError(t, err) + + assert.Equal(t, "Package created: GiteaContainer:latest", pl.Title) + assert.Equal(t, "Package created: GiteaContainer:latest", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 1) + for _, fact := range pl.Sections[0].Facts { + if fact.Name == "Package:" { + assert.Equal(t, p.Package.Name, fact.Value) + } else { + t.Fail() + } + } + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Wiki", func(t *testing.T) { p := wikiTestPayload() - d := new(MSTeamsPayload) p.Action = api.HookWikiCreated - pl, err := d.Wiki(p) + pl, err := mc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - - assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + + assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.Title) + assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.PotentialAction[0].Targets[0].URI) p.Action = api.HookWikiEdited - pl, err = d.Wiki(p) + pl, err = mc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - - assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + + assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.Title) + assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.PotentialAction[0].Targets[0].URI) p.Action = api.HookWikiDeleted - pl, err = d.Wiki(p) + pl, err = mc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - - assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + + assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.Title) + assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Release", func(t *testing.T) { p := pullReleaseTestPayload() - d := new(MSTeamsPayload) - pl, err := d.Release(p) + pl, err := mc.Release(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - - assert.Equal(t, "[test/repo] Release created: v1.0", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Release created: v1.0", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + + assert.Equal(t, "[test/repo] Release created: v1.0", pl.Title) + assert.Equal(t, "[test/repo] Release created: v1.0", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else if fact.Name == "Tag:" { @@ -427,21 +413,43 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.PotentialAction[0].Targets[0].URI) }) } func TestMSTeamsJSONPayload(t *testing.T) { p := pushTestPayload() - - pl, err := new(MSTeamsPayload).Push(p) + data, err := p.JSONPayload() require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - json, err := pl.JSONPayload() + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.MSTEAMS, + URL: "https://msteams.example.com/", + Meta: ``, + HTTPMethod: "POST", + } + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := newMSTeamsRequest(context.Background(), hook, task) + require.NotNil(t, req) + require.NotNil(t, reqBody) require.NoError(t, err) - assert.NotEmpty(t, json) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://msteams.example.com/", req.URL.String()) + assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256")) + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + var body MSTeamsPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "[test/repo:test] 2 new commits", body.Summary) } diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go index 714a4c076ef..7880d8b6066 100644 --- a/services/webhook/packagist.go +++ b/services/webhook/packagist.go @@ -4,7 +4,9 @@ package webhook import ( - "errors" + "context" + "fmt" + "net/http" webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/json" @@ -38,84 +40,85 @@ func GetPackagistHook(w *webhook_model.Webhook) *PackagistMeta { return s } -// JSONPayload Marshals the PackagistPayload to json -func (f *PackagistPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(f, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - -var _ PayloadConvertor = &PackagistPayload{} - // Create implements PayloadConvertor Create method -func (f *PackagistPayload) Create(_ *api.CreatePayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Create(_ *api.CreatePayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // Delete implements PayloadConvertor Delete method -func (f *PackagistPayload) Delete(_ *api.DeletePayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Delete(_ *api.DeletePayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // Fork implements PayloadConvertor Fork method -func (f *PackagistPayload) Fork(_ *api.ForkPayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Fork(_ *api.ForkPayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // Push implements PayloadConvertor Push method -func (f *PackagistPayload) Push(_ *api.PushPayload) (api.Payloader, error) { - return f, nil +// https://packagist.org/about +func (pc packagistConvertor) Push(_ *api.PushPayload) (PackagistPayload, error) { + return PackagistPayload{ + PackagistRepository: struct { + URL string `json:"url"` + }{ + URL: pc.PackageURL, + }, + }, nil } // Issue implements PayloadConvertor Issue method -func (f *PackagistPayload) Issue(_ *api.IssuePayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Issue(_ *api.IssuePayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // IssueComment implements PayloadConvertor IssueComment method -func (f *PackagistPayload) IssueComment(_ *api.IssueCommentPayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) IssueComment(_ *api.IssueCommentPayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // PullRequest implements PayloadConvertor PullRequest method -func (f *PackagistPayload) PullRequest(_ *api.PullRequestPayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) PullRequest(_ *api.PullRequestPayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // Review implements PayloadConvertor Review method -func (f *PackagistPayload) Review(_ *api.PullRequestPayload, _ webhook_module.HookEventType) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Review(_ *api.PullRequestPayload, _ webhook_module.HookEventType) (PackagistPayload, error) { + return PackagistPayload{}, nil } // Repository implements PayloadConvertor Repository method -func (f *PackagistPayload) Repository(_ *api.RepositoryPayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Repository(_ *api.RepositoryPayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // Wiki implements PayloadConvertor Wiki method -func (f *PackagistPayload) Wiki(_ *api.WikiPayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Wiki(_ *api.WikiPayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // Release implements PayloadConvertor Release method -func (f *PackagistPayload) Release(_ *api.ReleasePayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Release(_ *api.ReleasePayload) (PackagistPayload, error) { + return PackagistPayload{}, nil +} + +func (pc packagistConvertor) Package(_ *api.PackagePayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } -func (f *PackagistPayload) Package(_ *api.PackagePayload) (api.Payloader, error) { - return nil, nil +type packagistConvertor struct { + PackageURL string } -// GetPackagistPayload converts a packagist webhook into a PackagistPayload -func GetPackagistPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) { - s := new(PackagistPayload) +var _ payloadConvertor[PackagistPayload] = packagistConvertor{} - packagist := &PackagistMeta{} - if err := json.Unmarshal([]byte(meta), &packagist); err != nil { - return s, errors.New("GetPackagistPayload meta json:" + err.Error()) +func newPackagistRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + meta := &PackagistMeta{} + if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { + return nil, nil, fmt.Errorf("newpackagistRequest meta json: %w", err) + } + pc := packagistConvertor{ + PackageURL: meta.PackageURL, } - s.PackagistRepository.URL = packagist.PackageURL - return convertPayloader(s, p, event) + return newJSONRequest(pc, w, t, true) } diff --git a/services/webhook/packagist_test.go b/services/webhook/packagist_test.go index 932b56fe9b9..e9b0695baae 100644 --- a/services/webhook/packagist_test.go +++ b/services/webhook/packagist_test.go @@ -4,8 +4,11 @@ package webhook import ( + "context" "testing" + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -14,146 +17,199 @@ import ( ) func TestPackagistPayload(t *testing.T) { + pc := packagistConvertor{ + PackageURL: "https://packagist.org/packages/example", + } t.Run("Create", func(t *testing.T) { p := createTestPayload() - d := new(PackagistPayload) - pl, err := d.Create(p) + pl, err := pc.Create(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("Delete", func(t *testing.T) { p := deleteTestPayload() - d := new(PackagistPayload) - pl, err := d.Delete(p) + pl, err := pc.Delete(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("Fork", func(t *testing.T) { p := forkTestPayload() - d := new(PackagistPayload) - pl, err := d.Fork(p) + pl, err := pc.Fork(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("Push", func(t *testing.T) { p := pushTestPayload() - d := new(PackagistPayload) - d.PackagistRepository.URL = "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN" - pl, err := d.Push(p) + pl, err := pc.Push(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &PackagistPayload{}, pl) - assert.Equal(t, "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", pl.(*PackagistPayload).PackagistRepository.URL) + assert.Equal(t, "https://packagist.org/packages/example", pl.PackagistRepository.URL) }) t.Run("Issue", func(t *testing.T) { p := issueTestPayload() - d := new(PackagistPayload) p.Action = api.HookIssueOpened - pl, err := d.Issue(p) + pl, err := pc.Issue(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) p.Action = api.HookIssueClosed - pl, err = d.Issue(p) + pl, err = pc.Issue(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("IssueComment", func(t *testing.T) { p := issueCommentTestPayload() - d := new(PackagistPayload) - pl, err := d.IssueComment(p) + pl, err := pc.IssueComment(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("PullRequest", func(t *testing.T) { p := pullRequestTestPayload() - d := new(PackagistPayload) - pl, err := d.PullRequest(p) + pl, err := pc.PullRequest(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("PullRequestComment", func(t *testing.T) { p := pullRequestCommentTestPayload() - d := new(PackagistPayload) - pl, err := d.IssueComment(p) + pl, err := pc.IssueComment(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("Review", func(t *testing.T) { p := pullRequestTestPayload() p.Action = api.HookIssueReviewed - d := new(PackagistPayload) - pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved) + pl, err := pc.Review(p, webhook_module.HookEventPullRequestReviewApproved) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("Repository", func(t *testing.T) { p := repositoryTestPayload() - d := new(PackagistPayload) - pl, err := d.Repository(p) + pl, err := pc.Repository(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) + }) + + t.Run("Package", func(t *testing.T) { + p := packageTestPayload() + + pl, err := pc.Package(p) + require.NoError(t, err) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("Wiki", func(t *testing.T) { p := wikiTestPayload() - d := new(PackagistPayload) p.Action = api.HookWikiCreated - pl, err := d.Wiki(p) + pl, err := pc.Wiki(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) p.Action = api.HookWikiEdited - pl, err = d.Wiki(p) + pl, err = pc.Wiki(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) p.Action = api.HookWikiDeleted - pl, err = d.Wiki(p) + pl, err = pc.Wiki(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("Release", func(t *testing.T) { p := pullReleaseTestPayload() - d := new(PackagistPayload) - pl, err := d.Release(p) + pl, err := pc.Release(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) } func TestPackagistJSONPayload(t *testing.T) { p := pushTestPayload() + data, err := p.JSONPayload() + require.NoError(t, err) - pl, err := new(PackagistPayload).Push(p) + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.PACKAGIST, + URL: "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", + Meta: `{"package_url":"https://packagist.org/packages/example"}`, + HTTPMethod: "POST", + } + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := newPackagistRequest(context.Background(), hook, task) + require.NotNil(t, req) + require.NotNil(t, reqBody) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &PackagistPayload{}, pl) - json, err := pl.JSONPayload() + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", req.URL.String()) + assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256")) + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + var body PackagistPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "https://packagist.org/packages/example", body.PackagistRepository.URL) +} + +func TestPackagistEmptyPayload(t *testing.T) { + p := createTestPayload() + data, err := p.JSONPayload() require.NoError(t, err) - assert.NotEmpty(t, json) + + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.PACKAGIST, + URL: "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", + Meta: `{"package_url":"https://packagist.org/packages/example"}`, + HTTPMethod: "POST", + } + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventCreate, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := newPackagistRequest(context.Background(), hook, task) + require.NotNil(t, req) + require.NotNil(t, reqBody) + require.NoError(t, err) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", req.URL.String()) + assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256")) + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + var body PackagistPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "", body.PackagistRepository.URL) } diff --git a/services/webhook/payloader.go b/services/webhook/payloader.go index bd482c04ead..54a11a58682 100644 --- a/services/webhook/payloader.go +++ b/services/webhook/payloader.go @@ -4,58 +4,109 @@ package webhook import ( + "bytes" + "fmt" + "net/http" + + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" ) -// PayloadConvertor defines the interface to convert system webhook payload to external payload -type PayloadConvertor interface { - api.Payloader - Create(*api.CreatePayload) (api.Payloader, error) - Delete(*api.DeletePayload) (api.Payloader, error) - Fork(*api.ForkPayload) (api.Payloader, error) - Issue(*api.IssuePayload) (api.Payloader, error) - IssueComment(*api.IssueCommentPayload) (api.Payloader, error) - Push(*api.PushPayload) (api.Payloader, error) - PullRequest(*api.PullRequestPayload) (api.Payloader, error) - Review(*api.PullRequestPayload, webhook_module.HookEventType) (api.Payloader, error) - Repository(*api.RepositoryPayload) (api.Payloader, error) - Release(*api.ReleasePayload) (api.Payloader, error) - Wiki(*api.WikiPayload) (api.Payloader, error) - Package(*api.PackagePayload) (api.Payloader, error) +// payloadConvertor defines the interface to convert system payload to webhook payload +type payloadConvertor[T any] interface { + Create(*api.CreatePayload) (T, error) + Delete(*api.DeletePayload) (T, error) + Fork(*api.ForkPayload) (T, error) + Issue(*api.IssuePayload) (T, error) + IssueComment(*api.IssueCommentPayload) (T, error) + Push(*api.PushPayload) (T, error) + PullRequest(*api.PullRequestPayload) (T, error) + Review(*api.PullRequestPayload, webhook_module.HookEventType) (T, error) + Repository(*api.RepositoryPayload) (T, error) + Release(*api.ReleasePayload) (T, error) + Wiki(*api.WikiPayload) (T, error) + Package(*api.PackagePayload) (T, error) } -func convertPayloader(s PayloadConvertor, p api.Payloader, event webhook_module.HookEventType) (api.Payloader, error) { +func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte) (T, error) { + var p P + if err := json.Unmarshal(data, &p); err != nil { + var t T + return t, fmt.Errorf("could not unmarshal payload: %w", err) + } + return convert(p) +} + +func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module.HookEventType) (T, error) { switch event { case webhook_module.HookEventCreate: - return s.Create(p.(*api.CreatePayload)) + return convertUnmarshalledJSON(rc.Create, data) case webhook_module.HookEventDelete: - return s.Delete(p.(*api.DeletePayload)) + return convertUnmarshalledJSON(rc.Delete, data) case webhook_module.HookEventFork: - return s.Fork(p.(*api.ForkPayload)) + return convertUnmarshalledJSON(rc.Fork, data) case webhook_module.HookEventIssues, webhook_module.HookEventIssueAssign, webhook_module.HookEventIssueLabel, webhook_module.HookEventIssueMilestone: - return s.Issue(p.(*api.IssuePayload)) + return convertUnmarshalledJSON(rc.Issue, data) case webhook_module.HookEventIssueComment, webhook_module.HookEventPullRequestComment: - pl, ok := p.(*api.IssueCommentPayload) - if ok { - return s.IssueComment(pl) - } - return s.PullRequest(p.(*api.PullRequestPayload)) + // previous code sometimes sent s.PullRequest(p.(*api.PullRequestPayload)) + // however I couldn't find in notifier.go such a payload with an HookEvent***Comment event + + // History (most recent first): + // - refactored in https://github.com/go-gitea/gitea/pull/12310 + // - assertion added in https://github.com/go-gitea/gitea/pull/12046 + // - issue raised in https://github.com/go-gitea/gitea/issues/11940#issuecomment-645713996 + // > That's because for HookEventPullRequestComment event, some places use IssueCommentPayload and others use PullRequestPayload + + // In modules/actions/workflows.go:183 the type assertion is always payload.(*api.IssueCommentPayload) + return convertUnmarshalledJSON(rc.IssueComment, data) case webhook_module.HookEventPush: - return s.Push(p.(*api.PushPayload)) + return convertUnmarshalledJSON(rc.Push, data) case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestAssign, webhook_module.HookEventPullRequestLabel, webhook_module.HookEventPullRequestMilestone, webhook_module.HookEventPullRequestSync, webhook_module.HookEventPullRequestReviewRequest: - return s.PullRequest(p.(*api.PullRequestPayload)) + return convertUnmarshalledJSON(rc.PullRequest, data) case webhook_module.HookEventPullRequestReviewApproved, webhook_module.HookEventPullRequestReviewRejected, webhook_module.HookEventPullRequestReviewComment: - return s.Review(p.(*api.PullRequestPayload), event) + return convertUnmarshalledJSON(func(p *api.PullRequestPayload) (T, error) { + return rc.Review(p, event) + }, data) case webhook_module.HookEventRepository: - return s.Repository(p.(*api.RepositoryPayload)) + return convertUnmarshalledJSON(rc.Repository, data) case webhook_module.HookEventRelease: - return s.Release(p.(*api.ReleasePayload)) + return convertUnmarshalledJSON(rc.Release, data) case webhook_module.HookEventWiki: - return s.Wiki(p.(*api.WikiPayload)) + return convertUnmarshalledJSON(rc.Wiki, data) case webhook_module.HookEventPackage: - return s.Package(p.(*api.PackagePayload)) + return convertUnmarshalledJSON(rc.Package, data) + } + var t T + return t, fmt.Errorf("newPayload unsupported event: %s", event) +} + +func newJSONRequest[T any](pc payloadConvertor[T], w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) { + payload, err := newPayload(pc, []byte(t.PayloadContent), t.EventType) + if err != nil { + return nil, nil, err + } + + body, err := json.MarshalIndent(payload, "", " ") + if err != nil { + return nil, nil, err + } + + method := w.HTTPMethod + if method == "" { + method = http.MethodPost + } + + req, err := http.NewRequest(method, w.URL, bytes.NewReader(body)) + if err != nil { + return nil, nil, err + } + req.Header.Set("Content-Type", "application/json") + + if withDefaultHeaders { + return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body) } - return s, nil + return req, body, nil } diff --git a/services/webhook/slack.go b/services/webhook/slack.go index ac27b5bc71b..ba8bac27d97 100644 --- a/services/webhook/slack.go +++ b/services/webhook/slack.go @@ -4,8 +4,9 @@ package webhook import ( - "errors" + "context" "fmt" + "net/http" "regexp" "strings" @@ -39,7 +40,6 @@ func GetSlackHook(w *webhook_model.Webhook) *SlackMeta { type SlackPayload struct { Channel string `json:"channel"` Text string `json:"text"` - Color string `json:"-"` Username string `json:"username"` IconURL string `json:"icon_url"` UnfurlLinks int `json:"unfurl_links"` @@ -56,15 +56,6 @@ type SlackAttachment struct { Text string `json:"text"` } -// JSONPayload Marshals the SlackPayload to json -func (s *SlackPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(s, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - // SlackTextFormatter replaces &, <, > with HTML characters // see: https://api.slack.com/docs/formatting func SlackTextFormatter(s string) string { @@ -92,15 +83,14 @@ func SlackLinkFormatter(url, text string) string { // SlackLinkToRef slack-formatter link to a repo ref func SlackLinkToRef(repoURL, ref string) string { + // FIXME: SHA1 hardcoded here url := git.RefURL(repoURL, ref) refName := git.RefName(ref).ShortName() return SlackLinkFormatter(url, refName) } -var _ PayloadConvertor = &SlackPayload{} - -// Create implements PayloadConvertor Create method -func (s *SlackPayload) Create(p *api.CreatePayload) (api.Payloader, error) { +// Create implements payloadConvertor Create method +func (s slackConvertor) Create(p *api.CreatePayload) (SlackPayload, error) { repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) refLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref) text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName) @@ -109,7 +99,7 @@ func (s *SlackPayload) Create(p *api.CreatePayload) (api.Payloader, error) { } // Delete composes Slack payload for delete a branch or tag. -func (s *SlackPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { +func (s slackConvertor) Delete(p *api.DeletePayload) (SlackPayload, error) { refName := git.RefName(p.Ref).ShortName() repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName) @@ -118,7 +108,7 @@ func (s *SlackPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { } // Fork composes Slack payload for forked by a repository. -func (s *SlackPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { +func (s slackConvertor) Fork(p *api.ForkPayload) (SlackPayload, error) { baseLink := SlackLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName) forkLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink) @@ -126,8 +116,8 @@ func (s *SlackPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { return s.createPayload(text, nil), nil } -// Issue implements PayloadConvertor Issue method -func (s *SlackPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { +// Issue implements payloadConvertor Issue method +func (s slackConvertor) Issue(p *api.IssuePayload) (SlackPayload, error) { text, issueTitle, attachmentText, color := getIssuesPayloadInfo(p, SlackLinkFormatter, true) var attachments []SlackAttachment @@ -145,8 +135,8 @@ func (s *SlackPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { return s.createPayload(text, attachments), nil } -// IssueComment implements PayloadConvertor IssueComment method -func (s *SlackPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { +// IssueComment implements payloadConvertor IssueComment method +func (s slackConvertor) IssueComment(p *api.IssueCommentPayload) (SlackPayload, error) { text, issueTitle, color := getIssueCommentPayloadInfo(p, SlackLinkFormatter, true) return s.createPayload(text, []SlackAttachment{{ @@ -157,28 +147,28 @@ func (s *SlackPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, }}), nil } -// Wiki implements PayloadConvertor Wiki method -func (s *SlackPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { +// Wiki implements payloadConvertor Wiki method +func (s slackConvertor) Wiki(p *api.WikiPayload) (SlackPayload, error) { text, _, _ := getWikiPayloadInfo(p, SlackLinkFormatter, true) return s.createPayload(text, nil), nil } -// Release implements PayloadConvertor Release method -func (s *SlackPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { +// Release implements payloadConvertor Release method +func (s slackConvertor) Release(p *api.ReleasePayload) (SlackPayload, error) { text, _ := getReleasePayloadInfo(p, SlackLinkFormatter, true) return s.createPayload(text, nil), nil } -func (s *SlackPayload) Package(p *api.PackagePayload) (api.Payloader, error) { +func (s slackConvertor) Package(p *api.PackagePayload) (SlackPayload, error) { text, _ := getPackagePayloadInfo(p, SlackLinkFormatter, true) return s.createPayload(text, nil), nil } -// Push implements PayloadConvertor Push method -func (s *SlackPayload) Push(p *api.PushPayload) (api.Payloader, error) { +// Push implements payloadConvertor Push method +func (s slackConvertor) Push(p *api.PushPayload) (SlackPayload, error) { // n new commits var ( commitDesc string @@ -218,8 +208,8 @@ func (s *SlackPayload) Push(p *api.PushPayload) (api.Payloader, error) { }}), nil } -// PullRequest implements PayloadConvertor PullRequest method -func (s *SlackPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { +// PullRequest implements payloadConvertor PullRequest method +func (s slackConvertor) PullRequest(p *api.PullRequestPayload) (SlackPayload, error) { text, issueTitle, attachmentText, color := getPullRequestPayloadInfo(p, SlackLinkFormatter, true) var attachments []SlackAttachment @@ -237,8 +227,8 @@ func (s *SlackPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, er return s.createPayload(text, attachments), nil } -// Review implements PayloadConvertor Review method -func (s *SlackPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) { +// Review implements payloadConvertor Review method +func (s slackConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (SlackPayload, error) { senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title) titleLink := fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index) @@ -249,7 +239,7 @@ func (s *SlackPayload) Review(p *api.PullRequestPayload, event webhook_module.Ho case api.HookIssueReviewed: action, err := parseHookPullRequestEventType(event) if err != nil { - return nil, err + return SlackPayload{}, err } text = fmt.Sprintf("[%s] Pull request review %s: [%s](%s) by %s", repoLink, action, title, titleLink, senderLink) @@ -258,8 +248,8 @@ func (s *SlackPayload) Review(p *api.PullRequestPayload, event webhook_module.Ho return s.createPayload(text, nil), nil } -// Repository implements PayloadConvertor Repository method -func (s *SlackPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { +// Repository implements payloadConvertor Repository method +func (s slackConvertor) Repository(p *api.RepositoryPayload) (SlackPayload, error) { senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) repoLink := SlackLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName) var text string @@ -274,8 +264,8 @@ func (s *SlackPayload) Repository(p *api.RepositoryPayload) (api.Payloader, erro return s.createPayload(text, nil), nil } -func (s *SlackPayload) createPayload(text string, attachments []SlackAttachment) *SlackPayload { - return &SlackPayload{ +func (s slackConvertor) createPayload(text string, attachments []SlackAttachment) SlackPayload { + return SlackPayload{ Channel: s.Channel, Text: text, Username: s.Username, @@ -284,21 +274,27 @@ func (s *SlackPayload) createPayload(text string, attachments []SlackAttachment) } } -// GetSlackPayload converts a slack webhook into a SlackPayload -func GetSlackPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) { - s := new(SlackPayload) - - slack := &SlackMeta{} - if err := json.Unmarshal([]byte(meta), &slack); err != nil { - return s, errors.New("GetSlackPayload meta json:" + err.Error()) - } +type slackConvertor struct { + Channel string + Username string + IconURL string + Color string +} - s.Channel = slack.Channel - s.Username = slack.Username - s.IconURL = slack.IconURL - s.Color = slack.Color +var _ payloadConvertor[SlackPayload] = slackConvertor{} - return convertPayloader(s, p, event) +func newSlackRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + meta := &SlackMeta{} + if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { + return nil, nil, fmt.Errorf("newSlackRequest meta json: %w", err) + } + sc := slackConvertor{ + Channel: meta.Channel, + Username: meta.Username, + IconURL: meta.IconURL, + Color: meta.Color, + } + return newJSONRequest(sc, w, t, true) } var slackChannel = regexp.MustCompile(`^#?[a-z0-9_-]{1,80}$`) diff --git a/services/webhook/slack_test.go b/services/webhook/slack_test.go index d9828f374f0..7ebf16aba29 100644 --- a/services/webhook/slack_test.go +++ b/services/webhook/slack_test.go @@ -4,8 +4,11 @@ package webhook import ( + "context" "testing" + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -14,189 +17,180 @@ import ( ) func TestSlackPayload(t *testing.T) { + sc := slackConvertor{} + t.Run("Create", func(t *testing.T) { p := createTestPayload() - d := new(SlackPayload) - pl, err := d.Create(p) + pl, err := sc.Create(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[