mirror of
https://github.com/go-gitea/gitea.git
synced 2026-11-11 10:22:04 -06:00
Compare commits
92 Commits
lunny-snap
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a61ef7aa14 | ||
|
|
03fc2bdcc8 | ||
|
|
6eb5efb6bb | ||
|
|
c62bcf19f6 | ||
|
|
d399f844e6 | ||
|
|
db23b5a192 | ||
|
|
d5ce1cad7f | ||
|
|
a61ad4876a | ||
|
|
f5a54d6169 | ||
|
|
2ef5b7623b | ||
|
|
1fcb6b4cd6 | ||
|
|
d1d702bea9 | ||
|
|
518e08c2c7 | ||
|
|
25c5b6ab37 | ||
|
|
640cd615c4 | ||
|
|
c01f93b5a7 | ||
|
|
3c41728017 | ||
|
|
06b393e402 | ||
|
|
3561478109 | ||
|
|
2d6dca9abb | ||
|
|
bccc90d654 | ||
|
|
772f365254 | ||
|
|
e0b2f3425e | ||
|
|
0ab0bfc375 | ||
|
|
f3354aa902 | ||
|
|
7b1cde99d0 | ||
|
|
2bbac68f45 | ||
|
|
d715a5ae75 | ||
|
|
1554b6c939 | ||
|
|
3699309edd | ||
|
|
8cb6fb2001 | ||
|
|
cd92c6a008 | ||
|
|
edebfaa125 | ||
|
|
bd8fd71bc1 | ||
|
|
c54ec52f0f | ||
|
|
51879941dc | ||
|
|
87a703819b | ||
|
|
e09813f472 | ||
|
|
884884308d | ||
|
|
462ab3801a | ||
|
|
3aca9c8fb7 | ||
|
|
4f69599981 | ||
|
|
dd4672607b | ||
|
|
a165fdb63c | ||
|
|
6bc92f464a | ||
|
|
baccc2d7cd | ||
|
|
5d9bfa1e14 | ||
|
|
7efb5c055f | ||
|
|
b23c70d439 | ||
|
|
b6c291f87a | ||
|
|
c74f2f9216 | ||
|
|
b8b270bac9 | ||
|
|
df4544a58b | ||
|
|
ede9ec3e10 | ||
|
|
24700dca2a | ||
|
|
7e18e81c7d | ||
|
|
a5201dc427 | ||
|
|
98c937170f | ||
|
|
09922e4ae1 | ||
|
|
8d14c9af20 | ||
|
|
87ebf3b1ba | ||
|
|
fd98ab5472 | ||
|
|
0d59b737aa | ||
|
|
917badaae6 | ||
|
|
288d54951e | ||
|
|
b81b50be55 | ||
|
|
c62db5b2a3 | ||
|
|
4a9d49bb46 | ||
|
|
b7c1fb2a39 | ||
|
|
3b9b8a8976 | ||
|
|
565f1b7d52 | ||
|
|
e87ac3a0f4 | ||
|
|
4477d47d07 | ||
|
|
bfbf8cd366 | ||
|
|
dfe6db6149 | ||
|
|
d1c980cf8b | ||
|
|
1152bc0cf2 | ||
|
|
159e6d7e75 | ||
|
|
f29eedad83 | ||
|
|
e9c5804075 | ||
|
|
be297b7e05 | ||
|
|
fca64e7330 | ||
|
|
58279e39fd | ||
|
|
ab295e62e4 | ||
|
|
d919301d36 | ||
|
|
666e6e2d17 | ||
|
|
4d00ed2a1e | ||
|
|
ce0da8a378 | ||
|
|
f26a314636 | ||
|
|
5c68ba3e0a | ||
|
|
78eb95ab19 | ||
|
|
5c723ee0fd |
31
.github/actions/docker-dryrun/action.yml
vendored
31
.github/actions/docker-dryrun/action.yml
vendored
@@ -1,31 +0,0 @@
|
||||
name: docker-dryrun
|
||||
description: Composite action that performs the container build steps for a single platform.
|
||||
|
||||
inputs:
|
||||
platform:
|
||||
description: "The target platform: linux/amd64, linux/arm64, linux/riscv64."
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||
with:
|
||||
cache-image: false
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
- name: Build regular image
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ inputs.platform }}
|
||||
push: false
|
||||
file: Dockerfile
|
||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
|
||||
- name: Build rootless image
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ inputs.platform }}
|
||||
push: false
|
||||
file: Dockerfile.rootless
|
||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless
|
||||
2
.github/actions/node-setup/action.yml
vendored
2
.github/actions/node-setup/action.yml
vendored
@@ -9,7 +9,7 @@ inputs:
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
||||
- uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7
|
||||
with:
|
||||
node-version: 26
|
||||
|
||||
2
.github/actions/python-setup/action.yml
vendored
2
.github/actions/python-setup/action.yml
vendored
@@ -4,7 +4,7 @@ description: Set up uv and python and restore the uv cache
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
||||
- uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
||||
with:
|
||||
python-version: 3.15 # renovate: datasource=python-version depName=python versioning=python
|
||||
save-cache: ${{ github.workflow == 'cache-seeder' }}
|
||||
|
||||
4
.github/workflows/cron-renovate.yml
vendored
4
.github/workflows/cron-renovate.yml
vendored
@@ -9,7 +9,7 @@ concurrency:
|
||||
group: cron-renovate
|
||||
|
||||
env:
|
||||
RENOVATE_VERSION: 44.190.1 # renovate: datasource=npm depName=renovate
|
||||
RENOVATE_VERSION: 44.210.0 # renovate: datasource=npm depName=renovate
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: renovatebot/github-action@2804199d260ad00c1fae0e9e842888369391c776 # v46.3.10
|
||||
- uses: renovatebot/github-action@1c0cf989c02653fba0efe7ca5772338860ca71dc # v46.3.12
|
||||
with:
|
||||
renovate-version: ${{ env.RENOVATE_VERSION }}
|
||||
configurationFile: renovate.json5
|
||||
|
||||
2
.github/workflows/cron-translations.yml
vendored
2
.github/workflows/cron-translations.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: crowdin/github-action@959dc74ac104447731cba99a016f85d0ae719788 # v3.5.0
|
||||
- uses: crowdin/github-action@5b9790df740c85af5412a3a3a91b24988d5d324f # v3.5.1
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
|
||||
8
.github/workflows/files-changed.yml
vendored
8
.github/workflows/files-changed.yml
vendored
@@ -15,8 +15,6 @@ on:
|
||||
value: ${{ jobs.detect.outputs.templates }}
|
||||
docker:
|
||||
value: ${{ jobs.detect.outputs.docker }}
|
||||
dockerfile:
|
||||
value: ${{ jobs.detect.outputs.dockerfile }}
|
||||
swagger:
|
||||
value: ${{ jobs.detect.outputs.swagger }}
|
||||
yaml:
|
||||
@@ -42,7 +40,6 @@ jobs:
|
||||
actions: ${{ steps.changes.outputs.actions }}
|
||||
templates: ${{ steps.changes.outputs.templates }}
|
||||
docker: ${{ steps.changes.outputs.docker }}
|
||||
dockerfile: ${{ steps.changes.outputs.dockerfile }}
|
||||
swagger: ${{ steps.changes.outputs.swagger }}
|
||||
yaml: ${{ steps.changes.outputs.yaml }}
|
||||
json: ${{ steps.changes.outputs.json }}
|
||||
@@ -103,16 +100,11 @@ jobs:
|
||||
|
||||
docker:
|
||||
- ".github/workflows/pull-docker-dryrun.yml"
|
||||
- ".github/actions/docker-dryrun/**"
|
||||
- "Dockerfile"
|
||||
- "Dockerfile.rootless"
|
||||
- "docker/**"
|
||||
- "Makefile"
|
||||
|
||||
dockerfile:
|
||||
- "Dockerfile"
|
||||
- "Dockerfile.rootless"
|
||||
|
||||
swagger:
|
||||
- "templates/swagger/*.json"
|
||||
- "Makefile"
|
||||
|
||||
7
.github/workflows/giteabot-backport.yml
vendored
7
.github/workflows/giteabot-backport.yml
vendored
@@ -4,13 +4,14 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/v*
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
@@ -19,8 +20,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: go-gitea/giteabot@b830f7aa810e43afc1b2f8e8968a30754e6ff5d3 # v1.4.1
|
||||
- uses: go-gitea/giteabot@15b18fdedd98a80d0968d0bd9db3a9428814b808 # v1.5.1
|
||||
with:
|
||||
github_token: ${{ secrets.GITEABOT_TOKEN }}
|
||||
gitea_fork: giteabot/gitea
|
||||
checks: backport
|
||||
checks: backport,backport_conflict_free
|
||||
|
||||
6
.github/workflows/giteabot.yml
vendored
6
.github/workflows/giteabot.yml
vendored
@@ -31,7 +31,7 @@ on:
|
||||
checks:
|
||||
description: Comma-separated list of non-backport checks to run
|
||||
required: false
|
||||
default: backport_branches,labels,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions
|
||||
default: backport_branches,backport_conflict_free,labels,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
steps:
|
||||
- uses: go-gitea/giteabot@b830f7aa810e43afc1b2f8e8968a30754e6ff5d3 # v1.4.1
|
||||
- uses: go-gitea/giteabot@15b18fdedd98a80d0968d0bd9db3a9428814b808 # v1.5.1
|
||||
with:
|
||||
github_token: ${{ secrets.GITEABOT_TOKEN }}
|
||||
checks: ${{ github.event.inputs.checks || 'backport_branches,labels,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }}
|
||||
checks: ${{ github.event.inputs.checks || 'backport_branches,backport_conflict_free,labels,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }}
|
||||
|
||||
2
.github/workflows/pull-db-tests.yml
vendored
2
.github/workflows/pull-db-tests.yml
vendored
@@ -128,7 +128,7 @@ jobs:
|
||||
ports:
|
||||
- "9200:9200"
|
||||
meilisearch:
|
||||
image: getmeili/meilisearch:v1@sha256:1f578fa865f8251894b3bf38d6668d6209a3f451f8f65ab953e129d4c1db5ef4
|
||||
image: getmeili/meilisearch:v1@sha256:88f2a08eb9532cc54a60d8fd014a23bf5cfda33d0c01ecbbaca9b4b463e17466
|
||||
env:
|
||||
MEILI_ENV: development # disable auth
|
||||
ports:
|
||||
|
||||
38
.github/workflows/pull-docker-dryrun.yml
vendored
38
.github/workflows/pull-docker-dryrun.yml
vendored
@@ -14,34 +14,26 @@ jobs:
|
||||
files-changed:
|
||||
uses: ./.github/workflows/files-changed.yml
|
||||
|
||||
# QEMU-based build is slow (40-50 minutes), so run arm64 and riscv64 when dockerfile changes.
|
||||
# Run amd64 when any docker-related files change, which is fast (4 minutes).
|
||||
container-amd64:
|
||||
container:
|
||||
if: needs.files-changed.outputs.docker == 'true'
|
||||
needs: [files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: ./.github/actions/docker-dryrun
|
||||
- uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||
with:
|
||||
platform: linux/amd64
|
||||
|
||||
container-arm64:
|
||||
if: needs.files-changed.outputs.dockerfile == 'true'
|
||||
needs: [files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: ./.github/actions/docker-dryrun
|
||||
cache-image: false
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
- name: Build regular image
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
with:
|
||||
platform: linux/arm64
|
||||
|
||||
container-riscv64:
|
||||
if: needs.files-changed.outputs.dockerfile == 'true'
|
||||
needs: [files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: ./.github/actions/docker-dryrun
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
|
||||
- name: Build rootless image
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
with:
|
||||
platform: linux/riscv64
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
file: Dockerfile.rootless
|
||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
- Never assume, verify before claiming
|
||||
- List development targets with `make help`
|
||||
- Before working on code, read the `docs` guides for every area touched: `guidelines-backend.md` for Go, `guidelines-frontend.md` for TS, CSS and Vue, both for templates, `testing.md` for tests, `development.md` for config options and migrations, `guidelines-refactoring.md` for refactors
|
||||
- PR descriptions: minimal, only what and why, no task or file listings. Include screenshots for UI changes, before and after when modifying existing UI. Aim for less than 1000 characters
|
||||
- Before designing a feature or behavior, check prior art in GitHub and GitLab
|
||||
- Don't add code for theoretical edge cases, only for ones that can realistically occur. Ask humans to make decisions if the edge case matters, comment the edge case for why it can be ignored if it doesn't matter
|
||||
- Write docs concisely and user-facing, skip implementation details
|
||||
- PR descriptions: minimal, only what and why, no task or file listings, no headings. Aim for less than 1000 characters. References go at the end of the description like `Fixes: <url>`.
|
||||
- PR descriptions and update comments: Include screenshots for UI changes, before and after side by side. Take them at 4x scale in dark theme, cropped to the changes with some padding, and embed them at half CSS size
|
||||
- Reference issues and PRs by full URL, not by number
|
||||
- Use Conventional Commits for commit messages and PR titles, plus Gitea's `enhance` type for user-facing enhancements
|
||||
- Add an `Assisted-by: AGENT_NAME:MODEL_VERSION` trailer to commit messages, never `Co-Authored-By` or `Signed-off-by`
|
||||
- Add an `Assisted-by: AGENT_NAME:MODEL_VERSION` trailer to commit messages and PR descriptions, never `Co-Authored-By` or `Signed-off-by`
|
||||
- Attribute agent authorship on one trailing line in issue and PR comments, never as a PR description section
|
||||
- Never rewrite git history unless asked, update PRs with new commits and normal push
|
||||
- Comments: write almost none, short and preferably same-line, explaining why for a future reader. Never narrate code, the change or the prompt. Preserve existing ones that still apply. If you need to write a paragraph-long comment, rethink your implementation, it is likely too complicated
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@@ -8,8 +8,8 @@ RUN --mount=type=cache,target=/root/.local/share/pnpm/store pnpm install --froze
|
||||
COPY --exclude=.git/ . .
|
||||
RUN make frontend
|
||||
|
||||
# Build backend for each target platform
|
||||
FROM docker.io/library/golang:1.27-alpine3.24 AS build-env
|
||||
# Cross-compile backend on the native platform to avoid slow QEMU emulation
|
||||
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.27-alpine3.24 AS build-env
|
||||
|
||||
ARG GITEA_VERSION
|
||||
ARG TAGS=""
|
||||
@@ -28,11 +28,15 @@ RUN go mod download
|
||||
# ".git" directory is mounted separately later only for version data extraction.
|
||||
COPY --exclude=.git/ . .
|
||||
COPY --from=frontend-build /src/public/assets public/assets
|
||||
RUN --mount=type=cache,target="/root/.cache/go-build" make generate-backend
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
# Build gitea, .git mount is required for version data
|
||||
RUN --mount=type=cache,target="/root/.cache/go-build" \
|
||||
--mount=type=bind,source=".git/",target=".git/" \
|
||||
make backend
|
||||
GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#v} make gitea
|
||||
|
||||
COPY docker/root /tmp/local
|
||||
|
||||
@@ -48,7 +52,7 @@ FROM docker.io/library/alpine:3.24 AS gitea
|
||||
|
||||
EXPOSE 22 3000
|
||||
|
||||
RUN apk --no-cache add \
|
||||
RUN apk --no-cache upgrade && apk --no-cache add \
|
||||
bash \
|
||||
ca-certificates \
|
||||
curl \
|
||||
|
||||
@@ -8,8 +8,8 @@ RUN --mount=type=cache,target=/root/.local/share/pnpm/store pnpm install --froze
|
||||
COPY --exclude=.git/ . .
|
||||
RUN make frontend
|
||||
|
||||
# Build backend for each target platform
|
||||
FROM docker.io/library/golang:1.27-alpine3.24 AS build-env
|
||||
# Cross-compile backend on the native platform
|
||||
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.27-alpine3.24 AS build-env
|
||||
|
||||
ARG GITEA_VERSION
|
||||
ARG TAGS=""
|
||||
@@ -27,11 +27,15 @@ RUN go mod download
|
||||
# See the comments in Dockerfile
|
||||
COPY --exclude=.git/ . .
|
||||
COPY --from=frontend-build /src/public/assets public/assets
|
||||
RUN --mount=type=cache,target="/root/.cache/go-build" make generate-backend
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
# Build gitea, .git mount is required for version data
|
||||
RUN --mount=type=cache,target="/root/.cache/go-build" \
|
||||
--mount=type=bind,source=".git/",target=".git/" \
|
||||
make backend
|
||||
GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#v} make gitea
|
||||
|
||||
COPY docker/rootless /tmp/local
|
||||
|
||||
@@ -43,7 +47,7 @@ FROM docker.io/library/alpine:3.24 AS gitea-rootless
|
||||
|
||||
EXPOSE 2222 3000
|
||||
|
||||
RUN apk --no-cache add \
|
||||
RUN apk --no-cache upgrade && apk --no-cache add \
|
||||
bash \
|
||||
ca-certificates \
|
||||
dumb-init \
|
||||
|
||||
18
Makefile
18
Makefile
@@ -1,6 +1,6 @@
|
||||
DIST := dist
|
||||
DIST_DIRS := $(DIST)/binaries $(DIST)/release
|
||||
|
||||
HASH := \#
|
||||
GO ?= go
|
||||
SHASUM ?= shasum -a 256
|
||||
|
||||
@@ -8,7 +8,7 @@ AIR_PACKAGE ?= github.com/air-verse/air@v1.67.4 # renovate: datasource=go
|
||||
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v4/cmd/editorconfig-checker@v4.0.2 # renovate: datasource=go
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.15.0 # renovate: datasource=go
|
||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.17 # renovate: datasource=go
|
||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.36.6 # renovate: datasource=go
|
||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.36.7 # renovate: datasource=go
|
||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.9.0 # renovate: datasource=go
|
||||
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 # renovate: datasource=go
|
||||
SHELLCHECK_IMAGE ?= docker.io/koalaman/shellcheck:v0.11.0@sha256:61862eba1fcf09a484ebcc6feea46f1782532571a34ed51fedf90dd25f925a8d # renovate: datasource=docker
|
||||
@@ -385,10 +385,11 @@ test-check:
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: test-backend\#%
|
||||
test-backend\#%:
|
||||
TEST_BACKEND_GOALS := $(filter test-backend$(HASH)%,$(MAKECMDGOALS))
|
||||
.PHONY: $(TEST_BACKEND_GOALS)
|
||||
$(TEST_BACKEND_GOALS): test-backend$(HASH)%:
|
||||
@echo "Running go test with -tags '$(TAGS)'..."
|
||||
@$(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' -run '$(subst .,/,$*)' $(GO_TEST_PACKAGES)
|
||||
@$(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' -run '$*' $(GO_TEST_PACKAGES)
|
||||
|
||||
.PHONY: coverage
|
||||
coverage:
|
||||
@@ -443,9 +444,10 @@ test-integration: $(EXECUTABLE)
|
||||
test-integration-compile:
|
||||
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -c -o /dev/null gitea.dev/tests/integration
|
||||
|
||||
.PHONY: test-integration\#%
|
||||
test-integration\#%: $(EXECUTABLE)
|
||||
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -run '$(subst .,/,$*)' gitea.dev/tests/integration
|
||||
TEST_INTEGRATION_GOALS := $(filter test-integration$(HASH)%,$(MAKECMDGOALS))
|
||||
.PHONY: $(TEST_INTEGRATION_GOALS)
|
||||
$(TEST_INTEGRATION_GOALS): test-integration$(HASH)%: $(EXECUTABLE)
|
||||
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -run '$*' gitea.dev/tests/integration
|
||||
|
||||
.PHONY: test-migration
|
||||
test-migration: migrations.integration.test migrations.individual.test
|
||||
|
||||
18
SECURITY.md
18
SECURITY.md
@@ -14,12 +14,28 @@ Reports must be filed against a supported version. Before reporting, verify that
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please **DO NOT** file a public issue. Instead, report the vulnerability privately by opening a [GitHub security advisory](https://github.com/go-gitea/gitea/security/advisories/new), or optionally by sending your report to `security@gitea.io`.
|
||||
Please **DO NOT** file a public issue. Instead, report the vulnerability privately by opening a [GitHub security advisory](https://github.com/go-gitea/gitea/security/advisories/new), or optionally by sending your report to `security@gitea.io`. Former is preferred if given a choice.
|
||||
If reporting via GitHub, open an advisory per found issue. You can group the found issues if they relate to a single area (same feature, same fault present in multiple routes) but do not group unrelated reports into one as it makes it much harder to assess and track for duplicates.
|
||||
Do not use LLM to write the reports. It's meant for people to read so take your time and use your own words to write it. If you have used LLM to find the bug, be transparent about it and verify that it is not hallucination before submitting the report.
|
||||
|
||||
If your report turns out to be a duplicate, you will be credited in the original report.
|
||||
|
||||
### Report requirements
|
||||
|
||||
When filing the report please document it. Reports must include:
|
||||
|
||||
- Version or a range of versions it was successfully tested against. If reproducible against the `main` branch, a commit hash is also acceptable. Ambiguous versions like `latest` are not acceptable.
|
||||
- Settings used, especially when they diverge from the defaults
|
||||
- Reproduction steps from freshly installed instance. If the fault is only reproducible through upgrade from older version, reproduction should state that.
|
||||
|
||||
Take your time to format it properly as it'll be much easier to read. Prefer a concise writing style.
|
||||
When submitting an advisory, it should have the `gitea.dev` go package as package `name.cd`
|
||||
|
||||
### Triage process
|
||||
|
||||
Github does not have any search or tagging in GHSA so in most cases your report will leave triage state when we have a fix for it in works for next release.
|
||||
This helps us keep track of items fixed and pending release and separate them from things that we either haven't looked at yet or have not fixed.
|
||||
|
||||
## Protecting Security Information
|
||||
|
||||
Due to the sensitive nature of security information, you can use the below GPG public key to encrypt your mail body.
|
||||
|
||||
15
assets/go-licenses.json
generated
15
assets/go-licenses.json
generated
@@ -94,11 +94,6 @@
|
||||
"path": "github.com/andybalholm/brotli/LICENSE",
|
||||
"licenseText": "Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors.\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\nall copies 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\nTHE SOFTWARE.\n"
|
||||
},
|
||||
{
|
||||
"name": "github.com/andybalholm/brotli/flate",
|
||||
"path": "github.com/andybalholm/brotli/flate/LICENSE",
|
||||
"licenseText": "Copyright (c) 2009 The 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/andybalholm/cascadia",
|
||||
"path": "github.com/andybalholm/cascadia/LICENSE",
|
||||
@@ -959,16 +954,6 @@
|
||||
"path": "github.com/x448/float16/LICENSE",
|
||||
"licenseText": "MIT License\n\nCopyright (c) 2019 Montgomery Edwards⁴⁴⁸ and Faye Amacker\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\n"
|
||||
},
|
||||
{
|
||||
"name": "github.com/yohcop/openid-go",
|
||||
"path": "github.com/yohcop/openid-go/LICENSE",
|
||||
"licenseText": "Copyright 2015 Yohann Coppel\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
|
||||
},
|
||||
{
|
||||
"name": "github.com/yuin/goldmark-highlighting/v2",
|
||||
"path": "github.com/yuin/goldmark-highlighting/v2/LICENSE",
|
||||
"licenseText": "MIT License\n\nCopyright (c) 2019 Yusuke Inuzuka\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/yuin/goldmark",
|
||||
"path": "github.com/yuin/goldmark/LICENSE",
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gitea.dev/modules/json"
|
||||
)
|
||||
@@ -31,7 +32,9 @@ type emoji struct {
|
||||
description string
|
||||
}
|
||||
|
||||
var slugRe = regexp.MustCompile(`[^a-z0-9]+`)
|
||||
var slugRe = sync.OnceValue(func() *regexp.Regexp {
|
||||
return regexp.MustCompile(`[^a-z0-9]+`)
|
||||
})
|
||||
|
||||
func main() {
|
||||
if err := generate(); err != nil {
|
||||
@@ -108,7 +111,7 @@ func fetchEmojis(existingAliases map[string][]string) ([]*emoji, error) {
|
||||
for _, e := range emojis {
|
||||
e.Aliases = existingAliases[e.Emoji]
|
||||
if e.Aliases == nil {
|
||||
e.Aliases = []string{strings.Trim(slugRe.ReplaceAllString(strings.ReplaceAll(strings.ToLower(e.description), "’", ""), "_"), "_")}
|
||||
e.Aliases = []string{strings.Trim(slugRe().ReplaceAllString(strings.ReplaceAll(strings.ToLower(e.description), "’", ""), "_"), "_")}
|
||||
}
|
||||
delete(existingAliases, e.Emoji)
|
||||
for _, alias := range e.Aliases {
|
||||
|
||||
@@ -15,17 +15,24 @@ import (
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// regexp is based on go-license, excluding README and NOTICE
|
||||
// https://github.com/google/go-licenses/blob/master/licenses/find.go
|
||||
// also defined in vite.config.ts
|
||||
var licenseRe = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING).*$`)
|
||||
|
||||
// primaryLicenseRe matches exact primary license filenames without suffixes.
|
||||
// When a directory has both primary and variant files (e.g. LICENSE and
|
||||
// LICENSE.docs), only the primary files are kept.
|
||||
var primaryLicenseRe = regexp.MustCompile(`^(?i)(LICEN[SC]E|COPYING)$`)
|
||||
var globalVars = sync.OnceValue(func() (ret struct {
|
||||
licenseRe *regexp.Regexp
|
||||
primaryLicenseRe *regexp.Regexp
|
||||
},
|
||||
) {
|
||||
// regexp is based on go-license, excluding README and NOTICE
|
||||
// https://github.com/google/go-licenses/blob/master/licenses/find.go
|
||||
// also defined in vite.config.ts
|
||||
ret.licenseRe = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING).*$`)
|
||||
// primaryLicenseRe matches exact primary license filenames without suffixes.
|
||||
// When a directory has both primary and variant files (e.g. LICENSE and
|
||||
// LICENSE.docs), only the primary files are kept.
|
||||
ret.primaryLicenseRe = regexp.MustCompile(`^(?i)(LICEN[SC]E|COPYING)$`)
|
||||
return ret
|
||||
})
|
||||
|
||||
// ignoredNames are LicenseEntry.Name values to exclude from the output.
|
||||
var ignoredNames = map[string]bool{
|
||||
@@ -144,7 +151,7 @@ func scanDirForLicenses(dir, modulePath, moduleRoot string) []LicenseEntry {
|
||||
continue
|
||||
}
|
||||
name := entry.Name()
|
||||
if !licenseRe.MatchString(name) {
|
||||
if !globalVars().licenseRe.MatchString(name) {
|
||||
continue
|
||||
}
|
||||
if excludedExt[strings.ToLower(filepath.Ext(name))] {
|
||||
@@ -181,7 +188,7 @@ func scanDirForLicenses(dir, modulePath, moduleRoot string) []LicenseEntry {
|
||||
var primary []LicenseEntry
|
||||
for _, e := range entries {
|
||||
fileName := e.Path[strings.LastIndex(e.Path, "/")+1:]
|
||||
if primaryLicenseRe.MatchString(fileName) {
|
||||
if globalVars().primaryLicenseRe.MatchString(fileName) {
|
||||
primary = append(primary, e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gitea.dev/modules/json"
|
||||
|
||||
@@ -16,11 +17,19 @@ import (
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
)
|
||||
|
||||
// rxDeprecated matches "deprecated" as a word at the start of a description
|
||||
// or preceded by whitespace/punctuation that indicates a leading marker (e.g.
|
||||
// "Deprecated: true", "deprecated (use X instead)"). Rejects negated phrases
|
||||
// like "not deprecated" or "previously deprecated, now supported".
|
||||
var rxDeprecated = regexp.MustCompile(`(?i)(?:^|[\n.;])\s*deprecated\b`)
|
||||
var globalVars = sync.OnceValue(func() (ret struct {
|
||||
rxDeprecated *regexp.Regexp
|
||||
rxSwaggerEnum *regexp.Regexp
|
||||
},
|
||||
) {
|
||||
// rxDeprecated matches "deprecated" as a word at the start of a description
|
||||
// or preceded by whitespace/punctuation that indicates a leading marker (e.g.
|
||||
// "Deprecated: true", "deprecated (use X instead)"). Rejects negated phrases
|
||||
// like "not deprecated" or "previously deprecated, now supported".
|
||||
ret.rxDeprecated = regexp.MustCompile(`(?i)(?:^|[\n.;])\s*deprecated\b`)
|
||||
ret.rxSwaggerEnum = regexp.MustCompile(`swagger:enum\s+(\w+)`)
|
||||
return ret
|
||||
})
|
||||
|
||||
// Convert parses a Swagger 2.0 spec and returns an OAS3 spec, applying
|
||||
// Gitea-specific post-processing: file-schema fixups, URI formats,
|
||||
@@ -158,7 +167,7 @@ func addDeprecatedFlags(doc *openapi3.T) {
|
||||
if propRef == nil || propRef.Value == nil || propRef.Ref != "" {
|
||||
continue
|
||||
}
|
||||
if rxDeprecated.MatchString(propRef.Value.Description) {
|
||||
if globalVars().rxDeprecated.MatchString(propRef.Value.Description) {
|
||||
propRef.Value.Deprecated = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -31,8 +30,6 @@ func EnumKey(values []any) string {
|
||||
return strings.Join(strs, "|")
|
||||
}
|
||||
|
||||
var rxSwaggerEnum = regexp.MustCompile(`swagger:enum\s+(\w+)`)
|
||||
|
||||
// ScanSwaggerEnumTypes walks .go files under each dir and returns a map from
|
||||
// a canonical value-set key (see EnumKey) to the Go type names declared with
|
||||
// // swagger:enum TypeName. Multiple type names per key are allowed (e.g.
|
||||
@@ -146,7 +143,7 @@ func registerEnumAnnotation(doc *ast.CommentGroup, specs []ast.Spec, enumTypes m
|
||||
if doc == nil {
|
||||
return nil
|
||||
}
|
||||
matches := rxSwaggerEnum.FindStringSubmatch(doc.Text())
|
||||
matches := globalVars().rxSwaggerEnum.FindStringSubmatch(doc.Text())
|
||||
if len(matches) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -49,11 +49,11 @@ func commonLdapCLIFlags() []cli.Flag {
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "host",
|
||||
Usage: "The address where the LDAP server can be reached.",
|
||||
Usage: "Comma-separated LDAP hosts or ldap://, ldaps:// or ldap+starttls:// URLs, tried in order.",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "port",
|
||||
Usage: "The port to use when connecting to the LDAP server.",
|
||||
Usage: "The port to use when connecting to hosts given without a URL.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "user-search-base",
|
||||
|
||||
@@ -90,7 +90,7 @@ func newDumpCommand() *cli.Command {
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Usage: `Dump output format, default to "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "),
|
||||
Usage: `Dump output format, default to the --file extension if supported, else "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -261,13 +261,7 @@ func extractAsset(c *cli.Command, d string, a assetFile, overwrite, rename bool)
|
||||
perms = fi.Mode()
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(dest, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, perms)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", dest, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err = file.Write(data); err != nil {
|
||||
if err := os.WriteFile(dest, data, perms); err != nil {
|
||||
return fmt.Errorf("%s: %w", dest, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1145,11 +1145,6 @@ LEVEL = Info
|
||||
;; The default Git object format of new repositories. Available values: sha1, sha256.
|
||||
;DEFAULT_OBJECT_FORMAT = sha1
|
||||
;;
|
||||
;; Allow adoption of unadopted repositories
|
||||
;ALLOW_ADOPTION_OF_UNADOPTED_REPOSITORIES = false
|
||||
;;
|
||||
;; Allow deletion of unadopted repositories
|
||||
;ALLOW_DELETION_OF_UNADOPTED_REPOSITORIES = false
|
||||
|
||||
;; Don't allow download source archive files from UI
|
||||
;DISABLE_DOWNLOAD_SOURCE_ARCHIVES = false
|
||||
@@ -1242,7 +1237,7 @@ LEVEL = Info
|
||||
;DELAY_CHECK_FOR_INACTIVE_DAYS = 7
|
||||
;;
|
||||
;; Set the default value for "Delete pull request branch after merge by default" for new repositories
|
||||
;DEFAULT_DELETE_BRANCH_AFTER_MERGE = false
|
||||
;DEFAULT_DELETE_BRANCH_AFTER_MERGE = true
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -1721,58 +1716,18 @@ LEVEL = Info
|
||||
;; - manage_ssh_keys: a user cannot configure ssh keys
|
||||
;; - manage_gpg_keys: a user cannot configure gpg keys
|
||||
;; - manage_mfa: a user cannot configure mfa devices
|
||||
;; - manage_credentials: a user cannot configure emails, passwords, or openid
|
||||
;; - manage_credentials: a user cannot configure emails or passwords
|
||||
;USER_DISABLED_FEATURES =
|
||||
;; Comma separated list of disabled features ONLY if the user has an external login type (eg. LDAP, Oauth, etc.), could be "deletion", "manage_ssh_keys", "manage_gpg_keys", "manage_mfa", "manage_credentials". This setting is independent from `USER_DISABLED_FEATURES` and supplements its behavior.
|
||||
;; - 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
|
||||
;; - manage_mfa: a user cannot configure mfa devices
|
||||
;; - manage_credentials: a user cannot configure emails, passwords, or openid
|
||||
;; - manage_credentials: a user cannot configure emails or passwords
|
||||
;; - change_username: a user cannot change their username
|
||||
;; - change_full_name: a user cannot change their full name
|
||||
;;EXTERNAL_USER_DISABLE_FEATURES =
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;[openid]
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;
|
||||
;; OpenID is an open, standard and decentralized authentication protocol.
|
||||
;; Your identity is the address of a webpage you provide, which describes
|
||||
;; how to prove you are in control of that page.
|
||||
;;
|
||||
;; For more info: https://en.wikipedia.org/wiki/OpenID
|
||||
;;
|
||||
;; Current implementation supports OpenID-2.0
|
||||
;;
|
||||
;; Tested to work providers at the time of writing:
|
||||
;; - Any GNUSocial node (your.hostname.tld/username)
|
||||
;; - Any SimpleID provider (http://simpleid.koinic.net)
|
||||
;; - http://openid.org.cn/
|
||||
;; - openid.stackexchange.com
|
||||
;; - login.launchpad.net
|
||||
;; - <username>.livejournal.com
|
||||
;;
|
||||
;; Whether to allow sign-in via OpenID
|
||||
;ENABLE_OPENID_SIGNIN = false
|
||||
;;
|
||||
;; Whether to allow registering via OpenID
|
||||
;;ENABLE_OPENID_SIGNUP = false
|
||||
;;
|
||||
;; Allowed URI patterns (POSIX regexp).
|
||||
;; Space separated.
|
||||
;; Only these would be allowed if non-blank.
|
||||
;; Example value: trusted.domain.org trusted.domain.net
|
||||
;WHITELISTED_URIS =
|
||||
;;
|
||||
;; Forbidden URI patterns (POSIX regexp).
|
||||
;; Space separated.
|
||||
;; Only used if WHITELISTED_URIS is blank.
|
||||
;; Example value: loadaverage.org/badguy stackexchange.com/.*spammer
|
||||
;BLACKLISTED_URIS =
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;[oauth2_client]
|
||||
@@ -2098,7 +2053,7 @@ LEVEL = Info
|
||||
;ENABLED = true
|
||||
;;
|
||||
;; Comma-separated list of allowed issue/pull-request attachment file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
|
||||
;ALLOWED_TYPES = .avif,.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.webp,.xls,.xlsx,.zip
|
||||
;ALLOWED_TYPES = .avif,.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.jxl,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.webp,.xls,.xlsx,.zip
|
||||
;;
|
||||
;; Max size of each issue/pull-request attachment file. Defaults to 100MB
|
||||
;MAX_SIZE = 100
|
||||
@@ -2925,6 +2880,8 @@ LEVEL = Info
|
||||
;LIMIT_SIZE_VAGRANT = -1
|
||||
;; Maximum size of a Terraform state upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
;LIMIT_SIZE_TERRAFORM_STATE = -1
|
||||
;; Maximum size of a Terraform module archive upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
;LIMIT_SIZE_TERRAFORM_MODULE = -1
|
||||
;; Enable RPM re-signing by default. (It will overwrite the old signature ,using v4 format, not compatible with CentOS 6 or older)
|
||||
;DEFAULT_RPM_SIGN_ENABLED = false
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -88,6 +88,13 @@ conditional `UPDATE` or a lock.
|
||||
struct conditions and inserts of more than 1000 rows.
|
||||
- Paginated queries need a unique tiebreaker like `id` for a stable order.
|
||||
|
||||
### Git
|
||||
|
||||
- Pass already opened repositories down or use `git.RepositoryFromContextOrOpen`
|
||||
rather than reopening them or spawning extra `git` processes.
|
||||
- If git command stderr output is not handled by the caller, then it needs to use RunWithStderr
|
||||
to make sure the git error message is included in the returned error and displayed in the logs.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Go dependencies are managed with [Go Modules](https://go.dev/ref/mod).
|
||||
|
||||
@@ -24,6 +24,8 @@ Now, that's not a requirement anymore as our [backport bot](https://github.com/G
|
||||
|
||||
The `backport/manual` label signifies either that you want to backport the change yourself, or that there were conflicts when backporting, thus you **must** do it yourself.
|
||||
|
||||
The bot labels open PRs targeting `main` that have a `backport/<version>` label with `backport/conflict-free` when they would cherry-pick onto the latest release branch without conflicts.
|
||||
|
||||
### Format of backport PRs
|
||||
|
||||
The title of backport PRs should be
|
||||
|
||||
@@ -211,7 +211,7 @@ export default defineConfig([
|
||||
'@typescript-eslint/no-magic-numbers': [0],
|
||||
'@typescript-eslint/no-meaningless-void-operator': [0],
|
||||
'@typescript-eslint/no-misused-new': [2],
|
||||
'@typescript-eslint/no-misused-promises': [2, {checksVoidReturn: {attributes: false, arguments: false}}],
|
||||
'@typescript-eslint/no-misused-promises': [2, {checksVoidReturn: {attributes: false, arguments: false, properties: false}}],
|
||||
'@typescript-eslint/no-misused-spread': [2],
|
||||
'@typescript-eslint/no-mixed-enums': [0],
|
||||
'@typescript-eslint/no-namespace': [2],
|
||||
|
||||
34
go.mod
34
go.mod
@@ -2,14 +2,14 @@ module gitea.dev
|
||||
|
||||
go 1.27
|
||||
|
||||
toolchain go1.27.3
|
||||
toolchain go1.27.4
|
||||
|
||||
require (
|
||||
connectrpc.com/connect/v2 v2.0.2
|
||||
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
|
||||
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
|
||||
gitea.dev/actionslib v1.5.0
|
||||
gitea.dev/sdk v1.5.0
|
||||
gitea.com/lunny/levelqueue v0.5.1
|
||||
gitea.dev/actionslib v1.5.2
|
||||
gitea.dev/sdk v1.5.1
|
||||
github.com/42wim/httpsig v1.3.0
|
||||
github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432
|
||||
github.com/Azure/go-ntlmssp v0.1.1
|
||||
@@ -19,14 +19,14 @@ require (
|
||||
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0
|
||||
github.com/alecthomas/chroma/v2 v2.27.0
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
||||
github.com/blevesearch/bleve/v2 v2.6.1
|
||||
github.com/blevesearch/bleve/v2 v2.6.2
|
||||
github.com/bohde/codel v0.2.0
|
||||
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c
|
||||
github.com/buildkite/terminal-to-html/v3 v3.17.3
|
||||
github.com/caddyserver/certmagic v0.25.6
|
||||
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20260812203852-971c0284dc33
|
||||
github.com/coder/websocket v1.8.15
|
||||
github.com/dlclark/regexp2/v2 v2.11.0
|
||||
github.com/dlclark/regexp2/v2 v2.12.0
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
|
||||
github.com/dustin/go-humanize v1.1.0
|
||||
github.com/editorconfig/editorconfig-core-go/v2 v2.6.5
|
||||
@@ -79,14 +79,12 @@ require (
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/syndtr/goleveldb v1.0.0
|
||||
github.com/ulikunitz/xz v0.5.17
|
||||
github.com/urfave/cli-docs/v3 v3.1.0
|
||||
github.com/urfave/cli-docs/v3 v3.2.0
|
||||
github.com/urfave/cli/v3 v3.14.0
|
||||
github.com/wneessen/go-mail v0.8.1
|
||||
github.com/yohcop/openid-go v1.0.1
|
||||
github.com/yuin/goldmark v1.8.6
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
gitlab.com/gitlab-org/api/client-go/v3 v3.39.1
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6.0.20261013184558-04dd63131525
|
||||
github.com/yuin/goldmark v1.8.7
|
||||
gitlab.com/gitlab-org/api/client-go/v3 v3.43.0
|
||||
go.yaml.in/yaml/v4 v4.0.0
|
||||
golang.org/x/crypto v0.58.0
|
||||
golang.org/x/image v0.47.0
|
||||
golang.org/x/mod v0.42.0
|
||||
@@ -109,13 +107,13 @@ require (
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/RoaringBitmap/roaring/v2 v2.18.2 // indirect
|
||||
github.com/STARRY-S/zip v0.2.3 // indirect
|
||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||
github.com/andybalholm/brotli v1.2.7 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.4 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.5 // indirect
|
||||
github.com/blevesearch/bleve_index_api v1.4.1 // indirect
|
||||
github.com/blevesearch/geo v0.2.6 // indirect
|
||||
github.com/blevesearch/geo v0.2.7-0.20260831141219-9c3e3a4d84bc // indirect
|
||||
github.com/blevesearch/go-faiss v1.1.5 // indirect
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
|
||||
github.com/blevesearch/gtreap v0.1.1 // indirect
|
||||
@@ -124,15 +122,15 @@ require (
|
||||
github.com/blevesearch/segment v0.9.1 // indirect
|
||||
github.com/blevesearch/snowballstem v0.9.0 // indirect
|
||||
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
|
||||
github.com/blevesearch/vellum v1.2.0 // indirect
|
||||
github.com/blevesearch/vellum v1.2.1 // indirect
|
||||
github.com/blevesearch/zapx/v11 v11.4.3 // indirect
|
||||
github.com/blevesearch/zapx/v12 v12.4.3 // indirect
|
||||
github.com/blevesearch/zapx/v13 v13.4.3 // indirect
|
||||
github.com/blevesearch/zapx/v14 v14.4.3 // indirect
|
||||
github.com/blevesearch/zapx/v15 v15.4.3 // indirect
|
||||
github.com/blevesearch/zapx/v16 v16.3.4 // indirect
|
||||
github.com/blevesearch/zapx/v17 v17.2.3 // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.3 // indirect
|
||||
github.com/blevesearch/zapx/v17 v17.2.4 // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.4 // indirect
|
||||
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||
github.com/bodgit/sevenzip v1.6.4 // indirect
|
||||
github.com/bodgit/windows v1.0.1 // indirect
|
||||
@@ -223,7 +221,7 @@ require (
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.28.0 // indirect
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.6 // indirect
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||
golang.org/x/time v0.16.0 // indirect
|
||||
|
||||
84
go.sum
84
go.sum
@@ -8,14 +8,14 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:+wWBi6Qfruqu7xJgjOIrKVQGiLUZdpKYCZewJ4clqhw=
|
||||
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY=
|
||||
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o=
|
||||
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4/go.mod h1:HBqmLbz56JWpfEGG0prskAV97ATNRoj5LDmPicD22hU=
|
||||
gitea.com/lunny/levelqueue v0.5.1 h1:15q1oBWaO2Aw9t3rDthQSTX+vBNgjYNkCgRJgES9gA8=
|
||||
gitea.com/lunny/levelqueue v0.5.1/go.mod h1:HBqmLbz56JWpfEGG0prskAV97ATNRoj5LDmPicD22hU=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
|
||||
gitea.dev/actionslib v1.5.0 h1:dC3v2HTWbbvyNBSdg2W3kd4WNC/p+FnZaKpl1MdzM00=
|
||||
gitea.dev/actionslib v1.5.0/go.mod h1:C9wmOFgDBCXyRvg4A3KTX8Vszs8v9+elH4QLGVDolqU=
|
||||
gitea.dev/sdk v1.5.0 h1:qxtIHpkid4pva3id7S+UlJIGJAAQ8/riQidqD3jzErc=
|
||||
gitea.dev/sdk v1.5.0/go.mod h1:cM7S3Y9TY3LraJoRHHDLlY3YnVTu4bTc9TEBGuFa2Jw=
|
||||
gitea.dev/actionslib v1.5.2 h1:PSSUd+4jIjI+ycqo62+BRgAWkBnzjgeGQAcI3ZqH7LY=
|
||||
gitea.dev/actionslib v1.5.2/go.mod h1:i8wUQMnvGwT13f2re2ZTMCFL/TyauQLeOb1L4ngjtPk=
|
||||
gitea.dev/sdk v1.5.1 h1:bMBU3rPoU87a8OOgtfu5w4sZn2x/ydwNAkqFLfIN8c8=
|
||||
gitea.dev/sdk v1.5.1/go.mod h1:cM7S3Y9TY3LraJoRHHDLlY3YnVTu4bTc9TEBGuFa2Jw=
|
||||
github.com/42wim/httpsig v1.3.0 h1:HK2bieeZ99X9IvOoc1ST6yrhLek0f4MqlEGES2JCP3g=
|
||||
github.com/42wim/httpsig v1.3.0/go.mod h1:lgC3Yrvp4lxi1odEF1ge3kQBu/VM+SRcZQu5YwiR3dU=
|
||||
github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432 h1:3Fcz1QzlS7Jv4FT2KI3cHNSZL+KPN3dXxurn9f3YL/Y=
|
||||
@@ -48,16 +48,14 @@ github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0 h1:LvK7+C6qgz8BPnmn7x
|
||||
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0/go.mod h1:I28hc9eaiqKCoOB+9Wh/P1IOScfm0xCgyWC6DAo0lmo=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
|
||||
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
|
||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/brotli v1.2.7 h1:UgafRPGMam5ySHfRkhthMqOFA6IFoeF3D7mWIDLjG5A=
|
||||
github.com/andybalholm/brotli v1.2.7/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/cascadia v1.3.4 h1:vM2lgh0Vru9Vwyfm4cQqWP2HHMW0u0+2PAW7Q38Qufg=
|
||||
github.com/andybalholm/cascadia v1.3.4/go.mod h1:BLRmbRjpEtNKieZOCCvYj4RqN+KRA41GBe/5O+G93kM=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
@@ -68,12 +66,12 @@ github.com/bits-and-blooms/bitset v1.24.5 h1:654xBVHc23gJMAgOTkPNoCVfiRxuIOAUnAZ
|
||||
github.com/bits-and-blooms/bitset v1.24.5/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
|
||||
github.com/blevesearch/bleve/v2 v2.6.1 h1:47vLskRTqxvQEtxVPYHjf5KpOgzD2msslXFjvUQCgWQ=
|
||||
github.com/blevesearch/bleve/v2 v2.6.1/go.mod h1:Dvvx6ZoEBTOj6RSzfk0lEz0wce/qhe2yOUubXeuzd2c=
|
||||
github.com/blevesearch/bleve/v2 v2.6.2 h1:CaPS1fjYgp+ejW5Nd7OTmVTCkuFzzqyjrADi/XCizdU=
|
||||
github.com/blevesearch/bleve/v2 v2.6.2/go.mod h1:w86Ae7GPU0r5IW2S6OSrdUojZY3z7WmOb2pgAnfg8fQ=
|
||||
github.com/blevesearch/bleve_index_api v1.4.1 h1:CYIyecFlI+/RYjzUm+NmDjYbSvk870Bb7f+Vl4b12q8=
|
||||
github.com/blevesearch/bleve_index_api v1.4.1/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko=
|
||||
github.com/blevesearch/geo v0.2.6 h1:7K1oyQKYlauC+mJuo2AfNPyjN/4mihEoJMfyClVH1Mo=
|
||||
github.com/blevesearch/geo v0.2.6/go.mod h1:6qzVUiB4BK47QkSZcRqiXEP2W3EeXuzM5XFTF8AdZ8A=
|
||||
github.com/blevesearch/geo v0.2.7-0.20260831141219-9c3e3a4d84bc h1:2nCheDGYfGpD+w6d1czqyzuLXUgQpuk8tTi66si470c=
|
||||
github.com/blevesearch/geo v0.2.7-0.20260831141219-9c3e3a4d84bc/go.mod h1:6qzVUiB4BK47QkSZcRqiXEP2W3EeXuzM5XFTF8AdZ8A=
|
||||
github.com/blevesearch/go-faiss v1.1.5 h1:/IU5lkOahH9Ghfk9n3F6N0XD7PYVXZJWmNDc9TtXuco=
|
||||
github.com/blevesearch/go-faiss v1.1.5/go.mod h1:w3W9AiWsFRGVaMG+/cmJi7iHEAuGyC6blsgO1EzCK/M=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
|
||||
@@ -90,8 +88,8 @@ github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD
|
||||
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
|
||||
github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A=
|
||||
github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
|
||||
github.com/blevesearch/vellum v1.2.0 h1:xkDiOEsHc2t3Cp0NsNZZ36pvc130sCzcGKOPMzXe+e0=
|
||||
github.com/blevesearch/vellum v1.2.0/go.mod h1:uEcfBJz7mAOf0Kvq6qoEKQQkLODBF46SINYNkZNae4k=
|
||||
github.com/blevesearch/vellum v1.2.1 h1:PYryJDqBHjLM6n3YindZSQcrseaMs8QpcsrrAOpwwj8=
|
||||
github.com/blevesearch/vellum v1.2.1/go.mod h1:uEcfBJz7mAOf0Kvq6qoEKQQkLODBF46SINYNkZNae4k=
|
||||
github.com/blevesearch/zapx/v11 v11.4.3 h1:PTZOO5loKpHC/x/GzmPZNa9cw7GZIQxd5qRjwij9tHY=
|
||||
github.com/blevesearch/zapx/v11 v11.4.3/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc=
|
||||
github.com/blevesearch/zapx/v12 v12.4.3 h1:eElXvAaAX4m04t//CGBQAtHNPA+Q6A1hHZVrN3LSFYo=
|
||||
@@ -104,10 +102,10 @@ github.com/blevesearch/zapx/v15 v15.4.3 h1:iJiMJOHrz216jyO6lS0m9RTCEkprUnzvqAI2l
|
||||
github.com/blevesearch/zapx/v15 v15.4.3/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
|
||||
github.com/blevesearch/zapx/v16 v16.3.4 h1:hDAqA8qusZTNbPEL7//w5P65UZ2de6yhSeUaTbp0Po0=
|
||||
github.com/blevesearch/zapx/v16 v16.3.4/go.mod h1:zqkPPqs9GS9FzVWzCO3Wf1X044yWAV17+4zb+FTiEHg=
|
||||
github.com/blevesearch/zapx/v17 v17.2.3 h1:UYYJPAt5b2tVxldx5h0jmv23RMsg8/UZKFVya7v92po=
|
||||
github.com/blevesearch/zapx/v17 v17.2.3/go.mod h1:r7mb4QWbDQSkbAnOjCb9iCfkcrzajB4yBdJpuBIo/fE=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.3 h1:1UIv1+fqH3++ViVkpnaPTB+Pi1UHycnPFAq14zgK+gI=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.3/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/blevesearch/zapx/v17 v17.2.4 h1:6BekwKXVubfivq87E91dqJgRDhmguA0Hvm1JTyqNzWM=
|
||||
github.com/blevesearch/zapx/v17 v17.2.4/go.mod h1:8gLqU0Hr+MuFCBbL/lDS3O9MiTFjyoTcLXkiVCjwqMs=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.4 h1:oTKkRmRuOsKMqFe1AQazd9gGBFNEpOmgKp/xzB97dnA=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.4/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q=
|
||||
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
|
||||
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
|
||||
@@ -162,12 +160,10 @@ github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454Wv
|
||||
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2/v2 v2.11.0 h1:z5uRPHav+eTVpoffDSuUP2KPS4f1ONZaPDK73S8HA0w=
|
||||
github.com/dlclark/regexp2/v2 v2.11.0/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
|
||||
github.com/dlclark/regexp2/v2 v2.12.0 h1:GsCgDZZeHluUDHASr2yqQonZk4Tma5LFmLH9CSsFFEE=
|
||||
github.com/dlclark/regexp2/v2 v2.12.0/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
@@ -528,7 +524,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
@@ -545,8 +540,8 @@ github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77ro
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.17 h1:flR0y/x1hgM8EGV1AW3Xll6T413G0glV8UfBwR617V4=
|
||||
github.com/ulikunitz/xz v0.5.17/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw=
|
||||
github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw=
|
||||
github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to=
|
||||
github.com/urfave/cli-docs/v3 v3.2.0 h1:1J2pmtDuMRConzYb/QHyPbeFB6HvMtYm3xre9uhs81c=
|
||||
github.com/urfave/cli-docs/v3 v3.2.0/go.mod h1:cjSVza4yCaqQet06zO6QhYqXQYjGRqbUj8zok6mHDRU=
|
||||
github.com/urfave/cli/v3 v3.14.0 h1:a8414NQlHJs0c/iBsulKLzlES0n/lEAskbL2LKpU4/s=
|
||||
github.com/urfave/cli/v3 v3.14.0/go.mod h1:vXn6HxPNccJSzQr2QvwVncOKrgYGIHU0HY5h8B2nQj4=
|
||||
github.com/wneessen/go-mail v0.8.1 h1:tVcncj02/QySVFw3zr/kXOzZcuFQqBNT6K+Rbgm/pcM=
|
||||
@@ -555,15 +550,9 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yohcop/openid-go v1.0.1 h1:DPRd3iPO5F6O5zX2e62XpVAbPT6wV51cuucH0z9g3js=
|
||||
github.com/yohcop/openid-go v1.0.1/go.mod h1:b/AvD03P0KHj4yuihb+VtLD6bYYgsy0zqBzPCRjkCNs=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4=
|
||||
github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
||||
github.com/yuin/goldmark v1.8.7 h1:XT/tNqCIbabh0ln675w4x3dv04kQBKi8NwYqi9IJaxI=
|
||||
github.com/yuin/goldmark v1.8.7/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
||||
@@ -572,8 +561,8 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
gitlab.com/gitlab-org/api/client-go/v3 v3.39.1 h1:yBmlSUi5P6L354Aq3PJCWvW24zz9Ru3WnAXgNMwA9g8=
|
||||
gitlab.com/gitlab-org/api/client-go/v3 v3.39.1/go.mod h1:jBFhGkTHLKkgeHEdS/A8yA0tQ6UHq96QVsS5muu6flM=
|
||||
gitlab.com/gitlab-org/api/client-go/v3 v3.43.0 h1:X7qHPPt6If/5f0bgBpeT8GYWx2zDnuUzRsfnzJa9/cc=
|
||||
gitlab.com/gitlab-org/api/client-go/v3 v3.43.0/go.mod h1:8cuzgeUJzkVvaRvw57iZ8foGmL5q8GZXi4lHTiF5edY=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
go.uber.org/atomic v1.12.0 h1:BvcXdFKuviU4fTL/f+SxdQ5qJX/Jix8pAkgdUcb3XOE=
|
||||
@@ -590,17 +579,16 @@ go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
|
||||
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6.0.20261013184558-04dd63131525 h1:FtzHH3s777Nx50YcPAcodAPm1mNakc228nMYbr3f+90=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6.0.20261013184558-04dd63131525/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
go.yaml.in/yaml/v3 v3.0.6 h1:zcDfvjXEqTQmpjniUBLc91xYJMB2c4QOFxEF/LCEckM=
|
||||
go.yaml.in/yaml/v3 v3.0.6/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
go.yaml.in/yaml/v4 v4.0.0 h1:/LG1pNO84Z4l603j/xMn69CcS5OYhMQ/f7l2LoQUGuw=
|
||||
go.yaml.in/yaml/v4 v4.0.0/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
|
||||
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
@@ -608,7 +596,6 @@ golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aI
|
||||
golang.org/x/image v0.47.0 h1:za/8gVz++PPZqmZJBNSISICN5Rrj5zixP/8K++fgmqo=
|
||||
golang.org/x/image v0.47.0/go.mod h1:IkN5o4A7vq55vXN8FWjjnhg9Rs2GOtffqsSi9mWAKYE=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg=
|
||||
golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -617,8 +604,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY=
|
||||
golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
|
||||
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
|
||||
@@ -627,7 +612,6 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
|
||||
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -641,24 +625,17 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
|
||||
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
|
||||
golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
|
||||
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
|
||||
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
|
||||
@@ -666,7 +643,6 @@ golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs=
|
||||
golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -441,6 +441,10 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(362, "Add reusable_workflow_ref column to action_run_job", v29.AddReusableWorkflowRefToActionRunJob),
|
||||
newMigration(363, "Add release_id to reaction table", v29.AddReleaseIDToReaction),
|
||||
newMigration(364, "Add new table project_workflow", v29.AddProjectWorkflow),
|
||||
newMigration(365, "Normalize legacy repository unit and login source JSON", v29.NormalizeLegacyConfigJSON),
|
||||
newMigration(366, "Drop user_open_id table", v29.DropUserOpenIDTable),
|
||||
newMigration(367, "Add latest_release_id to repository table", v29.AddLatestReleaseIDToRepository),
|
||||
newMigration(368, "Add unique index for team name", v29.AddUniqueTeamNameIndex),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
95
modelmigration/v29/v365.go
Normal file
95
modelmigration/v29/v365.go
Normal file
@@ -0,0 +1,95 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v29
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
|
||||
"gitea.dev/modelmigration/base"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
// unmarshalHandleDoubleEncode - due to a bug in xorm (see https://gitea.com/xorm/xorm/pulls/1957) - it's
|
||||
// possible that a Blob may be double encoded or gain an unwanted prefix of 0xff 0xfe.
|
||||
func unmarshalHandleDoubleEncode(bs []byte, v any) error {
|
||||
if len(bs) == 0 {
|
||||
// json.Unmarshal will report errors if input is empty (nil or zero-length)
|
||||
// It seems that XORM ignores the nil but still passes zero-length string into this function
|
||||
// To be consistent, we should treat all empty inputs as success
|
||||
return nil
|
||||
}
|
||||
err := json.Unmarshal(bs, v)
|
||||
if err != nil {
|
||||
ok := true
|
||||
rs := []byte{}
|
||||
temp := make([]byte, 2)
|
||||
for _, rn := range string(bs) {
|
||||
if rn > 0xffff {
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
binary.LittleEndian.PutUint16(temp, uint16(rn))
|
||||
rs = append(rs, temp...)
|
||||
}
|
||||
if ok {
|
||||
if len(rs) > 1 && rs[0] == 0xff && rs[1] == 0xfe {
|
||||
rs = rs[2:]
|
||||
}
|
||||
err = json.Unmarshal(rs, v)
|
||||
}
|
||||
}
|
||||
if err != nil && len(bs) > 2 && bs[0] == 0xff && bs[1] == 0xfe {
|
||||
err = json.Unmarshal(bs[2:], v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func normalizeLegacyConfigJSON(config string) string {
|
||||
if config == "" || json.Valid(util.UnsafeStringToBytes(config)) {
|
||||
return config
|
||||
}
|
||||
|
||||
m := map[any]any{}
|
||||
err := unmarshalHandleDoubleEncode(util.UnsafeStringToBytes(config), &m)
|
||||
if err != nil {
|
||||
return config
|
||||
}
|
||||
if bs, err := json.Marshal(m); err == nil {
|
||||
return string(bs)
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func NormalizeLegacyConfigJSON(ctx context.Context, x base.EngineMigration) error {
|
||||
type RepoUnit struct {
|
||||
ID int64
|
||||
Config string
|
||||
}
|
||||
type LoginSource struct {
|
||||
ID int64
|
||||
Cfg string
|
||||
}
|
||||
return base.EngineTransaction(x, func(sess base.Session) error {
|
||||
if err := base.IterateTableByColumn(ctx, x, sess, "id", nil, func(_ context.Context, unit *RepoUnit) error {
|
||||
config := normalizeLegacyConfigJSON(unit.Config)
|
||||
if config == unit.Config {
|
||||
return nil
|
||||
}
|
||||
_, err := sess.ID(unit.ID).Cols("config").Update(&RepoUnit{Config: config})
|
||||
return err
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return base.IterateTableByColumn(ctx, x, sess, "id", nil, func(_ context.Context, source *LoginSource) error {
|
||||
config := normalizeLegacyConfigJSON(source.Cfg)
|
||||
if config == source.Cfg {
|
||||
return nil
|
||||
}
|
||||
_, err := sess.ID(source.ID).Cols("cfg").Update(&LoginSource{Cfg: config})
|
||||
return err
|
||||
})
|
||||
})
|
||||
}
|
||||
95
modelmigration/v29/v365_test.go
Normal file
95
modelmigration/v29/v365_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v29
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modelmigration/migrationtest"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNormalizeLegacyConfigJSONValue(t *testing.T) {
|
||||
const (
|
||||
config = `{"Host":"smtp.example.com"}`
|
||||
packed = "≻潈瑳㨢猢瑭硥浡汰潣≭⁽"
|
||||
)
|
||||
for _, tc := range []struct {
|
||||
name, input, want string
|
||||
}{
|
||||
{"empty", "", ""},
|
||||
{"valid", config + " ", config + " "},
|
||||
{"unicode and large number", ` {"name":"例😀","id":9007199254740993} `, ` {"name":"例😀","id":9007199254740993} `},
|
||||
{"double encoded", packed, config},
|
||||
{"double encoded with BOM", "\ufeff" + packed, config},
|
||||
{"BOM", "\xff\xfe" + config, config},
|
||||
{"BOM with non-BMP rune", "\xff\xfe" + `{"name":"😀"}`, `{"name":"😀"}`},
|
||||
{"corrupt", "invalid", "invalid"},
|
||||
{"corrupt non-BMP rune", "invalid😀", "invalid😀"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := normalizeLegacyConfigJSON(tc.input)
|
||||
assert.Equal(t, tc.want, got)
|
||||
assert.Equal(t, got, normalizeLegacyConfigJSON(got))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeLegacyConfigJSON(t *testing.T) {
|
||||
type RepoUnit struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64
|
||||
Config string `xorm:"TEXT"`
|
||||
}
|
||||
type LoginSource struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Name string
|
||||
Cfg string `xorm:"TEXT"`
|
||||
}
|
||||
x, cleanup := migrationtest.PrepareTestEnv(t, 0, new(RepoUnit), new(LoginSource))
|
||||
defer cleanup()
|
||||
if x == nil || t.Failed() {
|
||||
return
|
||||
}
|
||||
defer test.MockVariableValue(&setting.Database.IterateBufferSize, 2)()
|
||||
|
||||
inputs := []string{
|
||||
` {"id":9007199254740993,"name":"例😀"} `,
|
||||
"≻潈瑳㨢猢瑭硥浡汰潣≭⁽",
|
||||
"\ufeff≻潈瑳㨢猢瑭硥浡汰潣≭⁽",
|
||||
"",
|
||||
"invalid",
|
||||
}
|
||||
want := []string{inputs[0], `{"Host":"smtp.example.com"}`, `{"Host":"smtp.example.com"}`, "", "invalid", ""}
|
||||
for _, input := range inputs {
|
||||
_, err := x.Insert(&RepoUnit{RepoID: 42, Config: input})
|
||||
require.NoError(t, err)
|
||||
_, err = x.Insert(&LoginSource{Name: "test", Cfg: input})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
_, err := x.Exec("INSERT INTO `repo_unit` (`repo_id`, `config`) VALUES (?, NULL)", 42)
|
||||
require.NoError(t, err)
|
||||
_, err = x.Exec("INSERT INTO `login_source` (`name`, `cfg`) VALUES (?, NULL)", "test")
|
||||
require.NoError(t, err)
|
||||
|
||||
for range 2 {
|
||||
require.NoError(t, NormalizeLegacyConfigJSON(t.Context(), x))
|
||||
var units []RepoUnit
|
||||
require.NoError(t, x.Asc("id").Find(&units))
|
||||
require.Len(t, units, len(want))
|
||||
var sources []LoginSource
|
||||
require.NoError(t, x.Asc("id").Find(&sources))
|
||||
require.Len(t, sources, len(want))
|
||||
for i, config := range want {
|
||||
assert.Equal(t, config, units[i].Config)
|
||||
assert.EqualValues(t, 42, units[i].RepoID)
|
||||
assert.Equal(t, config, sources[i].Cfg)
|
||||
assert.Equal(t, "test", sources[i].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
14
modelmigration/v29/v366.go
Normal file
14
modelmigration/v29/v366.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v29
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.dev/modelmigration/base"
|
||||
)
|
||||
|
||||
func DropUserOpenIDTable(_ctx context.Context, x base.EngineMigration) error {
|
||||
return x.DropTables("user_open_id")
|
||||
}
|
||||
33
modelmigration/v29/v366_test.go
Normal file
33
modelmigration/v29/v366_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v29
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modelmigration/migrationtest"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDropUserOpenIDTable(t *testing.T) {
|
||||
type UserOpenID struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
}
|
||||
|
||||
x, cleanup := migrationtest.PrepareTestEnv(t, 0, new(UserOpenID))
|
||||
defer cleanup()
|
||||
if x == nil || t.Failed() {
|
||||
return
|
||||
}
|
||||
|
||||
exists, err := x.IsTableExist("user_open_id")
|
||||
require.NoError(t, err)
|
||||
require.True(t, exists)
|
||||
require.NoError(t, DropUserOpenIDTable(t.Context(), x))
|
||||
exists, err = x.IsTableExist("user_open_id")
|
||||
require.NoError(t, err)
|
||||
require.False(t, exists)
|
||||
require.NoError(t, DropUserOpenIDTable(t.Context(), x))
|
||||
}
|
||||
23
modelmigration/v29/v367.go
Normal file
23
modelmigration/v29/v367.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v29
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.dev/modelmigration/base"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddLatestReleaseIDToRepository(_ctx context.Context, engine base.EngineMigration) error {
|
||||
type Repository struct {
|
||||
LatestReleaseID int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
_, err := engine.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreDropIndices: true,
|
||||
IgnoreConstrains: true,
|
||||
}, new(Repository))
|
||||
return err
|
||||
}
|
||||
50
modelmigration/v29/v368.go
Normal file
50
modelmigration/v29/v368.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v29
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modelmigration/base"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddUniqueTeamNameIndex(_ctx context.Context, x base.EngineMigration) error {
|
||||
type Team struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OrgID int64 `xorm:"INDEX UNIQUE(s)"`
|
||||
LowerName string `xorm:"UNIQUE(s)"`
|
||||
Name string
|
||||
}
|
||||
|
||||
var teams []Team
|
||||
if err := x.Where("id NOT IN (SELECT MIN(id) FROM team GROUP BY org_id, lower_name)").Asc("id").Find(&teams); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, team := range teams {
|
||||
for suffix := team.ID; ; suffix++ {
|
||||
ending := fmt.Sprintf("-%d", suffix)
|
||||
name := util.TruncateStringBytes(team.Name, 255-len(ending)) + ending
|
||||
exists, err := x.Where("org_id = ? AND lower_name = ?", team.OrgID, strings.ToLower(name)).Exist(new(Team))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
if _, err := x.ID(team.ID).Cols("name", "lower_name").Update(&Team{Name: name, LowerName: strings.ToLower(name)}); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Warn("renamed duplicate team id=%d in org id=%d from %q to %q", team.ID, team.OrgID, team.Name, name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(Team))
|
||||
return err
|
||||
}
|
||||
52
modelmigration/v29/v368_test.go
Normal file
52
modelmigration/v29/v368_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v29
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modelmigration/migrationtest"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAddUniqueTeamNameIndex(t *testing.T) {
|
||||
type Team struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OrgID int64 `xorm:"INDEX"`
|
||||
LowerName, Name string
|
||||
}
|
||||
|
||||
x, cleanup := migrationtest.PrepareTestEnv(t, 0, new(Team))
|
||||
defer cleanup()
|
||||
if x == nil || t.Failed() {
|
||||
return
|
||||
}
|
||||
|
||||
_, err := x.Insert(
|
||||
&Team{OrgID: 1, Name: "Team", LowerName: "team"},
|
||||
&Team{OrgID: 1, Name: "Team", LowerName: "team"},
|
||||
&Team{OrgID: 1, Name: "Team-2", LowerName: "team-2"},
|
||||
&Team{OrgID: 2, Name: "Team", LowerName: "team"},
|
||||
&Team{OrgID: 1, Name: strings.Repeat("A", 255), LowerName: strings.Repeat("a", 255)},
|
||||
&Team{OrgID: 1, Name: strings.Repeat("A", 255), LowerName: strings.Repeat("a", 255)},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
for range 2 {
|
||||
require.NoError(t, AddUniqueTeamNameIndex(t.Context(), x))
|
||||
var teams []Team
|
||||
require.NoError(t, x.Asc("id").Find(&teams))
|
||||
require.Equal(t, []Team{
|
||||
{ID: 1, OrgID: 1, Name: "Team", LowerName: "team"},
|
||||
{ID: 2, OrgID: 1, Name: "Team-3", LowerName: "team-3"},
|
||||
{ID: 3, OrgID: 1, Name: "Team-2", LowerName: "team-2"},
|
||||
{ID: 4, OrgID: 2, Name: "Team", LowerName: "team"},
|
||||
{ID: 5, OrgID: 1, Name: strings.Repeat("A", 255), LowerName: strings.Repeat("a", 255)},
|
||||
{ID: 6, OrgID: 1, Name: strings.Repeat("A", 253) + "-6", LowerName: strings.Repeat("a", 253) + "-6"},
|
||||
}, teams)
|
||||
}
|
||||
_, err = x.Insert(&Team{OrgID: 1, Name: "TEAM", LowerName: "team"})
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -6,10 +6,10 @@ package actions
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/perm"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"xorm.io/xorm/convert"
|
||||
@@ -30,7 +30,7 @@ type OwnerActionsConfig struct {
|
||||
var _ convert.ConversionFrom = (*OwnerActionsConfig)(nil)
|
||||
|
||||
func (cfg *OwnerActionsConfig) FromDB(bytes []byte) error {
|
||||
_ = json.Unmarshal(bytes, cfg)
|
||||
db.UnmarshalFromDB(bytes, cfg)
|
||||
cfg.TokenPermissionMode, _ = util.EnumValue(cfg.TokenPermissionMode)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -65,26 +65,22 @@ func (opts FindScopedWorkflowSourceOpts) ToConds() builder.Cond {
|
||||
if opts.SourceRepoID != 0 {
|
||||
cond = cond.And(builder.Eq{"source_repo_id": opts.SourceRepoID})
|
||||
}
|
||||
return cond
|
||||
return cond.And(scopedWorkflowSourceOwnedCond())
|
||||
}
|
||||
|
||||
func scopedWorkflowSourceOwnedCond() builder.Cond {
|
||||
return builder.Eq{"owner_id": 0}.Or(builder.Expr("EXISTS (SELECT 1 FROM repository WHERE repository.id = action_scoped_workflow_source.source_repo_id AND repository.owner_id = action_scoped_workflow_source.owner_id)"))
|
||||
}
|
||||
|
||||
// GetEffectiveScopedWorkflowSources returns the scoped-workflow sources effective for a repo owned by repoOwnerID:
|
||||
// the owner's own sources plus instance-level (owner_id=0) sources.
|
||||
func GetEffectiveScopedWorkflowSources(ctx context.Context, repoOwnerID int64) ([]*ActionScopedWorkflowSource, error) {
|
||||
owners := []int64{0}
|
||||
if repoOwnerID != 0 {
|
||||
owners = append(owners, repoOwnerID)
|
||||
}
|
||||
return db.Find[ActionScopedWorkflowSource](ctx, FindScopedWorkflowSourceOpts{OwnerIDs: owners})
|
||||
return db.Find[ActionScopedWorkflowSource](ctx, FindScopedWorkflowSourceOpts{OwnerIDs: []int64{0, repoOwnerID}})
|
||||
}
|
||||
|
||||
// IsScopedWorkflowSourceEffective reports whether sourceRepoID is a scoped-workflow source effective for a repo owned by repoOwnerID.
|
||||
func IsScopedWorkflowSourceEffective(ctx context.Context, repoOwnerID, sourceRepoID int64) (bool, error) {
|
||||
owners := []int64{0}
|
||||
if repoOwnerID != 0 {
|
||||
owners = append(owners, repoOwnerID)
|
||||
}
|
||||
return db.Exist[ActionScopedWorkflowSource](ctx, FindScopedWorkflowSourceOpts{OwnerIDs: owners, SourceRepoID: sourceRepoID}.ToConds())
|
||||
return db.Exist[ActionScopedWorkflowSource](ctx, FindScopedWorkflowSourceOpts{OwnerIDs: []int64{0, repoOwnerID}, SourceRepoID: sourceRepoID}.ToConds())
|
||||
}
|
||||
|
||||
// IsWorkflowRequiredInSources reports whether workflowID from sourceRepoID is required by any of the given sources.
|
||||
@@ -138,12 +134,10 @@ func GetScopedWorkflowSourcesByOwner(ctx context.Context, ownerID int64) ([]*Act
|
||||
|
||||
// GetScopedWorkflowSource returns the (owner, repo) source registration or a NotExist error.
|
||||
func GetScopedWorkflowSource(ctx context.Context, ownerID, repoID int64) (*ActionScopedWorkflowSource, error) {
|
||||
src := &ActionScopedWorkflowSource{}
|
||||
has, err := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID).Get(src)
|
||||
src, has, err := db.Get[ActionScopedWorkflowSource](ctx, builder.Eq{"owner_id": ownerID, "source_repo_id": repoID}.And(scopedWorkflowSourceOwnedCond()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
} else if !has {
|
||||
return nil, util.NewNotExistErrorf("scoped workflow source (owner %d, repo %d) does not exist", ownerID, repoID)
|
||||
}
|
||||
return src, nil
|
||||
@@ -181,3 +175,9 @@ func RemoveScopedWorkflowSource(ctx context.Context, ownerID, repoID int64) erro
|
||||
_, err := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID).Delete(new(ActionScopedWorkflowSource))
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveOwnerScopedWorkflowSources removes every owner-level registration of repoID, as only its current owner may hold one.
|
||||
func RemoveOwnerScopedWorkflowSources(ctx context.Context, repoID int64) error {
|
||||
_, err := db.GetEngine(ctx).Where("owner_id <> 0 AND source_repo_id = ?", repoID).Delete(new(ActionScopedWorkflowSource))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -48,26 +48,28 @@ func TestGetEffectiveScopedWorkflowSources(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
ctx := t.Context()
|
||||
|
||||
rows := []*ActionScopedWorkflowSource{
|
||||
{OwnerID: 2, SourceRepoID: 100, WorkflowConfigs: nil}, // org 2 registers repo 100 (optional)
|
||||
{OwnerID: 0, SourceRepoID: 100, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"a.yml": {Required: true, Patterns: []string{"p"}}}}, // instance also registers repo 100 (required)
|
||||
{OwnerID: 0, SourceRepoID: 200, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"b.yml": {Required: true, Patterns: []string{"p"}}}}, // instance source 200
|
||||
{OwnerID: 3, SourceRepoID: 300, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"c.yml": {Required: true, Patterns: []string{"p"}}}}, // a different owner's source
|
||||
}
|
||||
for _, r := range rows {
|
||||
require.NoError(t, db.Insert(ctx, r))
|
||||
for _, source := range []*ActionScopedWorkflowSource{
|
||||
{OwnerID: 2, SourceRepoID: 1},
|
||||
{OwnerID: 0, SourceRepoID: 1, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"a.yml": {Required: true}}},
|
||||
{OwnerID: 0, SourceRepoID: 200, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"b.yml": {Required: true}}},
|
||||
{OwnerID: 3, SourceRepoID: 3, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"c.yml": {Required: true}}},
|
||||
{OwnerID: 2, SourceRepoID: 3, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"c.yml": {Required: true}}},
|
||||
} {
|
||||
require.NoError(t, db.Insert(ctx, source))
|
||||
}
|
||||
|
||||
// owner 2 sees its own sources plus instance-level ones, but not owner 3's.
|
||||
owner2, err := GetEffectiveScopedWorkflowSources(ctx, 2)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, owner2, 3)
|
||||
_, err = GetScopedWorkflowSource(ctx, 2, 3)
|
||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||
|
||||
required, err := IsScopedWorkflowRequired(ctx, 2, 100, "a.yml")
|
||||
required, err := IsScopedWorkflowRequired(ctx, 2, 1, "a.yml")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, required, "instance marks a.yml required → required for owner 2 even though org left it optional")
|
||||
|
||||
required, err = IsScopedWorkflowRequired(ctx, 2, 100, "x.yml")
|
||||
required, err = IsScopedWorkflowRequired(ctx, 2, 1, "x.yml")
|
||||
require.NoError(t, err)
|
||||
assert.False(t, required)
|
||||
|
||||
@@ -76,12 +78,12 @@ func TestGetEffectiveScopedWorkflowSources(t *testing.T) {
|
||||
assert.True(t, required)
|
||||
|
||||
// owner 3's source must not be effective for owner 2.
|
||||
required, err = IsScopedWorkflowRequired(ctx, 2, 300, "c.yml")
|
||||
required, err = IsScopedWorkflowRequired(ctx, 2, 3, "c.yml")
|
||||
require.NoError(t, err)
|
||||
assert.False(t, required)
|
||||
|
||||
// IsScopedWorkflowSourceEffective: owner-level and instance-level sources are effective; another owner's is not.
|
||||
effective, err := IsScopedWorkflowSourceEffective(ctx, 2, 100)
|
||||
effective, err := IsScopedWorkflowSourceEffective(ctx, 2, 1)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, effective, "owner 2's own source")
|
||||
|
||||
@@ -89,7 +91,7 @@ func TestGetEffectiveScopedWorkflowSources(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.True(t, effective, "instance-level source is effective for any owner")
|
||||
|
||||
effective, err = IsScopedWorkflowSourceEffective(ctx, 2, 300)
|
||||
effective, err = IsScopedWorkflowSourceEffective(ctx, 2, 3)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, effective, "owner 3's source is not effective for owner 2")
|
||||
|
||||
@@ -97,7 +99,7 @@ func TestGetEffectiveScopedWorkflowSources(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.False(t, effective, "unknown source repo")
|
||||
|
||||
effective, err = IsScopedWorkflowSourceEffective(ctx, 3, 300)
|
||||
effective, err = IsScopedWorkflowSourceEffective(ctx, 3, 3)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, effective, "owner 3's own source is effective for owner 3")
|
||||
}
|
||||
@@ -107,8 +109,8 @@ func TestScopedWorkflowSourceCRUD(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
// add is idempotent
|
||||
require.NoError(t, AddScopedWorkflowSource(ctx, 5, 10))
|
||||
require.NoError(t, AddScopedWorkflowSource(ctx, 5, 10))
|
||||
require.NoError(t, AddScopedWorkflowSource(ctx, 5, 4))
|
||||
require.NoError(t, AddScopedWorkflowSource(ctx, 5, 4))
|
||||
sources, err := GetScopedWorkflowSourcesByOwner(ctx, 5)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, sources, 1)
|
||||
@@ -118,20 +120,20 @@ func TestScopedWorkflowSourceCRUD(t *testing.T) {
|
||||
"a.yml": {Required: true, Patterns: []string{"src: a.yml / *"}},
|
||||
"b.yml": {Required: false, Patterns: []string{"src: b.yml / build (push)"}},
|
||||
}
|
||||
require.NoError(t, SetScopedWorkflowSourceConfigs(ctx, 5, 10, configs))
|
||||
src, err := GetScopedWorkflowSource(ctx, 5, 10)
|
||||
require.NoError(t, SetScopedWorkflowSourceConfigs(ctx, 5, 4, configs))
|
||||
src, err := GetScopedWorkflowSource(ctx, 5, 4)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, configs, src.WorkflowConfigs)
|
||||
|
||||
// clearing the configs works
|
||||
require.NoError(t, SetScopedWorkflowSourceConfigs(ctx, 5, 10, nil))
|
||||
src, err = GetScopedWorkflowSource(ctx, 5, 10)
|
||||
require.NoError(t, SetScopedWorkflowSourceConfigs(ctx, 5, 4, nil))
|
||||
src, err = GetScopedWorkflowSource(ctx, 5, 4)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, src.WorkflowConfigs)
|
||||
|
||||
// remove
|
||||
require.NoError(t, RemoveScopedWorkflowSource(ctx, 5, 10))
|
||||
_, err = GetScopedWorkflowSource(ctx, 5, 10)
|
||||
require.NoError(t, RemoveScopedWorkflowSource(ctx, 5, 4))
|
||||
_, err = GetScopedWorkflowSource(ctx, 5, 4)
|
||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||
sources, err = GetScopedWorkflowSourcesByOwner(ctx, 5)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
@@ -162,12 +161,7 @@ func (actions ActionList) LoadIssues(ctx context.Context) error {
|
||||
issueNum := 0
|
||||
for _, action := range actions {
|
||||
if action.IsIssueEvent() {
|
||||
infos := action.GetIssueInfos()
|
||||
if len(infos) == 0 {
|
||||
continue
|
||||
}
|
||||
index, _ := strconv.ParseInt(infos[0], 10, 64)
|
||||
if index > 0 {
|
||||
if index := action.getIssueIndex(); index > 0 {
|
||||
conditions = conditions.Or(builder.Eq{
|
||||
"repo_id": action.RepoID,
|
||||
"`index`": index,
|
||||
|
||||
@@ -180,15 +180,14 @@ func updateIssueNotification(ctx context.Context, userID, issueID, commentID, up
|
||||
}
|
||||
|
||||
// NOTICE: Only update comment id when the before notification on this issue is read, otherwise you may miss some old comments.
|
||||
// But we need update update_by so that the notification will be reorder
|
||||
var cols []string
|
||||
// Always update so that updated_unix moves the notification to the top of the list
|
||||
// Actually, it's unclear what UpdatedBy is used for, or maybe not used at all, or maybe it should just be dropped.
|
||||
notification.UpdatedBy = updatedByID
|
||||
cols := []string{"updated_by"}
|
||||
if notification.Status == NotificationStatusRead {
|
||||
notification.Status = NotificationStatusUnread
|
||||
notification.CommentID = commentID
|
||||
cols = []string{"status", "update_by", "comment_id"}
|
||||
} else {
|
||||
notification.UpdatedBy = updatedByID
|
||||
cols = []string{"update_by"}
|
||||
cols = append(cols, "status", "comment_id")
|
||||
}
|
||||
|
||||
_, err = db.GetEngine(ctx).ID(notification.ID).Cols(cols...).Update(notification)
|
||||
|
||||
@@ -71,16 +71,6 @@ func (opts FindNotificationOptions) ToOrders() string {
|
||||
// receiverID > 0 just send to receiver, else send to all watcher
|
||||
// Returns the set of user IDs whose notification rows were created or updated.
|
||||
func CreateOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) ([]int64, error) {
|
||||
var notifiedIDs []int64
|
||||
err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
var innerErr error
|
||||
notifiedIDs, innerErr = createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID)
|
||||
return innerErr
|
||||
})
|
||||
return notifiedIDs, err
|
||||
}
|
||||
|
||||
func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) ([]int64, error) {
|
||||
// init
|
||||
var toNotify container.Set[int64]
|
||||
notifications, err := db.Find[Notification](ctx, FindNotificationOptions{
|
||||
@@ -181,7 +171,11 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n
|
||||
if notificationExists(notifications, issue.ID, userID) {
|
||||
err = updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID)
|
||||
} else {
|
||||
err = createIssueNotification(ctx, userID, issue, commentID, notificationAuthorID)
|
||||
if err = createIssueNotification(ctx, userID, issue, commentID, notificationAuthorID); err != nil {
|
||||
if errUpdate := updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID); errUpdate == nil {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -32,6 +32,18 @@ func TestCreateOrUpdateIssueNotifications(t *testing.T) {
|
||||
|
||||
notf = unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{UserID: 4, IssueID: issue.ID})
|
||||
assert.Equal(t, activities_model.NotificationStatusUnread, notf.Status)
|
||||
|
||||
// unread notification should get latest updated_unix when re-notified
|
||||
_, err = db.GetEngine(t.Context()).ID(notf.ID).NoAutoTime().Cols("updated_unix").Update(&activities_model.Notification{UpdatedUnix: 123})
|
||||
assert.NoError(t, err)
|
||||
notf = unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: notf.ID})
|
||||
assert.EqualValues(t, 123, notf.UpdatedUnix)
|
||||
|
||||
_, err = activities_model.CreateOrUpdateIssueNotifications(t.Context(), issue.ID, 0, 3, 0)
|
||||
assert.NoError(t, err)
|
||||
notf = unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: notf.ID})
|
||||
assert.NotEqualValues(t, 123, notf.UpdatedUnix)
|
||||
assert.EqualValues(t, 3, notf.UpdatedBy)
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateIssueNotificationsForAssigneeAndReviewer(t *testing.T) {
|
||||
|
||||
@@ -6,6 +6,7 @@ package asymkey
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -107,7 +108,7 @@ func writeAuthorizedStringForKey(key *PublicKey, w io.Writer) (keyValid bool, er
|
||||
}
|
||||
|
||||
// appendAuthorizedKeysToFile appends new SSH keys' content to authorized_keys file.
|
||||
func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
|
||||
func appendAuthorizedKeysToFile(keys ...*PublicKey) (err error) {
|
||||
// Don't need to rewrite this file if builtin SSH server is enabled.
|
||||
if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
|
||||
return nil
|
||||
@@ -133,7 +134,7 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
defer func() { err = errors.Join(err, f.Close()) }()
|
||||
|
||||
// Note: chmod command does not support in Windows.
|
||||
if !consts.IsWindows {
|
||||
|
||||
@@ -88,8 +88,6 @@ var (
|
||||
UserWebAuthRemove = define("user:webauth:remove", "Removed WebAuthn key {credential} from user {scope}.")
|
||||
UserExternalLoginAdd = define("user:externallogin:add", "Added external login {external_id} for user {scope} using provider {provider}.")
|
||||
UserExternalLoginRemove = define("user:externallogin:remove", "Removed external login from authentication source {auth_source_id} for user {scope}.")
|
||||
UserOpenIDAdd = define("user:openid:add", "Associated OpenID {openid} to user {scope}.")
|
||||
UserOpenIDRemove = define("user:openid:remove", "Removed OpenID {openid} from user {scope}.")
|
||||
UserAccessTokenAdd = define("user:accesstoken:add", "Added access token {token} for user {scope} with scope {token_scope}.")
|
||||
UserAccessTokenRegenerate = define("user:accesstoken:regenerate", "Regenerated access token {token} of user {scope}.")
|
||||
UserAccessTokenRemove = define("user:accesstoken:remove", "Removed access token {token} from user {scope}.")
|
||||
|
||||
@@ -251,18 +251,8 @@ func (s AccessTokenScope) parse() (accessTokenScopeBitmap, error) {
|
||||
// The following is the more performant equivalent of 'for _, v := range strings.Split(remainingScope, ",")' as this is hot code
|
||||
remainingScopes := string(s)
|
||||
for len(remainingScopes) > 0 {
|
||||
i := strings.IndexByte(remainingScopes, ',')
|
||||
var v string
|
||||
if i < 0 {
|
||||
v = remainingScopes
|
||||
remainingScopes = ""
|
||||
} else if i+1 >= len(remainingScopes) {
|
||||
v = remainingScopes[:i]
|
||||
remainingScopes = ""
|
||||
} else {
|
||||
v = remainingScopes[:i]
|
||||
remainingScopes = remainingScopes[i+1:]
|
||||
}
|
||||
v, remainingScopes, _ = strings.Cut(remainingScopes, ",")
|
||||
singleScope := AccessTokenScope(v)
|
||||
if singleScope == "" {
|
||||
continue
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"reflect"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
@@ -60,14 +61,40 @@ var Names = map[Type]string{
|
||||
type Config interface {
|
||||
convert.Conversion
|
||||
SetAuthSource(*Source)
|
||||
GetAuthSource() *Source
|
||||
}
|
||||
|
||||
type ConfigBase struct {
|
||||
AuthSource *Source
|
||||
AuthSource *Source `json:"-"`
|
||||
}
|
||||
|
||||
func (p *ConfigBase) SetAuthSource(s *Source) {
|
||||
p.AuthSource = s
|
||||
func (c *ConfigBase) SetAuthSource(source *Source) {
|
||||
c.AuthSource = source
|
||||
}
|
||||
|
||||
func (c *ConfigBase) GetAuthSource() *Source {
|
||||
return c.AuthSource
|
||||
}
|
||||
|
||||
func UnmarshalAuthSourceConfig(bs []byte, v Config) {
|
||||
if db.UnmarshalFromDB(bs, &v) {
|
||||
return
|
||||
}
|
||||
// This is only a workaround because we don't know whether this unmarshal happens on sign-in page or admin panel.
|
||||
// * Admin should have the chance to edit the auth source in admin panel, so FromDB should not return an error
|
||||
// * The sign-in page should not use the auth source in the next login attempt, but we don't have a clear way to disable it immediately (acceptable)
|
||||
// Actually, corrupted database value is a really rare case, just because AI thinks it should be handled.
|
||||
authSourceID := v.GetAuthSource().ID
|
||||
log.Error("Unable to unmarshal login source id=%d, will mark it as inactive to avoid unexpected sign-in issues", authSourceID)
|
||||
go graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) {
|
||||
// A small time window to avoid race condition with admin's editing because there is no other way.
|
||||
// The UpdateUnix field is not filled when this function is called.
|
||||
_, err := db.GetEngine(ctx).ID(authSourceID).And("is_active = ? AND updated_unix < ?", true, timeutil.TimeStampNow()-5).
|
||||
Cols("is_active").Update(&Source{IsActive: false})
|
||||
if err != nil {
|
||||
log.Error("Unable to set login source id=%d to inactive: %v", authSourceID, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// SkipVerifiable configurations provide a IsSkipVerify to check if SkipVerify is set
|
||||
|
||||
@@ -6,10 +6,15 @@ package auth_test
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -24,7 +29,8 @@ type TestSource struct {
|
||||
|
||||
// FromDB fills up a LDAPConfig from serialized format.
|
||||
func (source *TestSource) FromDB(bs []byte) error {
|
||||
return json.Unmarshal(bs, &source)
|
||||
auth_model.UnmarshalAuthSourceConfig(bs, source)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDB exports a LDAPConfig to a serialized format.
|
||||
@@ -53,3 +59,37 @@ func TestDumpAuthSource(t *testing.T) {
|
||||
// INSERT INTO `login_source` (`id`, `type`, `name`, `is_active`, `is_sync_enabled`, `two_factor_policy`, `cfg`, `created_unix`, `updated_unix`) VALUES (1,6,'TestSource',0,0,'','{"TestField":"TestValue"}',1774179784,1774179784);
|
||||
assert.Contains(t, sb.String(), `'{"TestField":"TestValue"}'`)
|
||||
}
|
||||
|
||||
func TestUnmarshalAuthSourceConfig(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
defer test.MockVariableValue(&setting.IsProd, true)()
|
||||
defer test.MockVariableValue(&setting.IsInTesting, false)()
|
||||
|
||||
cfg := &TestSource{}
|
||||
source := &auth_model.Source{Type: auth_model.OAuth2, Name: t.Name(), IsActive: true, Cfg: cfg}
|
||||
err := db.Insert(t.Context(), source)
|
||||
require.NoError(t, err)
|
||||
cfg.SetAuthSource(source)
|
||||
|
||||
t.Run("ValidConfig", func(t *testing.T) {
|
||||
for _, data := range []string{`{"TestField":"TestValue"}`, "", "null"} {
|
||||
auth_model.UnmarshalAuthSourceConfig([]byte(data), cfg)
|
||||
assert.Equal(t, "TestValue", cfg.TestField)
|
||||
assert.Same(t, source, cfg.GetAuthSource())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidConfigDeactivatesSource", func(t *testing.T) {
|
||||
_, err = db.GetEngine(t.Context()).ID(source.ID).NoAutoTime().Cols("updated_unix").Update(&auth_model.Source{UpdatedUnix: timeutil.TimeStampNow() - 60})
|
||||
require.NoError(t, err)
|
||||
has, _ := db.GetEngine(t.Context()).ID(source.ID).And("is_active=?", true).Get(&auth_model.Source{})
|
||||
assert.True(t, has)
|
||||
|
||||
auth_model.UnmarshalAuthSourceConfig([]byte(`{"TestField":false}`), cfg)
|
||||
assert.Same(t, source, cfg.GetAuthSource())
|
||||
require.Eventually(t, func() bool {
|
||||
has, _ := db.GetEngine(t.Context()).ID(source.ID).And("is_active=?", false).Get(&auth_model.Source{})
|
||||
return has
|
||||
}, time.Second, time.Millisecond)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,8 +12,11 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/cache"
|
||||
"gitea.dev/modules/globallock"
|
||||
"gitea.dev/modules/secret"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/timeutil"
|
||||
@@ -48,6 +51,11 @@ func (err ErrTwoFactorNotEnrolled) Unwrap() error {
|
||||
return util.ErrNotExist
|
||||
}
|
||||
|
||||
const (
|
||||
totpMaxAttempts = 5
|
||||
totpAttemptsTTL = 15 * 60 // seconds, renewed by every failed attempt
|
||||
)
|
||||
|
||||
// TwoFactor represents a two-factor authentication token.
|
||||
type TwoFactor struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
@@ -120,26 +128,59 @@ func (t *TwoFactor) validateTOTP(passcode string) (bool, error) {
|
||||
return totp.Validate(passcode, secretStr), nil
|
||||
}
|
||||
|
||||
type TwoFactorResult int
|
||||
|
||||
const (
|
||||
TwoFactorResultValid TwoFactorResult = iota + 1
|
||||
TwoFactorResultInvalid
|
||||
TwoFactorResultTooManyAttempts
|
||||
)
|
||||
|
||||
// ValidateAndConsumeTOTP validates the passcode and atomically records it as used so that the
|
||||
// same passcode cannot be redeemed more than once (RFC 6238 §5.2). It returns false for an
|
||||
// invalid passcode as well as for a replay, including the case where a concurrent request with
|
||||
// the same passcode won the race first. All TOTP login surfaces must go through this helper.
|
||||
func (t *TwoFactor) ValidateAndConsumeTOTP(ctx context.Context, passcode string) (bool, error) {
|
||||
ok, err := t.validateTOTP(passcode)
|
||||
if err != nil || !ok {
|
||||
return false, err
|
||||
// same passcode cannot be redeemed more than once (RFC 6238 §5.2). Invalid passcodes and replays
|
||||
// return TwoFactorResultInvalid; exhausted attempts return TwoFactorResultTooManyAttempts.
|
||||
// All TOTP login surfaces must go through this helper.
|
||||
func (t *TwoFactor) ValidateAndConsumeTOTP(ctx context.Context, passcode string) (result TwoFactorResult, err error) {
|
||||
if passcode == "" {
|
||||
// password-only Basic auth clients must not use up the attempts
|
||||
return TwoFactorResultInvalid, nil
|
||||
}
|
||||
// Conditional update: only a row whose stored passcode differs from this one is updated, so a
|
||||
// replay (or a concurrent duplicate) matches zero rows and is rejected. The row lock taken by
|
||||
// the UPDATE serializes racing requests, closing the read-validate-write TOCTOU window.
|
||||
t.LastUsedPasscode = passcode
|
||||
n, err := db.GetEngine(ctx).ID(t.ID).
|
||||
Where(builder.Or(builder.IsNull{"last_used_passcode"}, builder.Neq{"last_used_passcode": passcode})).
|
||||
Cols("last_used_passcode").Update(t)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return n == 1, nil
|
||||
// the lock spans check, validation and counting so that concurrent requests cannot exceed the limit
|
||||
attemptsKey := fmt.Sprintf("twofa_attempts_%d", t.UID)
|
||||
err = globallock.LockAndDo(ctx, attemptsKey, func(ctx context.Context) error {
|
||||
value, _ := cache.GetCache().Get(attemptsKey)
|
||||
attempts, _ := strconv.Atoi(value)
|
||||
if attempts >= totpMaxAttempts {
|
||||
result = TwoFactorResultTooManyAttempts
|
||||
return nil
|
||||
}
|
||||
|
||||
valid, err := t.validateTOTP(passcode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if valid {
|
||||
// Conditional update: only a row whose stored passcode differs from this one is updated, so a
|
||||
// replay (or a concurrent duplicate) matches zero rows and is rejected. The row lock taken by
|
||||
// the UPDATE serializes racing requests, closing the read-validate-write TOCTOU window.
|
||||
t.LastUsedPasscode = passcode
|
||||
n, err := db.GetEngine(ctx).ID(t.ID).
|
||||
Where(builder.Or(builder.IsNull{"last_used_passcode"}, builder.Neq{"last_used_passcode": passcode})).
|
||||
Cols("last_used_passcode").Update(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
valid = n == 1
|
||||
}
|
||||
if !valid {
|
||||
result = TwoFactorResultInvalid
|
||||
return cache.GetCache().Put(attemptsKey, strconv.Itoa(attempts+1), totpAttemptsTTL)
|
||||
}
|
||||
cache.Remove(attemptsKey)
|
||||
result = TwoFactorResultValid
|
||||
return nil
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
// NewTwoFactor creates a new two-factor authentication token.
|
||||
|
||||
@@ -4,11 +4,15 @@
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/cache"
|
||||
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/pquerna/otp/totp"
|
||||
@@ -31,21 +35,42 @@ func TestTwoFactorLifecycle(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// first use of a valid passcode succeeds
|
||||
ok, err := tfa.ValidateAndConsumeTOTP(t.Context(), passcode)
|
||||
result, err := tfa.ValidateAndConsumeTOTP(t.Context(), passcode)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, auth_model.TwoFactorResultValid, result)
|
||||
|
||||
// replaying the same passcode is refused, even when still inside the TOTP validity window
|
||||
reloaded, err := auth_model.GetTwoFactorByUID(t.Context(), tfa.UID)
|
||||
require.NoError(t, err)
|
||||
ok, err = reloaded.ValidateAndConsumeTOTP(t.Context(), passcode)
|
||||
result, err = reloaded.ValidateAndConsumeTOTP(t.Context(), passcode)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, ok)
|
||||
assert.Equal(t, auth_model.TwoFactorResultInvalid, result)
|
||||
|
||||
// an invalid passcode is rejected without consuming anything
|
||||
ok, err = reloaded.ValidateAndConsumeTOTP(t.Context(), "000000")
|
||||
var limited atomic.Int32
|
||||
var wg sync.WaitGroup
|
||||
for range 10 {
|
||||
wg.Go(func() {
|
||||
result, err := reloaded.ValidateAndConsumeTOTP(t.Context(), "0000000")
|
||||
assert.NoError(t, err)
|
||||
if result == auth_model.TwoFactorResultTooManyAttempts {
|
||||
limited.Add(1)
|
||||
} else {
|
||||
assert.Equal(t, auth_model.TwoFactorResultInvalid, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
assert.EqualValues(t, 6, limited.Load())
|
||||
nextPasscode, err := totp.GenerateCode(key.Secret(), time.Now().Add(30*time.Second))
|
||||
require.NoError(t, err)
|
||||
assert.False(t, ok)
|
||||
result, err = reloaded.ValidateAndConsumeTOTP(t.Context(), nextPasscode)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, auth_model.TwoFactorResultTooManyAttempts, result)
|
||||
|
||||
cache.Remove(fmt.Sprintf("twofa_attempts_%d", tfa.UID))
|
||||
result, err = reloaded.ValidateAndConsumeTOTP(t.Context(), nextPasscode)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, auth_model.TwoFactorResultValid, result)
|
||||
})
|
||||
|
||||
t.Run("DisableTwoFactor", func(t *testing.T) {
|
||||
|
||||
@@ -6,6 +6,7 @@ package db
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"xorm.io/xorm"
|
||||
@@ -84,3 +85,21 @@ func CellToInt[T ~int | int64](cell xorm.Cell, def T) (ret T, has bool, err erro
|
||||
}
|
||||
return T(val), true, err
|
||||
}
|
||||
|
||||
func UnmarshalFromDB(data []byte, v any) bool {
|
||||
if len(data) == 0 {
|
||||
return true
|
||||
}
|
||||
err := json.Unmarshal(data, v)
|
||||
if err != nil {
|
||||
// Even if database value is corrupted, we should not let it cause unnecessary 500 error or block user's operation
|
||||
// Corrupted database value is a really rare case, it should never happen unless site admin manually modified the database.
|
||||
// Although some database values will be printed into logs, it isn't a real problem
|
||||
// because such case should never really happen. If it happens, logging the value
|
||||
// is the only way to tell site admin which record is wrong since there is no other context information,
|
||||
// and then they need to re-create the config (with new secrets)
|
||||
setting.PanicInDevOrTesting("UnmarshalFromDB %q failed: %v", string(data), err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
updated_unix: 1546869730
|
||||
|
||||
- id: 2
|
||||
user_id: 3
|
||||
user_id: 14
|
||||
application_id: 1
|
||||
counter: 1
|
||||
scope: "openid"
|
||||
@@ -23,7 +23,7 @@
|
||||
updated_unix: 1546869730
|
||||
|
||||
- id: 4
|
||||
user_id: 99
|
||||
user_id: 2
|
||||
application_id: 2
|
||||
counter: 1
|
||||
scope: "whatever"
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
-
|
||||
id: 1
|
||||
uid: 1
|
||||
uri: https://user1.domain1.tld/
|
||||
show: false
|
||||
|
||||
-
|
||||
id: 2
|
||||
uid: 1
|
||||
uri: http://user1.domain2.tld/
|
||||
show: true
|
||||
|
||||
-
|
||||
id: 3
|
||||
uid: 2
|
||||
uri: https://domain1.tld/user2/
|
||||
show: true
|
||||
|
||||
# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly
|
||||
@@ -217,11 +217,6 @@ func GetDeletedBranchByID(ctx context.Context, repoID, branchID int64) (*Branch,
|
||||
return &branch, nil
|
||||
}
|
||||
|
||||
func DeleteRepoBranches(ctx context.Context, repoID int64) error {
|
||||
_, err := db.GetEngine(ctx).Where("repo_id=?", repoID).Delete(new(Branch))
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteBranches(ctx context.Context, repoID, doerID int64, branchIDs []int64) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
branches := make([]*Branch, 0, len(branchIDs))
|
||||
|
||||
256
models/issues/code_owner.go
Normal file
256
models/issues/code_owner.go
Normal file
@@ -0,0 +1,256 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package issues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
org_model "gitea.dev/models/organization"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/glob"
|
||||
"gitea.dev/modules/optional"
|
||||
|
||||
"github.com/dlclark/regexp2/v2"
|
||||
)
|
||||
|
||||
// GetCodeOwnersFromContent returns the code owners configuration
|
||||
// Return empty slice if files missing
|
||||
// Return warning messages on parsing errors
|
||||
// We're trying to do the best we can when parsing a file.
|
||||
// Invalid lines are skipped. Non-existent users and teams too.
|
||||
func GetCodeOwnersFromContent(ctx context.Context, data string) (rules []*CodeOwnerRule, warnings []string) {
|
||||
globalReview := optional.None[bool]()
|
||||
sectionReview := optional.None[bool]()
|
||||
section := ""
|
||||
var owners []string
|
||||
|
||||
lineNum := 0
|
||||
for line := range strings.SplitSeq(data, "\n") {
|
||||
lineNum++
|
||||
line, reviewRequest, annotationWarnings := parseCodeOwnerAnnotation(line)
|
||||
for _, warning := range annotationWarnings {
|
||||
warnings = append(warnings, fmt.Sprintf("Line: %d: %s", lineNum, warning))
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
if reviewRequest.Has() {
|
||||
globalReview = reviewRequest
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// To support section headers, there could be an edge case for end users:
|
||||
// `[a-z] @user` now becomes a section header instead of a rule, but it is really rare to happen.
|
||||
// A workaround is to use `/[a-z] @user` to make it a rule.
|
||||
if heading := line; strings.HasPrefix(heading, "[") {
|
||||
if end := strings.IndexByte(heading, ']'); end > 1 && (end == len(heading)-1 || heading[end+1] == ' ' || heading[end+1] == '\t') {
|
||||
section = heading[1:end]
|
||||
owners = TokenizeCodeOwnersLine(heading[end+1:])
|
||||
sectionReview = reviewRequest
|
||||
continue
|
||||
}
|
||||
}
|
||||
tokens := TokenizeCodeOwnersLine(line)
|
||||
if len(tokens) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(tokens) == 1 && len(owners) > 0 {
|
||||
tokens = append(tokens, owners...)
|
||||
}
|
||||
if len(tokens) < 2 {
|
||||
warnings = append(warnings, fmt.Sprintf("Line: %d: incorrect format", lineNum))
|
||||
continue
|
||||
}
|
||||
rule, wr := ParseCodeOwnersLine(ctx, tokens)
|
||||
for _, w := range wr {
|
||||
warnings = append(warnings, fmt.Sprintf("Line: %d: %s", lineNum, w))
|
||||
}
|
||||
if rule == nil {
|
||||
continue
|
||||
}
|
||||
rule.LineNum = lineNum
|
||||
|
||||
rule.Section = section
|
||||
rule.ReviewRequest = reviewRequest
|
||||
if !rule.ReviewRequest.Has() {
|
||||
rule.ReviewRequest = sectionReview
|
||||
}
|
||||
if !rule.ReviewRequest.Has() {
|
||||
rule.ReviewRequest = globalReview
|
||||
}
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
return rules, warnings
|
||||
}
|
||||
|
||||
// codeOwnerMatchTimeout bounds a single pattern match so a crafted pattern
|
||||
// cannot stall via catastrophic backtracking. See also the aggregate budget
|
||||
// enforced by the caller across the whole rules×files match loop.
|
||||
const codeOwnerMatchTimeout = 150 * time.Millisecond
|
||||
|
||||
type CodeOwnerRule struct {
|
||||
LineNum int
|
||||
Section string
|
||||
ReviewRequest optional.Option[bool]
|
||||
|
||||
Rule *regexp2.Regexp // it supports negative lookahead, does better for end users
|
||||
Negative bool
|
||||
|
||||
// TODO: these objects should not be loaded here, but rather lazily when needed.
|
||||
// The parsed rule should only contain the user/team names.
|
||||
Users []*user_model.User
|
||||
Teams []*org_model.Team
|
||||
}
|
||||
|
||||
func ParseCodeOwnersLine(ctx context.Context, tokens []string) (rule *CodeOwnerRule, warnings []string) {
|
||||
rule = &CodeOwnerRule{Negative: strings.HasPrefix(tokens[0], "!")}
|
||||
|
||||
// Strip leading "!" for negative rules, then strip leading "/" since
|
||||
// git returns relative paths (e.g. "docs/foo.md" not "/docs/foo.md")
|
||||
// and the regex is already anchored with ^...$, so the "/" is redundant.
|
||||
pattern := strings.TrimPrefix(tokens[0], "!")
|
||||
pattern = strings.TrimPrefix(pattern, "/")
|
||||
expr := fmt.Sprintf("^%s$", pattern)
|
||||
|
||||
// To make life easier, we support glob expressions if the pattern contains a "*" or "/*" (e.g. "*.md" or "docs/*.md")
|
||||
// However, "src/test\\..*\\.js" is still the only way because "src/test.*.js" can't be used as a glob at the moment.
|
||||
// TODO: In the future, maybe it's better to introduce a new annotation to specify the pattern type, e.g.: "pattern-syntax=glob" or "pattern-syntax=regexp"
|
||||
if strings.HasPrefix(pattern, "*") || strings.Contains(pattern, "/*") {
|
||||
compiled, compileErr := glob.CompileToRegexp(pattern)
|
||||
if compileErr != nil {
|
||||
return nil, []string{fmt.Sprintf("incorrect codeowner glob: %s", compileErr)}
|
||||
}
|
||||
expr = compiled.String()
|
||||
}
|
||||
|
||||
var err error
|
||||
rule.Rule, err = regexp2.Compile(expr, regexp2.None)
|
||||
if err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("incorrect codeowner regexp: %s", err))
|
||||
return nil, warnings
|
||||
}
|
||||
|
||||
// Bound matching time so user-supplied patterns cannot stall PR creation via catastrophic backtracking.
|
||||
rule.Rule.MatchTimeout = codeOwnerMatchTimeout
|
||||
|
||||
for _, user := range tokens[1:] {
|
||||
user = strings.TrimPrefix(user, "@")
|
||||
|
||||
// Only @org/team can contain slashes
|
||||
if strings.Contains(user, "/") {
|
||||
s := strings.Split(user, "/")
|
||||
if len(s) != 2 {
|
||||
warnings = append(warnings, "incorrect codeowner group: "+user)
|
||||
continue
|
||||
}
|
||||
orgName := s[0]
|
||||
teamName := s[1]
|
||||
|
||||
org, err := org_model.GetOrgByName(ctx, orgName)
|
||||
if err != nil {
|
||||
warnings = append(warnings, "incorrect codeowner organization: "+user)
|
||||
continue
|
||||
}
|
||||
teams, err := org.LoadTeams(ctx)
|
||||
if err != nil {
|
||||
warnings = append(warnings, "incorrect codeowner team: "+user)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, team := range teams {
|
||||
if team.Name == teamName {
|
||||
rule.Teams = append(rule.Teams, team)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
u, err := user_model.GetUserByName(ctx, user)
|
||||
if err != nil {
|
||||
warnings = append(warnings, "incorrect codeowner user: "+user)
|
||||
continue
|
||||
}
|
||||
rule.Users = append(rule.Users, u)
|
||||
}
|
||||
}
|
||||
|
||||
if len(rule.Users) == 0 && len(rule.Teams) == 0 {
|
||||
warnings = append(warnings, "no users/groups matched")
|
||||
return nil, warnings
|
||||
}
|
||||
|
||||
return rule, warnings
|
||||
}
|
||||
|
||||
func TokenizeCodeOwnersLine(line string) (tokens []string) {
|
||||
line = strings.TrimSpace(line)
|
||||
line = strings.ReplaceAll(line, "\t", " ")
|
||||
if line == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
escape := false
|
||||
var token strings.Builder
|
||||
for _, char := range line {
|
||||
if escape {
|
||||
token.WriteRune(char)
|
||||
escape = false
|
||||
} else if char == '\\' {
|
||||
escape = true
|
||||
} else if char == '#' {
|
||||
break
|
||||
} else if char == ' ' {
|
||||
if token.Len() > 0 {
|
||||
tokens = append(tokens, token.String())
|
||||
token.Reset()
|
||||
}
|
||||
} else {
|
||||
token.WriteRune(char)
|
||||
}
|
||||
}
|
||||
|
||||
if token.Len() > 0 {
|
||||
tokens = append(tokens, token.String())
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
func parseCodeOwnerAnnotation(line string) (string, optional.Option[bool], []string) {
|
||||
escape := false
|
||||
for i := 0; i < len(line); i++ {
|
||||
if escape {
|
||||
escape = false
|
||||
continue
|
||||
}
|
||||
if line[i] == '\\' {
|
||||
escape = true
|
||||
continue
|
||||
}
|
||||
if line[i] != '#' {
|
||||
continue
|
||||
}
|
||||
review := optional.None[bool]()
|
||||
var warnings []string
|
||||
if strings.HasPrefix(line[i:], "##") {
|
||||
annotation, _, _ := strings.Cut(line[i+2:], "#")
|
||||
for field := range strings.FieldsSeq(annotation) {
|
||||
key, value, ok := strings.Cut(field, "=")
|
||||
if key != "review-request" {
|
||||
warnings = append(warnings, "unknown codeowner annotation: "+key)
|
||||
continue
|
||||
}
|
||||
if !ok || (value != "true" && value != "false") {
|
||||
warnings = append(warnings, "incorrect review-request annotation: "+field)
|
||||
continue
|
||||
}
|
||||
review = optional.Some(value == "true")
|
||||
}
|
||||
}
|
||||
return line[:i], review, warnings
|
||||
}
|
||||
return line, optional.None[bool](), nil
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
project_model "gitea.dev/models/project"
|
||||
@@ -112,10 +113,15 @@ type Issue struct {
|
||||
TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
|
||||
var (
|
||||
issueTasksPat = regexp.MustCompile(`(^\s*[-*]\s\[[\sxX]\]\s.)|(\n\s*[-*]\s\[[\sxX]\]\s.)`)
|
||||
issueTasksDonePat = regexp.MustCompile(`(^\s*[-*]\s\[[xX]\]\s.)|(\n\s*[-*]\s\[[xX]\]\s.)`)
|
||||
)
|
||||
var globalVars = sync.OnceValue(func() (ret struct {
|
||||
issueTasksPat *regexp.Regexp
|
||||
issueTasksDonePat *regexp.Regexp
|
||||
},
|
||||
) {
|
||||
ret.issueTasksPat = regexp.MustCompile(`(^\s*[-*]\s\[[\sxX]\]\s.)|(\n\s*[-*]\s\[[\sxX]\]\s.)`)
|
||||
ret.issueTasksDonePat = regexp.MustCompile(`(^\s*[-*]\s\[[xX]\]\s.)|(\n\s*[-*]\s\[[xX]\]\s.)`)
|
||||
return ret
|
||||
})
|
||||
|
||||
// IssueIndex represents the issue index table
|
||||
type IssueIndex db.ResourceIndex
|
||||
@@ -463,12 +469,12 @@ func (issue *Issue) IsPoster(uid int64) bool {
|
||||
|
||||
// GetTasks returns the amount of tasks in the issues content
|
||||
func (issue *Issue) GetTasks() int {
|
||||
return len(issueTasksPat.FindAllStringIndex(issue.Content, -1))
|
||||
return len(globalVars().issueTasksPat.FindAllStringIndex(issue.Content, -1))
|
||||
}
|
||||
|
||||
// GetTasksDone returns the amount of completed tasks in the issues content
|
||||
func (issue *Issue) GetTasksDone() int {
|
||||
return len(issueTasksDonePat.FindAllStringIndex(issue.Content, -1))
|
||||
return len(globalVars().issueTasksDonePat.FindAllStringIndex(issue.Content, -1))
|
||||
}
|
||||
|
||||
// GetLastEventTimestamp returns the last user visible event timestamp, either the creation of this issue or the close.
|
||||
|
||||
@@ -313,6 +313,7 @@ func clearIssueLabels(ctx context.Context, issue *Issue, doer *user_model.User)
|
||||
return fmt.Errorf("removeLabel: %w", err)
|
||||
}
|
||||
}
|
||||
issue.Labels = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -435,6 +436,7 @@ func ReplaceIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer
|
||||
}
|
||||
}
|
||||
|
||||
issue.isLabelsLoaded = false
|
||||
issue.Labels = nil
|
||||
return issue.LoadLabels(ctx)
|
||||
})
|
||||
|
||||
@@ -244,13 +244,7 @@ func NewLabel(ctx context.Context, l *Label) error {
|
||||
func NewLabels(ctx context.Context, labels ...*Label) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
for _, l := range labels {
|
||||
color, err := label.NormalizeColor(l.Color)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.Color = color
|
||||
|
||||
if err := db.Insert(ctx, l); err != nil {
|
||||
if err := NewLabel(ctx, l); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
"sync"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
git_model "gitea.dev/models/git"
|
||||
@@ -24,11 +24,22 @@ import (
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/dlclark/regexp2/v2"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
var ErrMustCollaborator = util.NewPermissionDeniedErrorf("user must be a collaborator")
|
||||
var Errors = sync.OnceValue(func() (ret struct {
|
||||
MustRepoUnitReader error
|
||||
MustMemberOrCollaborator error
|
||||
MustRepoUnitWriter error
|
||||
MustHeadRepoCodeWriter error
|
||||
},
|
||||
) {
|
||||
ret.MustRepoUnitReader = util.NewPermissionDeniedErrorf("user must have read access")
|
||||
ret.MustMemberOrCollaborator = util.NewPermissionDeniedErrorf("user must be a member or collaborator")
|
||||
ret.MustRepoUnitWriter = util.NewPermissionDeniedErrorf("user must have write access")
|
||||
ret.MustHeadRepoCodeWriter = util.NewPermissionDeniedErrorf("user must have write access to the head repository's code")
|
||||
return ret
|
||||
})
|
||||
|
||||
const reviewedBy = "Reviewed-by: "
|
||||
|
||||
@@ -825,162 +836,6 @@ func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr *
|
||||
return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0
|
||||
}
|
||||
|
||||
// GetCodeOwnersFromContent returns the code owners configuration
|
||||
// Return empty slice if files missing
|
||||
// Return warning messages on parsing errors
|
||||
// We're trying to do the best we can when parsing a file.
|
||||
// Invalid lines are skipped. Non-existent users and teams too.
|
||||
func GetCodeOwnersFromContent(ctx context.Context, data string) ([]*CodeOwnerRule, []string) {
|
||||
if len(data) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rules := make([]*CodeOwnerRule, 0)
|
||||
lines := strings.Split(data, "\n")
|
||||
warnings := make([]string, 0)
|
||||
|
||||
for i, line := range lines {
|
||||
tokens := TokenizeCodeOwnersLine(line)
|
||||
if len(tokens) == 0 {
|
||||
continue
|
||||
} else if len(tokens) < 2 {
|
||||
warnings = append(warnings, fmt.Sprintf("Line: %d: incorrect format", i+1))
|
||||
continue
|
||||
}
|
||||
rule, wr := ParseCodeOwnersLine(ctx, tokens)
|
||||
for _, w := range wr {
|
||||
warnings = append(warnings, fmt.Sprintf("Line: %d: %s", i+1, w))
|
||||
}
|
||||
if rule == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
return rules, warnings
|
||||
}
|
||||
|
||||
// codeOwnerMatchTimeout bounds a single pattern match so a crafted pattern
|
||||
// cannot stall via catastrophic backtracking. See also the aggregate budget
|
||||
// enforced by the caller across the whole rules×files match loop.
|
||||
const codeOwnerMatchTimeout = 150 * time.Millisecond
|
||||
|
||||
type CodeOwnerRule struct {
|
||||
Rule *regexp2.Regexp // it supports negative lookahead, does better for end users
|
||||
Negative bool
|
||||
Users []*user_model.User
|
||||
Teams []*org_model.Team
|
||||
}
|
||||
|
||||
func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule, []string) {
|
||||
var err error
|
||||
rule := &CodeOwnerRule{
|
||||
Users: make([]*user_model.User, 0),
|
||||
Teams: make([]*org_model.Team, 0),
|
||||
Negative: strings.HasPrefix(tokens[0], "!"),
|
||||
}
|
||||
|
||||
warnings := make([]string, 0)
|
||||
|
||||
// Strip leading "!" for negative rules, then strip leading "/" since
|
||||
// git returns relative paths (e.g. "docs/foo.md" not "/docs/foo.md")
|
||||
// and the regex is already anchored with ^...$, so the "/" is redundant.
|
||||
pattern := strings.TrimPrefix(tokens[0], "!")
|
||||
pattern = strings.TrimPrefix(pattern, "/")
|
||||
expr := fmt.Sprintf("^%s$", pattern)
|
||||
rule.Rule, err = regexp2.Compile(expr, regexp2.None)
|
||||
if err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("incorrect codeowner regexp: %s", err))
|
||||
return nil, warnings
|
||||
}
|
||||
// Bound matching time so user-supplied patterns cannot stall PR creation via catastrophic backtracking.
|
||||
rule.Rule.MatchTimeout = codeOwnerMatchTimeout
|
||||
|
||||
for _, user := range tokens[1:] {
|
||||
user = strings.TrimPrefix(user, "@")
|
||||
|
||||
// Only @org/team can contain slashes
|
||||
if strings.Contains(user, "/") {
|
||||
s := strings.Split(user, "/")
|
||||
if len(s) != 2 {
|
||||
warnings = append(warnings, "incorrect codeowner group: "+user)
|
||||
continue
|
||||
}
|
||||
orgName := s[0]
|
||||
teamName := s[1]
|
||||
|
||||
org, err := org_model.GetOrgByName(ctx, orgName)
|
||||
if err != nil {
|
||||
warnings = append(warnings, "incorrect codeowner organization: "+user)
|
||||
continue
|
||||
}
|
||||
teams, err := org.LoadTeams(ctx)
|
||||
if err != nil {
|
||||
warnings = append(warnings, "incorrect codeowner team: "+user)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, team := range teams {
|
||||
if team.Name == teamName {
|
||||
rule.Teams = append(rule.Teams, team)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
u, err := user_model.GetUserByName(ctx, user)
|
||||
if err != nil {
|
||||
warnings = append(warnings, "incorrect codeowner user: "+user)
|
||||
continue
|
||||
}
|
||||
rule.Users = append(rule.Users, u)
|
||||
}
|
||||
}
|
||||
|
||||
if (len(rule.Users) == 0) && (len(rule.Teams) == 0) {
|
||||
warnings = append(warnings, "no users/groups matched")
|
||||
return nil, warnings
|
||||
}
|
||||
|
||||
return rule, warnings
|
||||
}
|
||||
|
||||
func TokenizeCodeOwnersLine(line string) []string {
|
||||
if len(line) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
line = strings.ReplaceAll(line, "\t", " ")
|
||||
|
||||
tokens := make([]string, 0)
|
||||
|
||||
escape := false
|
||||
token := ""
|
||||
for _, char := range line {
|
||||
if escape {
|
||||
token += string(char)
|
||||
escape = false
|
||||
} else if string(char) == "\\" {
|
||||
escape = true
|
||||
} else if string(char) == "#" {
|
||||
break
|
||||
} else if string(char) == " " {
|
||||
if len(token) > 0 {
|
||||
tokens = append(tokens, token)
|
||||
token = ""
|
||||
}
|
||||
} else {
|
||||
token += string(char)
|
||||
}
|
||||
}
|
||||
|
||||
if len(token) > 0 {
|
||||
tokens = append(tokens, token)
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
// InsertPullRequests inserted pull requests
|
||||
func InsertPullRequests(ctx context.Context, prs ...*PullRequest) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
|
||||
@@ -44,6 +44,7 @@ func TestPullRequest(t *testing.T) {
|
||||
t.Run("ParseCodeOwnersLine", testParseCodeOwnersLine)
|
||||
t.Run("CodeOwnerAbsolutePathPatterns", testCodeOwnerAbsolutePathPatterns)
|
||||
t.Run("CodeOwnerPatternMatchTimeout", testCodeOwnerPatternMatchTimeout)
|
||||
t.Run("CodeOwnerSectionsAndAnnotations", testCodeOwnerSectionsAndAnnotations)
|
||||
t.Run("GetApprovers", testGetApprovers)
|
||||
t.Run("GetPullRequestByMergedCommit", testGetPullRequestByMergedCommit)
|
||||
t.Run("Migrate_InsertPullRequests", testMigrateInsertPullRequests)
|
||||
@@ -335,7 +336,7 @@ func testParseCodeOwnersLine(t *testing.T) {
|
||||
|
||||
given := []CodeOwnerTest{
|
||||
{Line: "", Tokens: nil},
|
||||
{Line: "# comment", Tokens: []string{}},
|
||||
{Line: "# comment", Tokens: nil},
|
||||
{Line: "!.* @user1 @org1/team1", Tokens: []string{"!.*", "@user1", "@org1/team1"}},
|
||||
{Line: `.*\\.js @user2 #comment`, Tokens: []string{`.*\.js`, "@user2"}},
|
||||
{Line: `docs/(aws|google|azure)/[^/]*\\.(md|txt) @org3 @org2/team2`, Tokens: []string{`docs/(aws|google|azure)/[^/]*\.(md|txt)`, "@org3", "@org2/team2"}},
|
||||
@@ -347,7 +348,7 @@ func testParseCodeOwnersLine(t *testing.T) {
|
||||
|
||||
for _, g := range given {
|
||||
tokens := issues_model.TokenizeCodeOwnersLine(g.Line)
|
||||
assert.Equal(t, g.Tokens, tokens, "Codeowners tokenizer failed")
|
||||
assert.Equal(t, g.Tokens, tokens, "Codeowners tokenizer failed: input=%s", g.Line)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,3 +449,49 @@ func testMigrateInsertPullRequests(t *testing.T) {
|
||||
|
||||
unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.PullRequest{})
|
||||
}
|
||||
|
||||
func testCodeOwnerSectionsAndAnnotations(t *testing.T) {
|
||||
rules, warnings := issues_model.GetCodeOwnersFromContent(t.Context(), "*.js @user5\n[Docs] @user1\n*.md\n")
|
||||
require.Empty(t, warnings)
|
||||
require.Len(t, rules, 2)
|
||||
for _, rule := range rules {
|
||||
assert.False(t, rule.ReviewRequest.Has())
|
||||
}
|
||||
|
||||
rules, warnings = issues_model.GetCodeOwnersFromContent(t.Context(), `## review-request=false # global
|
||||
*.js @user5 ## review-request=true # line
|
||||
[Documentation] @user1 ## review-request=true # section
|
||||
*.md
|
||||
/docs/* @user5 ## review-request=false
|
||||
[Other Section] @user5
|
||||
*.txt
|
||||
`)
|
||||
match := func(rule *issues_model.CodeOwnerRule, path string) bool {
|
||||
v, _ := rule.Rule.MatchString(path)
|
||||
return v
|
||||
}
|
||||
require.Empty(t, warnings)
|
||||
require.Len(t, rules, 4)
|
||||
assert.True(t, rules[0].ReviewRequest.Value())
|
||||
assert.True(t, match(rules[0], "src/app.js"))
|
||||
assert.False(t, match(rules[0], "appXjs"))
|
||||
assert.Equal(t, "Documentation", rules[1].Section)
|
||||
assert.Equal(t, "user1", rules[1].Users[0].Name)
|
||||
assert.True(t, rules[1].ReviewRequest.Value())
|
||||
assert.Equal(t, "user5", rules[2].Users[0].Name)
|
||||
assert.False(t, rules[2].ReviewRequest.Value())
|
||||
assert.True(t, match(rules[2], "docs/guide.md"))
|
||||
assert.False(t, match(rules[2], "other/docs/guide.md"))
|
||||
assert.False(t, rules[3].ReviewRequest.Value())
|
||||
|
||||
rules, warnings = issues_model.GetCodeOwnersFromContent(t.Context(), "*.js @user5 ## review-request=invalid\n*[ @user5\n")
|
||||
require.Len(t, rules, 1)
|
||||
assert.False(t, rules[0].ReviewRequest.Has())
|
||||
require.Len(t, warnings, 2)
|
||||
|
||||
rules, warnings = issues_model.GetCodeOwnersFromContent(t.Context(), `file\#name @user5 ## review-request=false`)
|
||||
require.Empty(t, warnings)
|
||||
require.Len(t, rules, 1)
|
||||
assert.True(t, match(rules[0], "file#name"))
|
||||
assert.False(t, rules[0].ReviewRequest.Value())
|
||||
}
|
||||
|
||||
@@ -121,11 +121,15 @@ func (rt ReviewType) Icon() string {
|
||||
|
||||
// Review represents collection of code comments giving feedback for a PR
|
||||
type Review struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Type ReviewType
|
||||
Reviewer *user_model.User `xorm:"-"`
|
||||
ReviewerID int64 `xorm:"index"`
|
||||
ReviewerTeamID int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Type ReviewType
|
||||
Reviewer *user_model.User `xorm:"-"`
|
||||
|
||||
// HINT: REVIEWER-ID-NEGATIVE: ReviewerID and ReviewerTeamID must be positive at the moment,
|
||||
// because the current template uses a trick "ItemID < 0" means "team id"
|
||||
ReviewerID int64 `xorm:"index"`
|
||||
ReviewerTeamID int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
|
||||
ReviewerTeam *organization.Team `xorm:"-"`
|
||||
OriginalAuthor string
|
||||
OriginalAuthorID int64
|
||||
@@ -186,7 +190,7 @@ func (r *Review) LoadReviewerTeam(ctx context.Context) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
r.ReviewerTeam, err = organization.GetTeamByID(ctx, r.ReviewerTeamID)
|
||||
r.ReviewerTeamID, r.ReviewerTeam, err = organization.GetPossibleTeamByID(ctx, r.ReviewerTeamID)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -285,23 +289,12 @@ func IsOfficialReviewer(ctx context.Context, issue *Issue, reviewer *user_model.
|
||||
}
|
||||
if rule == nil {
|
||||
// if no rule is found, then user with write access can make official reviews
|
||||
err := pr.LoadBaseRepo(ctx)
|
||||
if err != nil {
|
||||
if err := pr.LoadBaseRepo(ctx); err != nil {
|
||||
return false, err
|
||||
}
|
||||
writeAccess, err := access_model.HasAccessUnit(ctx, reviewer, pr.BaseRepo, unit.TypeCode, perm.AccessModeWrite)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return writeAccess, nil
|
||||
return access_model.HasAccessUnit(ctx, reviewer, pr.BaseRepo, unit.TypeCode, perm.AccessModeWrite)
|
||||
}
|
||||
|
||||
official, err := git_model.IsUserOfficialReviewer(ctx, rule, reviewer)
|
||||
if official || err != nil {
|
||||
return official, err
|
||||
}
|
||||
|
||||
return false, nil
|
||||
return git_model.IsUserOfficialReviewer(ctx, rule, reviewer)
|
||||
}
|
||||
|
||||
// IsOfficialReviewerTeam check if reviewer in this team can make official reviews in issue (counts towards required approvals)
|
||||
@@ -313,66 +306,82 @@ func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organizatio
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if pb == nil {
|
||||
return false, nil
|
||||
if pb != nil && pb.EnableApprovalsWhitelist {
|
||||
return slices.Contains(pb.ApprovalsWhitelistTeamIDs, team.ID), nil
|
||||
}
|
||||
|
||||
if !pb.EnableApprovalsWhitelist {
|
||||
return team.UnitAccessMode(ctx, unit.TypeCode) >= perm.AccessModeWrite, nil
|
||||
}
|
||||
|
||||
return slices.Contains(pb.ApprovalsWhitelistTeamIDs, team.ID), nil
|
||||
return team.UnitAccessMode(ctx, unit.TypeCode) >= perm.AccessModeWrite, nil
|
||||
}
|
||||
|
||||
// RecalculateReviewsOfficial re-evaluates the "official" flag of the latest approve
|
||||
// and reject reviews of an issue against its pull request's current base branch.
|
||||
// It must be called whenever the target branch changes, otherwise an approval that
|
||||
// was official on the previous (possibly unprotected) branch would keep satisfying
|
||||
// the new branch's protection rules.
|
||||
func RecalculateReviewsOfficial(ctx context.Context, issue *Issue) error {
|
||||
if err := issue.LoadPullRequest(ctx); err != nil {
|
||||
// updateReviewerOfficial keeps the official flag only on the reviewer's latest approval, rejection or review request, and only if the reviewer is official
|
||||
func updateReviewerOfficial(ctx context.Context, issueID, reviewerID int64, official bool) error {
|
||||
if _, err := db.GetEngine(ctx).Where(builder.Eq{"issue_id": issueID, "reviewer_id": reviewerID}).
|
||||
NoAutoTime().Cols("official").Update(&Review{}); err != nil {
|
||||
return err
|
||||
}
|
||||
if !official {
|
||||
return nil
|
||||
}
|
||||
latest, err := GetReviewByIssueIDAndUserID(ctx, issueID, reviewerID)
|
||||
if IsErrReviewNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = db.GetEngine(ctx).ID(latest.ID).NoAutoTime().Cols("official").Update(&Review{Official: true})
|
||||
return err
|
||||
}
|
||||
|
||||
// Clearing and restoring the official flags must happen atomically, otherwise a
|
||||
// failure in between would leave the reviews without any official flag set.
|
||||
// RecalculateReviewsOfficial re-evaluates the "official" flag of an issue's reviews and review requests
|
||||
// against its pull request's current base branch. It must be called whenever the target branch changes,
|
||||
// otherwise an approval that was official on the previous (possibly unprotected) branch would keep
|
||||
// satisfying the new branch's protection rules.
|
||||
func RecalculateReviewsOfficial(ctx context.Context, issue *Issue) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
// Only the latest approve/reject review of each reviewer counts as official, so
|
||||
// clear the flag on all of them first and restore it only where it still applies.
|
||||
if _, err := db.GetEngine(ctx).
|
||||
Where("issue_id = ?", issue.ID).
|
||||
In("type", ReviewTypeApprove, ReviewTypeReject).
|
||||
Cols("official").
|
||||
Update(&Review{Official: false}); err != nil {
|
||||
if _, err := db.GetEngine(ctx).Where("issue_id = ?", issue.ID).NoAutoTime().Cols("official").Update(&Review{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reviews, err := FindLatestReviews(ctx, FindReviewOptions{
|
||||
Types: []ReviewType{ReviewTypeApprove, ReviewTypeReject},
|
||||
IssueID: issue.ID,
|
||||
})
|
||||
if err != nil {
|
||||
reviewerIDs := make([]int64, 0, 5)
|
||||
if err := db.GetEngine(ctx).Table("review").Cols("reviewer_id").
|
||||
Where(builder.Eq{"issue_id": issue.ID, "original_author_id": 0}).
|
||||
And(builder.In("type", ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest)).
|
||||
And(builder.Neq{"reviewer_id": 0}).
|
||||
Distinct("reviewer_id").Find(&reviewerIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, review := range reviews {
|
||||
if err := review.LoadReviewer(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if review.Reviewer == nil {
|
||||
continue
|
||||
}
|
||||
official, err := IsOfficialReviewer(ctx, issue, review.Reviewer)
|
||||
for _, reviewerID := range reviewerIDs {
|
||||
_, reviewer, err := user_model.GetPossibleUserByID(ctx, reviewerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if official {
|
||||
if _, err := db.GetEngine(ctx).ID(review.ID).Cols("official").Update(&Review{Official: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
official, err := IsOfficialReviewer(ctx, issue, reviewer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := updateReviewerOfficial(ctx, issue.ID, reviewerID, official); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
teamRequests := make([]*Review, 0, 5)
|
||||
if err := db.GetEngine(ctx).Where(builder.Eq{"issue_id": issue.ID, "type": ReviewTypeRequest}).
|
||||
And(builder.Gt{"reviewer_team_id": 0}).Find(&teamRequests); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, request := range teamRequests {
|
||||
if err := request.LoadReviewerTeam(ctx); organization.IsErrTeamNotExist(err) {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
official, err := IsOfficialReviewerTeam(ctx, issue, request.ReviewerTeam)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.GetEngine(ctx).ID(request.ID).NoAutoTime().Cols("official").Update(&Review{Official: official}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -472,7 +481,13 @@ func SubmitReview(ctx context.Context, doer *user_model.User, issue *Issue, revi
|
||||
defer committer.Close()
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
isApproveOrReject := reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject
|
||||
official := false
|
||||
if isApproveOrReject {
|
||||
if official, err = IsOfficialReviewer(ctx, issue, doer); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
review, err := GetCurrentReview(ctx, doer, issue)
|
||||
if err != nil {
|
||||
@@ -484,16 +499,6 @@ func SubmitReview(ctx context.Context, doer *user_model.User, issue *Issue, revi
|
||||
return nil, nil, ContentEmptyErr{}
|
||||
}
|
||||
|
||||
if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
|
||||
// Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
|
||||
if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if official, err = IsOfficialReviewer(ctx, issue, doer); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// No current review. Create a new one!
|
||||
if review, err = CreateReview(ctx, CreateReviewOptions{
|
||||
Type: reviewType,
|
||||
@@ -514,16 +519,6 @@ func SubmitReview(ctx context.Context, doer *user_model.User, issue *Issue, revi
|
||||
return nil, nil, ContentEmptyErr{}
|
||||
}
|
||||
|
||||
if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
|
||||
// Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
|
||||
if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if official, err = IsOfficialReviewer(ctx, issue, doer); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
review.Official = official
|
||||
review.Issue = issue
|
||||
review.Content = content
|
||||
@@ -544,6 +539,16 @@ func SubmitReview(ctx context.Context, doer *user_model.User, issue *Issue, revi
|
||||
}
|
||||
}
|
||||
|
||||
if isApproveOrReject {
|
||||
if err := updateReviewerOfficial(ctx, issue.ID, doer.ID, official); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if reviewType == ReviewTypePending {
|
||||
return review, nil, committer.Commit()
|
||||
}
|
||||
|
||||
comm, err := CreateComment(ctx, &CreateCommentOptions{
|
||||
Type: CommentTypeReview,
|
||||
Doer: doer,
|
||||
@@ -558,7 +563,7 @@ func SubmitReview(ctx context.Context, doer *user_model.User, issue *Issue, revi
|
||||
}
|
||||
|
||||
// try to remove team review request if need
|
||||
if issue.Repo.Owner.IsOrganization() && (reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject) {
|
||||
if issue.Repo.Owner.IsOrganization() && isApproveOrReject {
|
||||
teamReviewRequests := make([]*Review, 0, 10)
|
||||
if err := sess.SQL("SELECT * FROM review WHERE issue_id = ? AND reviewer_team_id > 0 AND type = ?", issue.ID, ReviewTypeRequest).Find(&teamReviewRequests); err != nil {
|
||||
return nil, nil, err
|
||||
@@ -696,8 +701,6 @@ func InsertReviews(ctx context.Context, reviews []*Review) error {
|
||||
// AddReviewRequest add a review request from one reviewer
|
||||
func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_model.User, isCodeOwners bool) (*Comment, error) {
|
||||
return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) {
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID)
|
||||
if err != nil && !IsErrReviewNotExist(err) {
|
||||
return nil, err
|
||||
@@ -723,15 +726,9 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo
|
||||
}
|
||||
}
|
||||
|
||||
// if the reviewer is an official reviewer,
|
||||
// remove the official flag in the all previous reviews
|
||||
official, err := IsOfficialReviewer(ctx, issue, reviewer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if official {
|
||||
if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, reviewer.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
review, err = CreateReview(ctx, CreateReviewOptions{
|
||||
@@ -745,6 +742,10 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := updateReviewerOfficial(ctx, issue.ID, reviewer.ID, official); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
comment, err := CreateComment(ctx, &CreateCommentOptions{
|
||||
Type: CommentTypeReviewRequest,
|
||||
Doer: doer,
|
||||
@@ -785,10 +786,9 @@ func RemoveReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user
|
||||
official, err := IsOfficialReviewer(ctx, issue, reviewer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if official {
|
||||
if err := restoreLatestOfficialReview(ctx, issue.ID, reviewer.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := updateReviewerOfficial(ctx, issue.ID, reviewer.ID, official); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return CreateComment(ctx, &CreateCommentOptions{
|
||||
@@ -802,22 +802,6 @@ func RemoveReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user
|
||||
})
|
||||
}
|
||||
|
||||
// Recalculate the latest official review for reviewer
|
||||
func restoreLatestOfficialReview(ctx context.Context, issueID, reviewerID int64) error {
|
||||
review, err := GetReviewByIssueIDAndUserID(ctx, issueID, reviewerID)
|
||||
if err != nil && !IsErrReviewNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if review != nil {
|
||||
if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddTeamReviewRequest add a review request from one team
|
||||
func AddTeamReviewRequest(ctx context.Context, issue *Issue, reviewer *organization.Team, doer *user_model.User, isCodeOwners bool) (*Comment, error) {
|
||||
return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) {
|
||||
@@ -834,10 +818,6 @@ func AddTeamReviewRequest(ctx context.Context, issue *Issue, reviewer *organizat
|
||||
official, err := IsOfficialReviewerTeam(ctx, issue, reviewer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("isOfficialReviewerTeam(): %w", err)
|
||||
} else if !official {
|
||||
if official, err = IsOfficialReviewer(ctx, issue, doer); err != nil {
|
||||
return nil, fmt.Errorf("isOfficialReviewer(): %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if review, err = CreateReview(ctx, CreateReviewOptions{
|
||||
@@ -850,12 +830,6 @@ func AddTeamReviewRequest(ctx context.Context, issue *Issue, reviewer *organizat
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if official {
|
||||
if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_team_id=?", false, issue.ID, reviewer.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
comment, err := CreateComment(ctx, &CreateCommentOptions{
|
||||
Type: CommentTypeReviewRequest,
|
||||
Doer: doer,
|
||||
@@ -890,25 +864,6 @@ func RemoveTeamReviewRequest(ctx context.Context, issue *Issue, reviewer *organi
|
||||
return nil, err
|
||||
}
|
||||
|
||||
official, err := IsOfficialReviewerTeam(ctx, issue, reviewer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("isOfficialReviewerTeam(): %w", err)
|
||||
}
|
||||
|
||||
if official {
|
||||
// recalculate which is the latest official review from that team
|
||||
review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, -reviewer.ID)
|
||||
if err != nil && !IsErrReviewNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if review != nil {
|
||||
if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if doer == nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -1001,44 +956,22 @@ func DeleteReview(ctx context.Context, r *Review) error {
|
||||
return errors.New("review request can not be deleted using this method")
|
||||
}
|
||||
|
||||
opts := FindCommentsOptions{
|
||||
Type: CommentTypeCode,
|
||||
IssueID: r.IssueID,
|
||||
ReviewID: r.ID,
|
||||
}
|
||||
|
||||
if _, err := db.Delete[Comment](ctx, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts = FindCommentsOptions{
|
||||
Type: CommentTypeReview,
|
||||
IssueID: r.IssueID,
|
||||
ReviewID: r.ID,
|
||||
}
|
||||
|
||||
if _, err := db.Delete[Comment](ctx, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts = FindCommentsOptions{
|
||||
Type: CommentTypeDismissReview,
|
||||
IssueID: r.IssueID,
|
||||
ReviewID: r.ID,
|
||||
}
|
||||
|
||||
if _, err := db.Delete[Comment](ctx, opts); err != nil {
|
||||
return err
|
||||
for _, commentType := range [...]CommentType{CommentTypeCode, CommentTypeReview, CommentTypeDismissReview} {
|
||||
if _, err := db.Delete[Comment](ctx, FindCommentsOptions{
|
||||
Type: commentType,
|
||||
IssueID: r.IssueID,
|
||||
ReviewID: r.ID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := db.DeleteByID[Review](ctx, r.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.Official {
|
||||
if err := restoreLatestOfficialReview(ctx, r.IssueID, r.ReviewerID); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Official && r.OriginalAuthorID == 0 {
|
||||
return updateReviewerOfficial(ctx, r.IssueID, r.ReviewerID, true)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"gitea.dev/models/db"
|
||||
git_model "gitea.dev/models/git"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
"gitea.dev/models/organization"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
@@ -434,3 +435,17 @@ func TestRecalculateReviewsOfficial(t *testing.T) {
|
||||
review = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: review.ID})
|
||||
assert.True(t, review.Official)
|
||||
}
|
||||
|
||||
func TestTeamReviewRequestOfficial(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 12})
|
||||
assert.NoError(t, issue.LoadRepo(t.Context()))
|
||||
comment, err := issues_model.AddTeamReviewRequest(t.Context(), issue, unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2}), &user_model.User{ID: 2}, false)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: comment.ReviewID}).Official)
|
||||
|
||||
assert.NoError(t, db.Insert(t.Context(), &git_model.ProtectedBranch{RepoID: issue.RepoID, RuleName: "master", EnableApprovalsWhitelist: true}))
|
||||
assert.NoError(t, issues_model.RecalculateReviewsOfficial(t.Context(), issue))
|
||||
assert.False(t, unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: comment.ReviewID}).Official)
|
||||
}
|
||||
|
||||
@@ -74,9 +74,9 @@ const OwnerTeamName = "Owners"
|
||||
|
||||
// Team represents a organization team.
|
||||
type Team struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OrgID int64 `xorm:"INDEX"`
|
||||
LowerName string
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OrgID int64 `xorm:"INDEX UNIQUE(s)"`
|
||||
LowerName string `xorm:"UNIQUE(s)"`
|
||||
Name string
|
||||
Description string
|
||||
AccessMode perm.AccessMode `xorm:"'authorize'"`
|
||||
|
||||
@@ -87,15 +87,7 @@ func SearchPackages(ctx context.Context, opts *PackageSearchOptions) ([]*package
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pfds := make([]*packages.PackageFileDescriptor, 0, len(pkgFiles))
|
||||
for _, pf := range pkgFiles {
|
||||
pfd, err := packages.GetPackageFileDescriptor(ctx, pf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pfds = append(pfds, pfd)
|
||||
}
|
||||
return pfds, nil
|
||||
return packages.GetPackageFileDescriptors(ctx, pkgFiles)
|
||||
}
|
||||
|
||||
// GetDistributions gets all available distributions
|
||||
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
"gitea.dev/modules/packages/rpm"
|
||||
"gitea.dev/modules/packages/rubygems"
|
||||
"gitea.dev/modules/packages/swift"
|
||||
"gitea.dev/modules/packages/terraform_module"
|
||||
"gitea.dev/modules/packages/vagrant"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
@@ -212,8 +213,10 @@ func GetPackageDescriptorWithCache(ctx context.Context, pv *PackageVersion, c *c
|
||||
metadata = &rubygems.Metadata{}
|
||||
case TypeSwift:
|
||||
metadata = &swift.Metadata{}
|
||||
case TypeTerraformModule:
|
||||
metadata = &terraform_module.Metadata{}
|
||||
case TypeTerraformState:
|
||||
// terraform packages have no metadata
|
||||
// terraform state packages have no metadata
|
||||
case TypeVagrant:
|
||||
metadata = &vagrant.Metadata{}
|
||||
default:
|
||||
|
||||
@@ -30,29 +30,30 @@ type Type string
|
||||
|
||||
// List of supported packages
|
||||
const (
|
||||
TypeAlpine Type = "alpine"
|
||||
TypeArch Type = "arch"
|
||||
TypeCargo Type = "cargo"
|
||||
TypeChef Type = "chef"
|
||||
TypeComposer Type = "composer"
|
||||
TypeConan Type = "conan"
|
||||
TypeConda Type = "conda"
|
||||
TypeContainer Type = "container"
|
||||
TypeCran Type = "cran"
|
||||
TypeDebian Type = "debian"
|
||||
TypeGeneric Type = "generic"
|
||||
TypeGo Type = "go"
|
||||
TypeHelm Type = "helm"
|
||||
TypeMaven Type = "maven"
|
||||
TypeNpm Type = "npm"
|
||||
TypeNuGet Type = "nuget"
|
||||
TypePub Type = "pub"
|
||||
TypePyPI Type = "pypi"
|
||||
TypeRpm Type = "rpm"
|
||||
TypeRubyGems Type = "rubygems"
|
||||
TypeSwift Type = "swift"
|
||||
TypeTerraformState Type = "terraform"
|
||||
TypeVagrant Type = "vagrant"
|
||||
TypeAlpine Type = "alpine"
|
||||
TypeArch Type = "arch"
|
||||
TypeCargo Type = "cargo"
|
||||
TypeChef Type = "chef"
|
||||
TypeComposer Type = "composer"
|
||||
TypeConan Type = "conan"
|
||||
TypeConda Type = "conda"
|
||||
TypeContainer Type = "container"
|
||||
TypeCran Type = "cran"
|
||||
TypeDebian Type = "debian"
|
||||
TypeGeneric Type = "generic"
|
||||
TypeGo Type = "go"
|
||||
TypeHelm Type = "helm"
|
||||
TypeMaven Type = "maven"
|
||||
TypeNpm Type = "npm"
|
||||
TypeNuGet Type = "nuget"
|
||||
TypePub Type = "pub"
|
||||
TypePyPI Type = "pypi"
|
||||
TypeRpm Type = "rpm"
|
||||
TypeRubyGems Type = "rubygems"
|
||||
TypeSwift Type = "swift"
|
||||
TypeTerraformModule Type = "terraform-module"
|
||||
TypeTerraformState Type = "terraform"
|
||||
TypeVagrant Type = "vagrant"
|
||||
)
|
||||
|
||||
var TypeList = []Type{
|
||||
@@ -77,6 +78,7 @@ var TypeList = []Type{
|
||||
TypeRpm,
|
||||
TypeRubyGems,
|
||||
TypeSwift,
|
||||
TypeTerraformModule,
|
||||
TypeTerraformState,
|
||||
TypeVagrant,
|
||||
}
|
||||
@@ -126,6 +128,8 @@ func (pt Type) Name() string {
|
||||
return "RubyGems"
|
||||
case TypeSwift:
|
||||
return "Swift"
|
||||
case TypeTerraformModule:
|
||||
return "Terraform Module"
|
||||
case TypeTerraformState:
|
||||
return "Terraform State"
|
||||
case TypeVagrant:
|
||||
@@ -179,6 +183,8 @@ func (pt Type) SVGName() string {
|
||||
return "gitea-rubygems"
|
||||
case TypeSwift:
|
||||
return "gitea-swift"
|
||||
case TypeTerraformModule:
|
||||
return "gitea-terraform"
|
||||
case TypeTerraformState:
|
||||
return "gitea-terraform"
|
||||
case TypeVagrant:
|
||||
|
||||
@@ -224,30 +224,32 @@ func RecalculateTeamAccesses(ctx context.Context, repo *repo_model.Repository, i
|
||||
|
||||
// RecalculateUserAccess recalculates new access for a single user
|
||||
// Usable if we know access only affected one user
|
||||
func RecalculateUserAccess(ctx context.Context, repo *repo_model.Repository, uid int64) (err error) {
|
||||
func RecalculateUserAccess(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (err error) {
|
||||
if err = repo.LoadOwner(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
minMode := perm.AccessModeRead
|
||||
if !repo.IsPrivate {
|
||||
if !repo.IsPrivate && !repo.Owner.IsOrganization() && !user.IsRestricted {
|
||||
minMode = perm.AccessModeWrite
|
||||
}
|
||||
|
||||
accessMode := perm.AccessModeNone
|
||||
e := db.GetEngine(ctx)
|
||||
collaborator, err := repo_model.GetCollaboration(ctx, repo.ID, uid)
|
||||
collaborator, err := repo_model.GetCollaboration(ctx, repo.ID, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if collaborator != nil {
|
||||
accessMode = collaborator.Mode
|
||||
}
|
||||
|
||||
if err = repo.LoadOwner(ctx); err != nil {
|
||||
return err
|
||||
} else if repo.Owner.IsOrganization() {
|
||||
if repo.Owner.IsOrganization() {
|
||||
var teams []organization.Team
|
||||
if err := e.Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
||||
Join("INNER", "team_user", "team_user.team_id = team.id").
|
||||
Where("team.org_id = ?", repo.OwnerID).
|
||||
And("team_repo.repo_id=?", repo.ID).
|
||||
And("team_user.uid=?", uid).
|
||||
And("team_user.uid=?", user.ID).
|
||||
Find(&teams); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -262,10 +264,10 @@ func RecalculateUserAccess(ctx context.Context, repo *repo_model.Repository, uid
|
||||
}
|
||||
|
||||
// Delete old user accesses and insert new one for repository.
|
||||
if _, err = e.Delete(&Access{RepoID: repo.ID, UserID: uid}); err != nil {
|
||||
if _, err = e.Delete(&Access{RepoID: repo.ID, UserID: user.ID}); err != nil {
|
||||
return fmt.Errorf("delete old user accesses: %w", err)
|
||||
} else if accessMode >= minMode {
|
||||
if err = db.Insert(ctx, &Access{RepoID: repo.ID, UserID: uid, Mode: accessMode}); err != nil {
|
||||
if err = db.Insert(ctx, &Access{RepoID: repo.ID, UserID: user.ID, Mode: accessMode}); err != nil {
|
||||
return fmt.Errorf("insert new user accesses: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,8 @@ type Permission struct {
|
||||
everyoneAccessMode map[unit.Type]perm_model.AccessMode // the unit's minimal access mode for every signed-in user
|
||||
anonymousAccessMode map[unit.Type]perm_model.AccessMode // the unit's minimal access mode for anonymous (non-signed-in) user
|
||||
|
||||
orgRepoTeams []*organization.Team
|
||||
isCollaborator bool
|
||||
orgRepoTeams []*organization.Team
|
||||
}
|
||||
|
||||
// IsOwner returns true if current user is the owner of repository.
|
||||
@@ -48,6 +49,10 @@ func (p *Permission) IsAdmin() bool {
|
||||
return p.AccessMode >= perm_model.AccessModeAdmin
|
||||
}
|
||||
|
||||
func (p *Permission) IsMemberOrCollaborator() bool {
|
||||
return p.IsAdmin() || p.isCollaborator || len(p.orgRepoTeams) > 0
|
||||
}
|
||||
|
||||
// HasAnyUnitAccess returns true if the user might have at least one access mode to any unit of this repository.
|
||||
// It doesn't count the "public(anonymous/everyone) access mode".
|
||||
// TODO: most calls to this function should be replaced with `HasAnyUnitAccessOrPublicAccess`
|
||||
@@ -240,23 +245,10 @@ func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) {
|
||||
origPermUnits := perm.units
|
||||
perm.units = make([]*repo_model.RepoUnit, 0, len(perm.units))
|
||||
for _, u := range origPermUnits {
|
||||
shouldKeep := false
|
||||
for t := range perm.unitsMode {
|
||||
if shouldKeep = u.Type == t; shouldKeep {
|
||||
break
|
||||
}
|
||||
}
|
||||
for t := range perm.anonymousAccessMode {
|
||||
if shouldKeep = shouldKeep || u.Type == t; shouldKeep {
|
||||
break
|
||||
}
|
||||
}
|
||||
for t := range perm.everyoneAccessMode {
|
||||
if shouldKeep = shouldKeep || u.Type == t; shouldKeep {
|
||||
break
|
||||
}
|
||||
}
|
||||
if shouldKeep {
|
||||
_, hasUnitMode := perm.unitsMode[u.Type]
|
||||
_, hasAnonymousMode := perm.anonymousAccessMode[u.Type]
|
||||
_, hasEveryoneMode := perm.everyoneAccessMode[u.Type]
|
||||
if hasUnitMode || hasAnonymousMode || hasEveryoneMode {
|
||||
perm.units = append(perm.units, u)
|
||||
}
|
||||
}
|
||||
@@ -445,9 +437,8 @@ func GetIndividualUserRepoPermission(ctx context.Context, repo *repo_model.Repos
|
||||
return perm, nil
|
||||
}
|
||||
|
||||
var isCollaborator bool
|
||||
if user != nil {
|
||||
isCollaborator, err = repo_model.IsCollaborator(ctx, repo.ID, user.ID)
|
||||
perm.isCollaborator, err = repo_model.IsCollaborator(ctx, repo.ID, user.ID)
|
||||
if err != nil {
|
||||
return perm, err
|
||||
}
|
||||
@@ -460,7 +451,7 @@ func GetIndividualUserRepoPermission(ctx context.Context, repo *repo_model.Repos
|
||||
// Prevent strangers from checking out public repo of private organization/users
|
||||
// Allow user if they are a collaborator of a repo within a private user or a private organization but not a member of the organization itself
|
||||
// TODO: rename it to "IsOwnerVisibleToDoer"
|
||||
if !organization.HasOrgOrUserVisible(ctx, repo.Owner, user) && !isCollaborator {
|
||||
if !organization.HasOrgOrUserVisible(ctx, repo.Owner, user) && !perm.isCollaborator {
|
||||
perm.AccessMode = perm_model.AccessModeNone
|
||||
return perm, nil
|
||||
}
|
||||
@@ -503,7 +494,7 @@ func GetIndividualUserRepoPermission(ctx context.Context, repo *repo_model.Repos
|
||||
perm.unitsMode = make(map[unit.Type]perm_model.AccessMode)
|
||||
|
||||
// Collaborators on organization
|
||||
if isCollaborator {
|
||||
if perm.isCollaborator {
|
||||
for _, u := range repo.Units {
|
||||
perm.unitsMode[u.Type] = perm.AccessMode
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"sync"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/setting"
|
||||
@@ -36,10 +37,12 @@ const (
|
||||
)
|
||||
|
||||
// ColumnColorPattern is a regexp witch can validate ColumnColor
|
||||
var ColumnColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$")
|
||||
var ColumnColorPattern = sync.OnceValue(func() *regexp.Regexp {
|
||||
return regexp.MustCompile("^#[0-9a-fA-F]{6}$")
|
||||
})
|
||||
|
||||
func validateColumnColor(color string) error {
|
||||
if len(color) != 0 && !ColumnColorPattern.MatchString(color) {
|
||||
if len(color) != 0 && !ColumnColorPattern().MatchString(color) {
|
||||
return util.ErrorWrap(util.ErrUnprocessableContent, "invalid column color %q, expected a 6-digit hex string like #FF0000", color)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -43,6 +43,41 @@ func columnIssueIDs(column *Column) []int64 {
|
||||
return []int64{column.ID}
|
||||
}
|
||||
|
||||
// LockProject serializes concurrent card moves of a project, avoiding row-lock deadlocks between them.
|
||||
func LockProject(ctx context.Context, projectID int64) error {
|
||||
_, err := db.GetEngine(ctx).Exec("UPDATE `project` SET title=title WHERE id=?", projectID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ColumnIssueSorting is an issue's position in a column.
|
||||
type ColumnIssueSorting struct {
|
||||
IssueID int64 `xorm:"issue_id"`
|
||||
Sorting int64 `xorm:"sorting"`
|
||||
}
|
||||
|
||||
// GetColumnIssueSortings returns the column's issues except excludeIssueID in board order.
|
||||
func GetColumnIssueSortings(ctx context.Context, column *Column, excludeIssueID int64) (rows []ColumnIssueSorting, _ error) {
|
||||
err := db.GetEngine(ctx).Table("project_issue").
|
||||
Select("project_issue.issue_id, project_issue.sorting").
|
||||
Join("INNER", "issue", "issue.id = project_issue.issue_id").
|
||||
Where("project_issue.project_id=?", column.ProjectID).
|
||||
In("project_issue.project_board_id", columnIssueIDs(column)).
|
||||
And("project_issue.issue_id<>?", excludeIssueID).
|
||||
OrderBy("project_issue.sorting ASC, issue.created_unix DESC, issue.id DESC").
|
||||
Find(&rows)
|
||||
return rows, err
|
||||
}
|
||||
|
||||
// SetIssueSorting sets the issue's sorting if it is still in the column.
|
||||
func SetIssueSorting(ctx context.Context, column *Column, issueID, sorting int64) error {
|
||||
_, err := db.GetEngine(ctx).
|
||||
Where("project_id=? AND issue_id=?", column.ProjectID, issueID).
|
||||
In("project_board_id", columnIssueIDs(column)).
|
||||
Cols("sorting").
|
||||
Update(&ProjectIssue{Sorting: sorting})
|
||||
return err
|
||||
}
|
||||
|
||||
// IsIssueInColumn reports whether the issue is placed in the column.
|
||||
func IsIssueInColumn(ctx context.Context, issueID int64, column *Column) (bool, error) {
|
||||
return db.GetEngine(ctx).
|
||||
|
||||
@@ -459,9 +459,6 @@ func DeleteProjectByRepoID(ctx context.Context, repoID int64) error {
|
||||
if _, err := db.GetEngine(ctx).Exec("DELETE FROM project_board WHERE project_board.id IN (SELECT project_board.id FROM project_board INNER JOIN project WHERE project.id = project_board.project_id AND project.repo_id = ?)", repoID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.GetEngine(ctx).Table("project").Where("repo_id = ? ", repoID).Delete(&Project{}); err != nil {
|
||||
return err
|
||||
}
|
||||
case setting.Database.Type.IsPostgreSQL():
|
||||
if _, err := db.GetEngine(ctx).Exec("DELETE FROM project_issue USING project WHERE project.id = project_issue.project_id AND project.repo_id = ? ", repoID); err != nil {
|
||||
return err
|
||||
@@ -469,9 +466,6 @@ func DeleteProjectByRepoID(ctx context.Context, repoID int64) error {
|
||||
if _, err := db.GetEngine(ctx).Exec("DELETE FROM project_board USING project WHERE project.id = project_board.project_id AND project.repo_id = ? ", repoID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.GetEngine(ctx).Table("project").Where("repo_id = ? ", repoID).Delete(&Project{}); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if _, err := db.GetEngine(ctx).Exec("DELETE project_issue FROM project_issue INNER JOIN project ON project.id = project_issue.project_id WHERE project.repo_id = ? ", repoID); err != nil {
|
||||
return err
|
||||
@@ -479,10 +473,10 @@ func DeleteProjectByRepoID(ctx context.Context, repoID int64) error {
|
||||
if _, err := db.GetEngine(ctx).Exec("DELETE project_board FROM project_board INNER JOIN project ON project.id = project_board.project_id WHERE project.repo_id = ? ", repoID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.GetEngine(ctx).Table("project").Where("repo_id = ? ", repoID).Delete(&Project{}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := db.GetEngine(ctx).Table("project").Where("repo_id = ? ", repoID).Delete(&Project{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return updateRepositoryProjectCount(ctx, repoID)
|
||||
}
|
||||
|
||||
@@ -42,6 +42,10 @@ func init() {
|
||||
db.RegisterModel(new(Attachment))
|
||||
}
|
||||
|
||||
func (a *Attachment) IsLinked() bool {
|
||||
return a.IssueID > 0 || a.ReleaseID > 0 || a.CommentID > 0
|
||||
}
|
||||
|
||||
// IncreaseDownloadCount is update download count + 1
|
||||
func (a *Attachment) IncreaseDownloadCount(ctx context.Context) error {
|
||||
// Update download count.
|
||||
|
||||
@@ -7,54 +7,26 @@ import (
|
||||
"context"
|
||||
|
||||
"gitea.dev/models/unit"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
)
|
||||
|
||||
// ___________.__ ___________ __
|
||||
// \__ ___/|__| _____ ___\__ ___/___________ ____ | | __ ___________
|
||||
// | | | |/ \_/ __ \| | \_ __ \__ \ _/ ___\| |/ // __ \_ __ \
|
||||
// | | | | Y Y \ ___/| | | | \// __ \\ \___| <\ ___/| | \/
|
||||
// |____| |__|__|_| /\___ >____| |__| (____ /\___ >__|_ \\___ >__|
|
||||
// \/ \/ \/ \/ \/ \/
|
||||
|
||||
// CanEnableTimetracker returns true when the server admin enabled time tracking
|
||||
// This overrules IsTimetrackerEnabled
|
||||
func (repo *Repository) CanEnableTimetracker() bool {
|
||||
return setting.Service.EnableTimetracking
|
||||
}
|
||||
|
||||
// IsTimetrackerEnabled returns whether or not the timetracker is enabled. It returns the default value from config if an error occurs.
|
||||
func (repo *Repository) IsTimetrackerEnabled(ctx context.Context) bool {
|
||||
if !setting.Service.EnableTimetracking {
|
||||
return false
|
||||
}
|
||||
|
||||
var u *RepoUnit
|
||||
var err error
|
||||
if u, err = repo.GetUnit(ctx, unit.TypeIssues); err != nil {
|
||||
return setting.Service.DefaultEnableTimetracking
|
||||
}
|
||||
return u.IssuesConfig().EnableTimetracker
|
||||
return repo.MustGetUnit(ctx, unit.TypeIssues).IssuesConfig().EnableTimetracker
|
||||
}
|
||||
|
||||
// AllowOnlyContributorsToTrackTime returns value of IssuesConfig or the default value
|
||||
func (repo *Repository) AllowOnlyContributorsToTrackTime(ctx context.Context) bool {
|
||||
var u *RepoUnit
|
||||
var err error
|
||||
if u, err = repo.GetUnit(ctx, unit.TypeIssues); err != nil {
|
||||
return setting.Service.DefaultAllowOnlyContributorsToTrackTime
|
||||
}
|
||||
return u.IssuesConfig().AllowOnlyContributorsToTrackTime
|
||||
return repo.MustGetUnit(ctx, unit.TypeIssues).IssuesConfig().AllowOnlyContributorsToTrackTime
|
||||
}
|
||||
|
||||
// IsDependenciesEnabled returns if dependencies are enabled and returns the default setting if not set.
|
||||
func (repo *Repository) IsDependenciesEnabled(ctx context.Context) bool {
|
||||
var u *RepoUnit
|
||||
var err error
|
||||
if u, err = repo.GetUnit(ctx, unit.TypeIssues); err != nil {
|
||||
log.Trace("IsDependenciesEnabled: %v", err)
|
||||
return setting.Service.DefaultEnableDependencies
|
||||
}
|
||||
return u.IssuesConfig().EnableDependencies
|
||||
return repo.MustGetUnit(ctx, unit.TypeIssues).IssuesConfig().EnableDependencies
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ func UpdateLanguageStats(ctx context.Context, repo *Repository, commitID string,
|
||||
var topLang string
|
||||
var s int64
|
||||
for lang, size := range stats {
|
||||
if size > s {
|
||||
if size > s || (size == s && lang < topLang) {
|
||||
s = size
|
||||
topLang = lang
|
||||
}
|
||||
|
||||
28
models/repo/language_stats_test.go
Normal file
28
models/repo/language_stats_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUpdateLanguageStatsPrimaryTie(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
require.NoError(t, repo_model.UpdateLanguageStats(t.Context(), repo, "1234567890", map[string]int64{"Go": 10, "C": 10}))
|
||||
|
||||
stats, err := repo_model.GetLanguageStats(t.Context(), repo)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, stats, 2)
|
||||
for _, stat := range stats {
|
||||
assert.Equal(t, stat.Language == "C", stat.IsPrimary, stat.Language)
|
||||
}
|
||||
}
|
||||
@@ -334,18 +334,32 @@ func GetTagNamesByRepoID(ctx context.Context, repoID int64) ([]string, error) {
|
||||
return tags, sess.Find(&tags)
|
||||
}
|
||||
|
||||
// GetLatestReleaseByRepoID returns the latest release for a repository
|
||||
func GetLatestReleaseByRepoID(ctx context.Context, repoID int64) (*Release, error) {
|
||||
cond := builder.NewCond().
|
||||
And(builder.Eq{"repo_id": repoID}).
|
||||
And(builder.Eq{"is_draft": false}).
|
||||
And(builder.Eq{"is_prerelease": false}).
|
||||
And(builder.Eq{"is_tag": false})
|
||||
func (r *Release) CanBeLatest() bool {
|
||||
return !r.IsDraft && !r.IsPrerelease && !r.IsTag
|
||||
}
|
||||
|
||||
func latestReleaseCond(repoID int64) builder.Cond {
|
||||
return FindReleasesOptions{RepoID: repoID, IsPreRelease: optional.Some(false)}.ToConds()
|
||||
}
|
||||
|
||||
func GetLatestRelease(ctx context.Context, repo *Repository) (*Release, error) {
|
||||
if repo.LatestReleaseID != 0 {
|
||||
rel, has, err := db.Get[Release](ctx, latestReleaseCond(repo.ID).And(builder.Eq{"id": repo.LatestReleaseID}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if has {
|
||||
return rel, nil
|
||||
}
|
||||
}
|
||||
return GetLatestReleaseByRepoID(ctx, repo.ID)
|
||||
}
|
||||
|
||||
// GetLatestReleaseByRepoID returns the newest published stable release of a repository by date
|
||||
func GetLatestReleaseByRepoID(ctx context.Context, repoID int64) (*Release, error) {
|
||||
rel := new(Release)
|
||||
has, err := db.GetEngine(ctx).
|
||||
Desc("created_unix", "id").
|
||||
Where(cond).
|
||||
Where(latestReleaseCond(repoID)).
|
||||
Get(rel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -356,6 +370,27 @@ func GetLatestReleaseByRepoID(ctx context.Context, repoID int64) (*Release, erro
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
func SetLatestReleaseID(ctx context.Context, repo *Repository, releaseID int64) error {
|
||||
repo.LatestReleaseID = releaseID
|
||||
_, err := db.GetEngine(ctx).ID(repo.ID).Where(builder.Neq{"latest_release_id": releaseID}).
|
||||
Cols("latest_release_id").NoAutoTime().Update(repo)
|
||||
return err
|
||||
}
|
||||
|
||||
func UnsetLatestReleaseID(ctx context.Context, repo *Repository, releaseID int64) error {
|
||||
if releaseID == 0 {
|
||||
return nil
|
||||
}
|
||||
if _, err := db.GetEngine(ctx).Where(builder.Eq{"id": repo.ID, "latest_release_id": releaseID}).
|
||||
Cols("latest_release_id").NoAutoTime().Update(&Repository{LatestReleaseID: 0}); err != nil {
|
||||
return err
|
||||
}
|
||||
if repo.LatestReleaseID == releaseID {
|
||||
repo.LatestReleaseID = 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type releaseMetaSearch struct {
|
||||
IDs []int64
|
||||
Releases []*Release
|
||||
@@ -543,8 +578,3 @@ func FindTagsByCommitIDs(ctx context.Context, repoID int64, commitIDs ...string)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func DeleteRepoReleases(ctx context.Context, repoID int64) error {
|
||||
_, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Delete(new(Release))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -209,6 +209,7 @@ type Repository struct {
|
||||
CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"`
|
||||
Topics []string `xorm:"TEXT JSON"`
|
||||
ObjectFormatName string `xorm:"VARCHAR(6) NOT NULL DEFAULT 'sha1'"`
|
||||
LatestReleaseID int64 `xorm:"NOT NULL DEFAULT 0"` // 0 selects the latest release by date
|
||||
|
||||
TrustModel TrustModelType
|
||||
|
||||
|
||||
@@ -204,8 +204,6 @@ type SearchRepoOptions struct {
|
||||
// True -> include just has milestones
|
||||
// False -> include just has no milestone
|
||||
HasMilestones optional.Option[bool]
|
||||
// LowerNames represents valid lower names to restrict to
|
||||
LowerNames []string
|
||||
// When specified true, apply some filters over the conditions:
|
||||
// - Don't show forks, when opts.Fork is OptionalBoolNone.
|
||||
// - Do not display repositories that don't have a description, an icon and topics.
|
||||
@@ -658,8 +656,13 @@ func userAllPublicRepoCond(cond builder.Cond, ownerVisibilityLimit []structs.Vis
|
||||
|
||||
// AccessibleRepositoryCondition takes a user a returns a condition for checking if a repository is accessible
|
||||
func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) builder.Cond {
|
||||
unitCond := builder.NewCond()
|
||||
if unitType != unit.TypeInvalid {
|
||||
unitCond = builder.Exists(builder.Select("id").From("repo_unit").Where(builder.Expr("repo_unit.repo_id = `repository`.id").
|
||||
And(builder.Eq{"repo_unit.type": unitType}, builder.NotIn("repo_unit.type", unit.DisabledRepoUnitsGet()))))
|
||||
}
|
||||
if user_model.IsPublicOnlyDoer(user) && !user.IsRestricted {
|
||||
return PublicRepoUnderPublicOwnerCond()
|
||||
return PublicRepoUnderPublicOwnerCond().And(unitCond)
|
||||
}
|
||||
cond := builder.NewCond()
|
||||
|
||||
@@ -698,7 +701,7 @@ func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) bu
|
||||
cond = userAllPublicRepoCond(cond, orgVisibilityLimit)
|
||||
}
|
||||
}
|
||||
return cond.And(DoerTokenRepoCond(user))
|
||||
return cond.And(DoerTokenRepoCond(user), unitCond)
|
||||
}
|
||||
|
||||
// SearchRepositoryByName takes keyword and part of repository name to search,
|
||||
@@ -824,10 +827,6 @@ func GetUserRepositories(ctx context.Context, opts SearchRepoOptions) (Repositor
|
||||
cond = cond.And(builder.Eq{"is_private": false})
|
||||
}
|
||||
|
||||
if len(opts.LowerNames) > 0 {
|
||||
cond = cond.And(builder.In("lower_name", opts.LowerNames))
|
||||
}
|
||||
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
count, err := sess.Where(cond).Count(new(Repository))
|
||||
|
||||
@@ -485,6 +485,19 @@ func TestSearchRepositoryByTopicName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindUserCodeAccessibleRepoIDsRequiresCodeUnit(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
publicOnlyUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
publicOnlyUser.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialAccessToken, 1, "public-only,read:repository")
|
||||
for _, user := range []*user_model.User{nil, publicOnlyUser} {
|
||||
repoIDs, err := repo_model.FindUserCodeAccessibleRepoIDs(t.Context(), user)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, repoIDs, int64(1))
|
||||
assert.NotContains(t, repoIDs, int64(50))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindUserActionsAccessibleOwnerRepoIDs(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
// user2 is on org3's owner team, so it can access org3's private repo3 (which has the actions unit)
|
||||
@@ -492,6 +505,7 @@ func TestFindUserActionsAccessibleOwnerRepoIDs(t *testing.T) {
|
||||
|
||||
// org3 is a public org owning repo3 (private) and repo32 (public), both with the actions unit
|
||||
const orgID = 3
|
||||
require.NoError(t, db.Insert(t.Context(), &repo_model.RepoUnit{RepoID: 32, Type: unit.TypeActions, Config: &repo_model.ActionsConfig{}}))
|
||||
|
||||
all, err := repo_model.SearchRepositoryIDsByCondition(t.Context(), repo_model.UserActionsAccessibleOwnerRepoCond(orgID, user))
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -56,12 +56,11 @@ func init() {
|
||||
// UnitConfig describes common unit config
|
||||
type UnitConfig struct{}
|
||||
|
||||
// FromDB fills up a UnitConfig from serialized format.
|
||||
func (cfg *UnitConfig) FromDB(bs []byte) error {
|
||||
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
db.UnmarshalFromDB(bs, cfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDB exports a UnitConfig to a serialized format.
|
||||
func (cfg *UnitConfig) ToDB() ([]byte, error) {
|
||||
return json.Marshal(cfg)
|
||||
}
|
||||
@@ -71,12 +70,11 @@ type ExternalWikiConfig struct {
|
||||
ExternalWikiURL string
|
||||
}
|
||||
|
||||
// FromDB fills up a ExternalWikiConfig from serialized format.
|
||||
func (cfg *ExternalWikiConfig) FromDB(bs []byte) error {
|
||||
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
db.UnmarshalFromDB(bs, cfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDB exports a ExternalWikiConfig to a serialized format.
|
||||
func (cfg *ExternalWikiConfig) ToDB() ([]byte, error) {
|
||||
return json.Marshal(cfg)
|
||||
}
|
||||
@@ -89,29 +87,48 @@ type ExternalTrackerConfig struct {
|
||||
ExternalTrackerRegexpPattern string
|
||||
}
|
||||
|
||||
// FromDB fills up a ExternalTrackerConfig from serialized format.
|
||||
func (cfg *ExternalTrackerConfig) FromDB(bs []byte) error {
|
||||
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
db.UnmarshalFromDB(bs, cfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDB exports a ExternalTrackerConfig to a serialized format.
|
||||
func (cfg *ExternalTrackerConfig) ToDB() ([]byte, error) {
|
||||
return json.Marshal(cfg)
|
||||
}
|
||||
|
||||
const (
|
||||
// CreationRestrictionReader allows any user with read access to the unit to create issues/pull requests (default, legacy behavior)
|
||||
CreationRestrictionReader = "reader"
|
||||
// CreationRestrictionMember restricts creation to effective collaborators (owner, team members, explicit collaborators) with at least read access
|
||||
CreationRestrictionMember = "member"
|
||||
// CreationRestrictionWriter restricts creation to users with at least write access
|
||||
CreationRestrictionWriter = "writer"
|
||||
)
|
||||
|
||||
// IssuesConfig describes issues config
|
||||
type IssuesConfig struct {
|
||||
EnableTimetracker bool
|
||||
AllowOnlyContributorsToTrackTime bool
|
||||
EnableDependencies bool
|
||||
CreationRestriction string
|
||||
}
|
||||
|
||||
func DefaultIssuesConfig() *IssuesConfig {
|
||||
return &IssuesConfig{
|
||||
EnableTimetracker: setting.Service.DefaultEnableTimetracking,
|
||||
AllowOnlyContributorsToTrackTime: setting.Service.DefaultAllowOnlyContributorsToTrackTime,
|
||||
EnableDependencies: setting.Service.DefaultEnableDependencies,
|
||||
CreationRestriction: CreationRestrictionReader,
|
||||
}
|
||||
}
|
||||
|
||||
// FromDB fills up a IssuesConfig from serialized format.
|
||||
func (cfg *IssuesConfig) FromDB(bs []byte) error {
|
||||
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
*cfg = *DefaultIssuesConfig()
|
||||
db.UnmarshalFromDB(bs, cfg)
|
||||
cfg.CreationRestriction = util.IfZero(cfg.CreationRestriction, CreationRestrictionReader)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDB exports a IssuesConfig to a serialized format.
|
||||
func (cfg *IssuesConfig) ToDB() ([]byte, error) {
|
||||
return json.Marshal(cfg)
|
||||
}
|
||||
@@ -134,6 +151,7 @@ type PullRequestsConfig struct {
|
||||
DefaultSquashMergeMessage SquashMergeMessage
|
||||
DefaultAllowMaintainerEdit bool
|
||||
DefaultTargetBranch string
|
||||
CreationRestriction string
|
||||
}
|
||||
|
||||
func DefaultPullRequestsConfig() *PullRequestsConfig {
|
||||
@@ -147,6 +165,7 @@ func DefaultPullRequestsConfig() *PullRequestsConfig {
|
||||
AllowRebaseUpdate: true,
|
||||
DefaultUpdateStyle: UpdateStyleMerge,
|
||||
DefaultAllowMaintainerEdit: true,
|
||||
CreationRestriction: CreationRestrictionReader,
|
||||
}
|
||||
cfg.DefaultDeleteBranchAfterMerge = setting.Repository.PullRequest.DefaultDeleteBranchAfterMerge
|
||||
cfg.DefaultMergeStyle = MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle)
|
||||
@@ -155,16 +174,15 @@ func DefaultPullRequestsConfig() *PullRequestsConfig {
|
||||
return cfg
|
||||
}
|
||||
|
||||
// FromDB fills up a PullRequestsConfig from serialized format.
|
||||
func (cfg *PullRequestsConfig) FromDB(bs []byte) error {
|
||||
// set default values for existing PullRequestConfig in DB
|
||||
*cfg = *DefaultPullRequestsConfig()
|
||||
_ = json.UnmarshalHandleDoubleEncode(bs, &cfg) // don't let corrupted database value cause unnecessary 500 error
|
||||
db.UnmarshalFromDB(bs, cfg)
|
||||
cfg.DefaultUpdateStyle = util.IfZero(cfg.DefaultUpdateStyle, UpdateStyleMerge)
|
||||
cfg.CreationRestriction = util.IfZero(cfg.CreationRestriction, CreationRestrictionReader)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDB exports a PullRequestsConfig to a serialized format.
|
||||
func (cfg *PullRequestsConfig) ToDB() ([]byte, error) {
|
||||
return json.Marshal(cfg)
|
||||
}
|
||||
@@ -228,34 +246,18 @@ type ProjectsConfig struct {
|
||||
ProjectsMode ProjectsMode
|
||||
}
|
||||
|
||||
// FromDB fills up a ProjectsConfig from serialized format.
|
||||
func (cfg *ProjectsConfig) FromDB(bs []byte) error {
|
||||
// TODO: remove GetProjectsMode, only use ProjectsMode
|
||||
cfg.ProjectsMode = ProjectsModeAll
|
||||
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
db.UnmarshalFromDB(bs, cfg)
|
||||
cfg.ProjectsMode = util.IfZero(cfg.ProjectsMode, ProjectsModeAll)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDB exports a ProjectsConfig to a serialized format.
|
||||
func (cfg *ProjectsConfig) ToDB() ([]byte, error) {
|
||||
return json.Marshal(cfg)
|
||||
}
|
||||
|
||||
func (cfg *ProjectsConfig) GetProjectsMode() ProjectsMode {
|
||||
if cfg.ProjectsMode != "" {
|
||||
return cfg.ProjectsMode
|
||||
}
|
||||
|
||||
return ProjectsModeAll
|
||||
}
|
||||
|
||||
func (cfg *ProjectsConfig) IsProjectsAllowed(m ProjectsMode) bool {
|
||||
projectsMode := cfg.GetProjectsMode()
|
||||
|
||||
if m == ProjectsModeNone {
|
||||
return true
|
||||
}
|
||||
|
||||
return projectsMode == m || projectsMode == ProjectsModeAll
|
||||
return m == ProjectsModeNone || cfg.ProjectsMode == m || cfg.ProjectsMode == ProjectsModeAll
|
||||
}
|
||||
|
||||
// BeforeSet is invoked from XORM before setting the value of a field of this object.
|
||||
|
||||
@@ -6,6 +6,7 @@ package repo
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/perm"
|
||||
"gitea.dev/models/unit"
|
||||
"gitea.dev/modules/json"
|
||||
@@ -173,14 +174,12 @@ func (cfg *ActionsConfig) ClampPermissions(perms ActionsTokenPermissions) Action
|
||||
return ClampActionsTokenPermissions(perms, maxPerms)
|
||||
}
|
||||
|
||||
// FromDB fills up a ActionsConfig from serialized format.
|
||||
func (cfg *ActionsConfig) FromDB(bs []byte) error {
|
||||
_ = json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
db.UnmarshalFromDB(bs, cfg)
|
||||
cfg.TokenPermissionMode, _ = util.EnumValue(cfg.TokenPermissionMode)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDB exports a ActionsConfig to a serialized format.
|
||||
func (cfg *ActionsConfig) ToDB() ([]byte, error) {
|
||||
return json.Marshal(cfg)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/container"
|
||||
@@ -22,7 +23,9 @@ func init() {
|
||||
db.RegisterModel(new(RepoTopic))
|
||||
}
|
||||
|
||||
var topicPattern = regexp.MustCompile(`^[a-z0-9][-.a-z0-9]*$`)
|
||||
var topicPattern = sync.OnceValue(func() *regexp.Regexp {
|
||||
return regexp.MustCompile(`^[a-z0-9][-.a-z0-9]*$`)
|
||||
})
|
||||
|
||||
// Topic represents a topic of repositories
|
||||
type Topic struct {
|
||||
@@ -55,7 +58,7 @@ func (err ErrTopicNotExist) Unwrap() error {
|
||||
|
||||
// ValidateTopic checks a topic by length and match pattern rules
|
||||
func ValidateTopic(topic string) bool {
|
||||
return len(topic) <= 35 && topicPattern.MatchString(topic)
|
||||
return len(topic) <= 35 && topicPattern().MatchString(topic)
|
||||
}
|
||||
|
||||
// SanitizeAndValidateTopics sanitizes and checks an array or topics
|
||||
|
||||
@@ -18,7 +18,9 @@ var (
|
||||
ErrBlockOrganization = util.NewInvalidArgumentErrorf("cannot block an organization")
|
||||
ErrCanNotBlock = util.NewInvalidArgumentErrorf("cannot block the user")
|
||||
ErrCanNotUnblock = util.NewInvalidArgumentErrorf("cannot unblock the user")
|
||||
ErrBlockedUser = util.NewPermissionDeniedErrorf("user is blocked")
|
||||
|
||||
// ErrBlockedUser is for end users: the doer is blocked by the repo owner, or an assignee when creating an issue, etc
|
||||
ErrBlockedUser = util.NewPermissionDeniedErrorf("you are blocked by the user")
|
||||
)
|
||||
|
||||
type Blocking struct {
|
||||
|
||||
@@ -43,7 +43,12 @@ func IsErrUserNotExist(err error) bool {
|
||||
}
|
||||
|
||||
func (err ErrUserNotExist) Error() string {
|
||||
ret := fmt.Sprintf("user does not exist [uid: %d, name: %s]", err.UID, err.Name)
|
||||
ret := "user does not exist"
|
||||
if err.UID != 0 {
|
||||
ret += fmt.Sprintf(" [id: %d]", err.UID)
|
||||
} else if err.Name != "" {
|
||||
ret += fmt.Sprintf(" [name: %s]", err.Name)
|
||||
}
|
||||
if err.ExtraMsg != "" {
|
||||
ret += ": " + err.ExtraMsg
|
||||
}
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// UserOpenID is the list of all OpenID identities of a user.
|
||||
// Since this is a middle table, name it OpenID is not suitable, so we ignore the lint here
|
||||
type UserOpenID struct { //revive:disable-line:exported
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UID int64 `xorm:"INDEX NOT NULL"`
|
||||
URI string `xorm:"UNIQUE NOT NULL"`
|
||||
Show bool `xorm:"DEFAULT false"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(UserOpenID))
|
||||
}
|
||||
|
||||
// GetUserOpenIDs returns all openid addresses that belongs to given user.
|
||||
func GetUserOpenIDs(ctx context.Context, uid int64) ([]*UserOpenID, error) {
|
||||
openids := make([]*UserOpenID, 0, 5)
|
||||
if err := db.GetEngine(ctx).
|
||||
Where("uid=?", uid).
|
||||
Asc("id").
|
||||
Find(&openids); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return openids, nil
|
||||
}
|
||||
|
||||
// isOpenIDUsed returns true if the openid has been used.
|
||||
func isOpenIDUsed(ctx context.Context, uri string) (bool, error) {
|
||||
if len(uri) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return db.Exist[UserOpenID](ctx, builder.Eq{"uri": uri})
|
||||
}
|
||||
|
||||
// ErrOpenIDAlreadyUsed represents a "OpenIDAlreadyUsed" kind of error.
|
||||
type ErrOpenIDAlreadyUsed struct {
|
||||
OpenID string
|
||||
}
|
||||
|
||||
// IsErrOpenIDAlreadyUsed checks if an error is a ErrOpenIDAlreadyUsed.
|
||||
func IsErrOpenIDAlreadyUsed(err error) bool {
|
||||
_, ok := err.(ErrOpenIDAlreadyUsed)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrOpenIDAlreadyUsed) Error() string {
|
||||
return fmt.Sprintf("OpenID already in use [oid: %s]", err.OpenID)
|
||||
}
|
||||
|
||||
func (err ErrOpenIDAlreadyUsed) Unwrap() error {
|
||||
return util.ErrAlreadyExist
|
||||
}
|
||||
|
||||
// AddUserOpenID adds an pre-verified/normalized OpenID URI to given user.
|
||||
// NOTE: make sure openid.URI is normalized already
|
||||
func AddUserOpenID(ctx context.Context, openid *UserOpenID) error {
|
||||
used, err := isOpenIDUsed(ctx, openid.URI)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if used {
|
||||
return ErrOpenIDAlreadyUsed{openid.URI}
|
||||
}
|
||||
|
||||
return db.Insert(ctx, openid)
|
||||
}
|
||||
|
||||
// GetUserOpenIDByID returns the OpenID with the given ID owned by uid.
|
||||
func GetUserOpenIDByID(ctx context.Context, id, uid int64) (*UserOpenID, error) {
|
||||
oid, has, err := db.Get[UserOpenID](ctx, builder.Eq{"id": id, "uid": uid})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, util.NewNotExistErrorf("OpenID is unknown")
|
||||
}
|
||||
return oid, nil
|
||||
}
|
||||
|
||||
// DeleteUserOpenID deletes an openid address of given user.
|
||||
func DeleteUserOpenID(ctx context.Context, openid *UserOpenID) (err error) {
|
||||
var deleted int64
|
||||
// ask to check UID
|
||||
address := UserOpenID{
|
||||
UID: openid.UID,
|
||||
}
|
||||
if openid.ID > 0 {
|
||||
deleted, err = db.GetEngine(ctx).ID(openid.ID).Delete(&address)
|
||||
} else {
|
||||
deleted, err = db.GetEngine(ctx).
|
||||
Where("openid=?", openid.URI).
|
||||
Delete(&address)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if deleted != 1 {
|
||||
return util.NewNotExistErrorf("OpenID is unknown")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToggleUserOpenIDVisibility toggles visibility of an openid address of given user.
|
||||
func ToggleUserOpenIDVisibility(ctx context.Context, id int64, user *User) error {
|
||||
affected, err := db.GetEngine(ctx).Exec("update `user_open_id` set `show` = not `show` where `id` = ? AND uid = ?", id, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n, _ := affected.RowsAffected(); n != 1 {
|
||||
return util.NewNotExistErrorf("OpenID is unknown")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUserOpenIDs(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
t.Run("GetUserOpenIDs", func(t *testing.T) {
|
||||
oids, err := user_model.GetUserOpenIDs(t.Context(), int64(1))
|
||||
if assert.NoError(t, err) && assert.Len(t, oids, 2) {
|
||||
assert.Equal(t, "https://user1.domain1.tld/", oids[0].URI)
|
||||
assert.False(t, oids[0].Show)
|
||||
assert.Equal(t, "http://user1.domain2.tld/", oids[1].URI)
|
||||
assert.True(t, oids[1].Show)
|
||||
}
|
||||
|
||||
oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2))
|
||||
if assert.NoError(t, err) && assert.Len(t, oids, 1) {
|
||||
assert.Equal(t, "https://domain1.tld/user2/", oids[0].URI)
|
||||
assert.True(t, oids[0].Show)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ToggleUserOpenIDVisibilityRequiresOwnership", func(t *testing.T) {
|
||||
unauthorizedUser, err := user_model.GetUserByID(t.Context(), int64(2))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = user_model.ToggleUserOpenIDVisibility(t.Context(), int64(1), unauthorizedUser)
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||
})
|
||||
|
||||
t.Run("ToggleUserOpenIDVisibility", func(t *testing.T) {
|
||||
user, err := user_model.GetUserByID(t.Context(), int64(2))
|
||||
require.NoError(t, err)
|
||||
oids, err := user_model.GetUserOpenIDs(t.Context(), int64(2))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, oids, 1)
|
||||
assert.True(t, oids[0].Show)
|
||||
|
||||
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, oids, 1)
|
||||
|
||||
assert.False(t, oids[0].Show)
|
||||
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2))
|
||||
require.NoError(t, err)
|
||||
if assert.Len(t, oids, 1) {
|
||||
assert.True(t, oids[0].Show)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/auth/openid"
|
||||
"gitea.dev/modules/auth/password/hash"
|
||||
"gitea.dev/modules/base"
|
||||
"gitea.dev/modules/container"
|
||||
@@ -838,10 +837,7 @@ func createUser(ctx context.Context, u *User, meta *Meta, createdByAdmin bool, o
|
||||
}
|
||||
|
||||
// trim user agent string to a reasonable length, if necessary
|
||||
userAgent := strings.TrimSpace(meta.InitialUserAgent)
|
||||
if len(userAgent) > 255 {
|
||||
userAgent = userAgent[:255]
|
||||
}
|
||||
userAgent := util.TruncateRunes(strings.TrimSpace(meta.InitialUserAgent), 255)
|
||||
if err = SetUserSetting(ctx, u.ID, SignupUserAgent, userAgent); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1441,32 +1437,6 @@ func GetIndividualUserByLoginSource(ctx context.Context, loginType auth.Type, lo
|
||||
return user, has, err
|
||||
}
|
||||
|
||||
// GetUserByOpenID returns the user object by given OpenID if exists.
|
||||
func GetUserByOpenID(ctx context.Context, uri string) (*User, error) {
|
||||
if len(uri) == 0 {
|
||||
return nil, ErrUserNotExist{Name: uri}
|
||||
}
|
||||
|
||||
uri, err := openid.Normalize(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Trace("Normalized OpenID URI: " + uri)
|
||||
|
||||
// Otherwise, check in openid table
|
||||
oid := &UserOpenID{}
|
||||
has, err := db.GetEngine(ctx).Where("uri=?", uri).Get(oid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if has {
|
||||
return GetUserByID(ctx, oid.UID)
|
||||
}
|
||||
|
||||
return nil, ErrUserNotExist{Name: uri}
|
||||
}
|
||||
|
||||
// GetAdminUser returns the first administrator
|
||||
func GetAdminUser(ctx context.Context) (*User, error) {
|
||||
var admin User
|
||||
|
||||
@@ -417,25 +417,6 @@ func TestNewUserRedirect(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserByOpenID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
_, err := user_model.GetUserByOpenID(t.Context(), "https://unknown")
|
||||
if assert.Error(t, err) {
|
||||
assert.True(t, user_model.IsErrUserNotExist(err))
|
||||
}
|
||||
|
||||
user, err := user_model.GetUserByOpenID(t.Context(), "https://user1.domain1.tld")
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, int64(1), user.ID)
|
||||
}
|
||||
|
||||
user, err = user_model.GetUserByOpenID(t.Context(), "https://domain1.tld/user2/")
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, int64(2), user.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFollowUser(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: Step with leading new line
|
||||
'on':
|
||||
"on":
|
||||
push:
|
||||
jobs:
|
||||
test:
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
|
||||
@@ -75,7 +76,9 @@ func ParseWorkflowCallConfig(content []byte) (*model.WorkflowCall, error) {
|
||||
const SecretsInherit = "inherit"
|
||||
|
||||
// callerSecretValueRegexp matches the `${{ secrets.NAME }}` form expected for each value in a caller's `secrets:` mapping.
|
||||
var callerSecretValueRegexp = regexp.MustCompile(`^\s*\$\{\{\s*secrets\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}\s*$`)
|
||||
var callerSecretValueRegexp = sync.OnceValue(func() *regexp.Regexp {
|
||||
return regexp.MustCompile(`^\s*\$\{\{\s*secrets\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}\s*$`)
|
||||
})
|
||||
|
||||
// ParseCallerSecrets decodes a caller's "secrets:" YAML node into one of two forms:
|
||||
// - inherit == true: the caller wrote `secrets: inherit`; mapping is nil
|
||||
@@ -101,7 +104,7 @@ func ParseCallerSecrets(node yaml.Node) (inherit bool, mapping map[string]string
|
||||
if err := v.Decode(&sv); err != nil {
|
||||
return false, nil, fmt.Errorf("decode secret %q: %w", k.Value, err)
|
||||
}
|
||||
matches := callerSecretValueRegexp.FindStringSubmatch(sv)
|
||||
matches := callerSecretValueRegexp().FindStringSubmatch(sv)
|
||||
if len(matches) != 2 {
|
||||
return false, nil, fmt.Errorf("caller secret %q value must be of the form ${{ secrets.NAME }}", k.Value)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"gitea.dev/models/dbfs"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/storage"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/zstd"
|
||||
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
@@ -213,10 +214,7 @@ func OpenLogs(ctx context.Context, inStorage bool, filename string) (io.ReadSeek
|
||||
func FormatLog(timestamp time.Time, content string) string {
|
||||
// Content shouldn't contain new line, it will break log indexes, other control chars are safe.
|
||||
content = strings.ReplaceAll(content, "\n", `\n`)
|
||||
if len(content) > MaxLineSize {
|
||||
content = content[:MaxLineSize]
|
||||
}
|
||||
return fmt.Sprintf("%s %s", timestamp.UTC().Format(timeFormat), content)
|
||||
return fmt.Sprintf("%s %s", timestamp.UTC().Format(timeFormat), util.TruncateStringBytes(content, MaxLineSize))
|
||||
}
|
||||
|
||||
func ParseLog(in string) (time.Time, string, error) {
|
||||
|
||||
@@ -165,11 +165,12 @@ func ScopedWorkflowStatusContextName(prefix, displayName, jobName, event string)
|
||||
return strings.TrimSpace(fmt.Sprintf("%s: %s", prefix, WorkflowStatusContextName(displayName, jobName, event)))
|
||||
}
|
||||
|
||||
// ShouldEventCreateCommitStatus reports whether a run triggered by the given workflow `on:` event posts a commit status,
|
||||
// IsPullRequestCheckEvent reports whether a run triggered by the given workflow `on:` event posts a commit status that pull requests count,
|
||||
// so its context can serve as a required status check.
|
||||
// TODO: this allowlist duplicates the truth in services/actions.getCommitStatusEventNameAndCommitID, which decides the actual event string and whether a status is posted.
|
||||
// The two are kept in sync by hand and can drift; unify them into a single source so adding a status-producing event in one place automatically updates the other.
|
||||
func ShouldEventCreateCommitStatus(event string) bool {
|
||||
// TODO: this allowlist duplicates the truth in services/actions.getCommitStatusEventNameAndCommitID, which decides the actual event string and whether a status is posted,
|
||||
// and in services/pull.GetLatestPullCommitStatuses, which decides whether pull requests count it.
|
||||
// They are kept in sync by hand and can drift, unify them into a single source so adding a status-producing event in one place automatically updates the others.
|
||||
func IsPullRequestCheckEvent(event string) bool {
|
||||
switch event {
|
||||
case "push", "pull_request", "pull_request_target", "pull_request_review", "pull_request_review_comment", "release":
|
||||
return true
|
||||
@@ -510,27 +511,11 @@ func matchIssuesEvent(issuePayload *api.IssuePayload, evt *jobparser.Event) bool
|
||||
// See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issues
|
||||
// Actions with the same name:
|
||||
// opened, edited, closed, reopened, assigned, unassigned, milestoned, demilestoned
|
||||
// Actions need to be converted:
|
||||
// label_updated -> labeled (when adding) or unlabeled (when removing)
|
||||
// label_cleared -> unlabeled
|
||||
// Actions need to be converted: see toActivityTypes
|
||||
// Unsupported activity types:
|
||||
// deleted, transferred, pinned, unpinned, locked, unlocked
|
||||
|
||||
actions := []string{}
|
||||
switch issuePayload.Action {
|
||||
case api.HookIssueLabelUpdated:
|
||||
if len(issuePayload.Changes.AddedLabels) > 0 {
|
||||
actions = append(actions, "labeled")
|
||||
}
|
||||
if len(issuePayload.Changes.RemovedLabels) > 0 {
|
||||
actions = append(actions, "unlabeled")
|
||||
}
|
||||
case api.HookIssueLabelCleared:
|
||||
actions = append(actions, "unlabeled")
|
||||
default:
|
||||
actions = append(actions, string(issuePayload.Action))
|
||||
}
|
||||
|
||||
actions := toActivityTypes(issuePayload.Action, issuePayload.Changes)
|
||||
for _, val := range vals {
|
||||
if matchActivityType(val, actions...) {
|
||||
matchTimes++
|
||||
@@ -557,25 +542,14 @@ func matchPullRequestEvent(ctx context.Context, gitRepo *git.Repository, commit
|
||||
// See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
|
||||
// Actions with the same name:
|
||||
// opened, edited, closed, reopened, assigned, unassigned, review_requested, review_request_removed, milestoned, demilestoned
|
||||
// Actions need to be converted:
|
||||
// synchronized -> synchronize
|
||||
// label_updated -> labeled
|
||||
// label_cleared -> unlabeled
|
||||
// Actions need to be converted: see toActivityTypes
|
||||
// Unsupported activity types:
|
||||
// converted_to_draft, ready_for_review, locked, unlocked, auto_merge_enabled, auto_merge_disabled, enqueued, dequeued
|
||||
|
||||
action := prPayload.Action
|
||||
switch action {
|
||||
case api.HookIssueSynchronized:
|
||||
action = "synchronize"
|
||||
case api.HookIssueLabelUpdated:
|
||||
action = "labeled"
|
||||
case api.HookIssueLabelCleared:
|
||||
action = "unlabeled"
|
||||
}
|
||||
log.Trace("matching pull_request %s with %v", action, vals)
|
||||
actions := toActivityTypes(prPayload.Action, prPayload.Changes)
|
||||
log.Trace("matching pull_request %v with %v", actions, vals)
|
||||
for _, val := range vals {
|
||||
if matchActivityType(val, string(action)) {
|
||||
if matchActivityType(val, actions...) {
|
||||
activityTypeMatched = true
|
||||
matchTimes++
|
||||
break
|
||||
@@ -587,7 +561,7 @@ func matchPullRequestEvent(ctx context.Context, gitRepo *git.Repository, commit
|
||||
headCommit = commit
|
||||
err error
|
||||
)
|
||||
if evt.Name == GithubEventPullRequestTarget && (len(acts["paths"]) > 0 || len(acts["paths-ignore"]) > 0) {
|
||||
if (len(acts["paths"]) > 0 || len(acts["paths-ignore"]) > 0) && commit.ID.String() != prPayload.PullRequest.Head.Sha {
|
||||
headCommit, err = gitRepo.GetCommit(ctx, prPayload.PullRequest.Head.Sha)
|
||||
if err != nil {
|
||||
log.Error("GetCommit [ref: %s]: %v", prPayload.PullRequest.Head.Sha, err)
|
||||
@@ -894,6 +868,27 @@ func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event
|
||||
return matchTimes == len(evt.Acts())
|
||||
}
|
||||
|
||||
// toActivityTypes returns several types because one label update can both add and remove labels
|
||||
func toActivityTypes(action api.HookIssueAction, changes *api.ChangesPayload) []string {
|
||||
switch action {
|
||||
case api.HookIssueSynchronized:
|
||||
return []string{"synchronize"}
|
||||
case api.HookIssueLabelUpdated:
|
||||
var types []string
|
||||
if len(changes.AddedLabels) > 0 {
|
||||
types = append(types, "labeled")
|
||||
}
|
||||
if len(changes.RemovedLabels) > 0 {
|
||||
types = append(types, "unlabeled")
|
||||
}
|
||||
return types
|
||||
case api.HookIssueLabelCleared:
|
||||
return []string{"unlabeled"}
|
||||
default:
|
||||
return []string{string(action)}
|
||||
}
|
||||
}
|
||||
|
||||
// matchActivityType treats an invalid user-provided pattern as no match instead of panicking
|
||||
func matchActivityType(pattern string, actions ...string) bool {
|
||||
g, err := glob.Compile(pattern, '/')
|
||||
|
||||
@@ -185,10 +185,17 @@ func TestDetectMatched(t *testing.T) {
|
||||
{
|
||||
desc: "HookEventPullRequest(pull_request) `label_updated` action matches GithubEventPullRequest(pull_request) with `label` activity type",
|
||||
triggedEvent: webhook_module.HookEventPullRequest,
|
||||
payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated},
|
||||
payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated, Changes: &api.ChangesPayload{AddedLabels: []*api.Label{{ID: 1, Name: "label"}}}},
|
||||
yamlOn: "on:\n pull_request:\n types: [labeled]",
|
||||
expected: detectMatched,
|
||||
},
|
||||
{
|
||||
desc: "HookEventPullRequestLabel(pull_request_label) label removal doesn't match GithubEventPullRequest(pull_request) with `labeled` activity type",
|
||||
triggedEvent: webhook_module.HookEventPullRequestLabel,
|
||||
payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated, Changes: &api.ChangesPayload{RemovedLabels: []*api.Label{{ID: 1}}}},
|
||||
yamlOn: "on:\n pull_request:\n types: [labeled]",
|
||||
expected: detectNotApplicable,
|
||||
},
|
||||
{
|
||||
desc: "HookEventPullRequestReviewComment(pull_request_review_comment) matches GithubEventPullRequestReviewComment(pull_request_review_comment)",
|
||||
triggedEvent: webhook_module.HookEventPullRequestReviewComment,
|
||||
|
||||
@@ -6,6 +6,7 @@ package assetfs
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
@@ -301,12 +302,12 @@ var GetExecutableModTime = sync.OnceValue(func() (modTime time.Time) {
|
||||
return st.ModTime()
|
||||
})
|
||||
|
||||
func GenerateEmbedBindata(fsRootPath, outputFile string) error {
|
||||
func GenerateEmbedBindata(fsRootPath, outputFile string) (err error) {
|
||||
output, err := os.OpenFile(outputFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer output.Close()
|
||||
defer func() { err = errors.Join(err, output.Close()) }()
|
||||
|
||||
meta := &EmbeddedMeta{}
|
||||
meta.Root = &embeddedFileInfo{}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package openid
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/yohcop/openid-go"
|
||||
)
|
||||
|
||||
type timedDiscoveredInfo struct {
|
||||
info openid.DiscoveredInfo
|
||||
time time.Time
|
||||
}
|
||||
|
||||
type timedDiscoveryCache struct {
|
||||
cache map[string]timedDiscoveredInfo
|
||||
ttl time.Duration
|
||||
mutex *sync.Mutex
|
||||
}
|
||||
|
||||
func newTimedDiscoveryCache(ttl time.Duration) *timedDiscoveryCache {
|
||||
return &timedDiscoveryCache{cache: map[string]timedDiscoveredInfo{}, ttl: ttl, mutex: &sync.Mutex{}}
|
||||
}
|
||||
|
||||
func (s *timedDiscoveryCache) Put(id string, info openid.DiscoveredInfo) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
s.cache[id] = timedDiscoveredInfo{info: info, time: time.Now()}
|
||||
}
|
||||
|
||||
// Delete timed-out cache entries
|
||||
func (s *timedDiscoveryCache) cleanTimedOut() {
|
||||
now := time.Now()
|
||||
for k, e := range s.cache {
|
||||
diff := now.Sub(e.time)
|
||||
if diff > s.ttl {
|
||||
delete(s.cache, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *timedDiscoveryCache) Get(id string) openid.DiscoveredInfo {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
// Delete old cached while we are at it.
|
||||
s.cleanTimedOut()
|
||||
|
||||
if info, has := s.cache[id]; has {
|
||||
return info.info
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package openid
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testDiscoveredInfo struct{}
|
||||
|
||||
func (s *testDiscoveredInfo) ClaimedID() string {
|
||||
return "claimedID"
|
||||
}
|
||||
|
||||
func (s *testDiscoveredInfo) OpEndpoint() string {
|
||||
return "opEndpoint"
|
||||
}
|
||||
|
||||
func (s *testDiscoveredInfo) OpLocalID() string {
|
||||
return "opLocalID"
|
||||
}
|
||||
|
||||
func TestTimedDiscoveryCache(t *testing.T) {
|
||||
ttl := 50 * time.Millisecond
|
||||
dc := newTimedDiscoveryCache(ttl)
|
||||
|
||||
// Put some initial values
|
||||
dc.Put("foo", &testDiscoveredInfo{}) // openid.opEndpoint: "a", openid.opLocalID: "b", openid.claimedID: "c"})
|
||||
|
||||
// Make sure we can retrieve them
|
||||
di := dc.Get("foo")
|
||||
require.NotNil(t, di)
|
||||
assert.Equal(t, "opEndpoint", di.OpEndpoint())
|
||||
assert.Equal(t, "opLocalID", di.OpLocalID())
|
||||
assert.Equal(t, "claimedID", di.ClaimedID())
|
||||
|
||||
// Attempt to get a non-existent value
|
||||
assert.Nil(t, dc.Get("bar"))
|
||||
|
||||
// Sleep for a while and try to retrieve again
|
||||
time.Sleep(ttl * 3 / 2)
|
||||
|
||||
assert.Nil(t, dc.Get("foo"))
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package openid
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/egress"
|
||||
|
||||
"github.com/yohcop/openid-go"
|
||||
)
|
||||
|
||||
// For the demo, we use in-memory infinite storage nonce and discovery
|
||||
// cache. In your app, do not use this as it will eat up memory and
|
||||
// never
|
||||
// free it. Use your own implementation, on a better database system.
|
||||
// If you have multiple servers for example, you may need to share at
|
||||
// least
|
||||
// the nonceStore between them.
|
||||
var (
|
||||
nonceStore = openid.NewSimpleNonceStore()
|
||||
discoveryCache = newTimedDiscoveryCache(24 * time.Hour)
|
||||
|
||||
// openIDInstance keeps user-supplied OpenID identifiers within [security] ALLOWED_HOST_LIST
|
||||
openIDInstance = sync.OnceValue(func() *openid.OpenID {
|
||||
return openid.NewOpenID(&http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: egress.NewSecurityPolicy("openid").NewHTTPTransport(),
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
// Verify handles response from OpenID provider
|
||||
func Verify(fullURL string) (id string, err error) {
|
||||
return openIDInstance().Verify(fullURL, discoveryCache, nonceStore)
|
||||
}
|
||||
|
||||
// Normalize normalizes an OpenID URI
|
||||
func Normalize(url string) (id string, err error) {
|
||||
return openid.Normalize(url)
|
||||
}
|
||||
|
||||
// RedirectURL redirects browser
|
||||
func RedirectURL(id, callbackURL, realm string) (string, error) {
|
||||
return openIDInstance().RedirectURL(id, callbackURL, realm)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package openid
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOpenIDDiscoveryBlocksInternalHost(t *testing.T) {
|
||||
var reached atomic.Bool
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
reached.Store(true)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
// RedirectURL performs server-side discovery of the identifier URL; a loopback URL
|
||||
// must be refused at dial time instead of reaching the internal server
|
||||
_, err := RedirectURL(srv.URL, "http://example.com/callback", "http://example.com/")
|
||||
require.Error(t, err)
|
||||
assert.False(t, reached.Load(), "OpenID discovery must not reach an internal/loopback host")
|
||||
}
|
||||
@@ -29,6 +29,9 @@ func TestAmbiguousCharacters(t *testing.T) {
|
||||
found := i < len(ambiguous.Confusable) && ambiguous.Confusable[i] == confusable
|
||||
assert.True(t, found, "%c is not in %d", confusable, i)
|
||||
}
|
||||
for r := rune(0x20); r < 0x7f; r++ {
|
||||
assert.False(t, unicode.Is(ambiguous.RangeTable, r), "%c must not be ambiguous, isStringAsciiSafe skips it", r)
|
||||
}
|
||||
}
|
||||
|
||||
var confusableTo rune
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user