mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-06 18:00:18 +09:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e28e79a648 | |||
| 6a0e125018 | |||
| 1c19e2ba43 |
@@ -0,0 +1,59 @@
|
||||
# The full repository name
|
||||
repo: go-gitea/gitea
|
||||
|
||||
# Service type (gitea or github)
|
||||
service: github
|
||||
|
||||
# Base URL for Gitea instance if using gitea service type (optional)
|
||||
# Default: https://gitea.com
|
||||
base-url:
|
||||
|
||||
# Changelog groups and which labeled PRs to add to each group
|
||||
groups:
|
||||
-
|
||||
name: BREAKING
|
||||
labels:
|
||||
- pr/breaking
|
||||
-
|
||||
name: SECURITY
|
||||
labels:
|
||||
- topic/security
|
||||
-
|
||||
name: FEATURES
|
||||
labels:
|
||||
- type/feature
|
||||
-
|
||||
name: ENHANCEMENTS
|
||||
labels:
|
||||
- type/enhancement
|
||||
-
|
||||
name: PERFORMANCE
|
||||
labels:
|
||||
- performance/memory
|
||||
- performance/speed
|
||||
- performance/bigrepo
|
||||
- performance/cpu
|
||||
-
|
||||
name: BUGFIXES
|
||||
labels:
|
||||
- type/bug
|
||||
|
||||
-
|
||||
name: TESTING
|
||||
labels:
|
||||
- type/testing
|
||||
-
|
||||
name: BUILD
|
||||
labels:
|
||||
- topic/build
|
||||
- topic/code-linting
|
||||
-
|
||||
name: DOCS
|
||||
labels:
|
||||
- type/docs
|
||||
-
|
||||
name: MISC
|
||||
default: true
|
||||
|
||||
# regex indicating which labels to skip for the changelog
|
||||
skip-labels: skip-changelog|backport\/.+
|
||||
@@ -1,59 +0,0 @@
|
||||
name: Release
|
||||
description: Track a Gitea release (for release managers).
|
||||
title: "Release Gitea "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Follow the [release management guide](https://github.com/go-gitea/gitea/blob/main/docs/release-management.md).
|
||||
Set the issue title and milestone to the version being released. Replace the examples below and mark inapplicable tasks as such.
|
||||
CI signs the tag, generates release notes, and publishes binaries and containers. Track verification here; no manual changelog PR or release upload is needed.
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
placeholder: "28.0.1"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: branch
|
||||
attributes:
|
||||
label: Release branch
|
||||
placeholder: "release/v28"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Release checklist
|
||||
description: Keep workflow runs, release URLs, and follow-up PRs alongside the relevant tasks.
|
||||
value: |
|
||||
### Preparation
|
||||
|
||||
- [ ] Resolve release blockers and confirm milestone issues and PRs are resolved or deferred.
|
||||
- [ ] Confirm required backports are merged and release branch CI passes.
|
||||
- [ ] For a new release line, create the release branch and tag its fork point on main with the next version's -dev tag.
|
||||
|
||||
### Release
|
||||
|
||||
- [ ] Run https://github.com/go-gitea/gitea/actions/workflows/release-create-tag.yml on the release branch with the selected version and obtain maintainer approval.
|
||||
- [ ] Confirm https://github.com/go-gitea/gitea/actions/workflows/release-tag-version.yml succeeds for the new tag (binaries and containers).
|
||||
- [ ] Verify the public GitHub release, generated notes, binary attachments, and signatures at https://github.com/go-gitea/gitea/releases.
|
||||
- [ ] Verify binaries and signatures at https://dl.gitea.com/gitea/ for this version.
|
||||
- [ ] Verify versioned regular and rootless images on Docker Hub and GHCR, and smoke-test the release.
|
||||
|
||||
### Follow-up
|
||||
|
||||
- [ ] Verify the automated https://dl.gitea.com/gitea/version.json update, where applicable to this release line.
|
||||
- [ ] Verify automated Helm chart and Terraform provider update PRs and follow up if needed.
|
||||
- [ ] Check Homebrew and Snap availability; record any outstanding packaging follow-up.
|
||||
- [ ] Confirm documentation reflects the release, where applicable.
|
||||
- [ ] Confirm and merge the release blog post, if planned: https://gitea.com/gitea/blog.
|
||||
- [ ] Announce the release in Discord #announcements.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: notes
|
||||
attributes:
|
||||
label: Blockers and notes
|
||||
description: Link outstanding work or release-specific checks using full URLs. Do not include undisclosed security details.
|
||||
@@ -34,7 +34,7 @@ runs:
|
||||
env:
|
||||
# pgsql is chosen to be the unlucky one to run with the slow "race detector", it is about 60% slower.
|
||||
GOTEST_FLAGS: -race -timeout=40m
|
||||
TAGS: bindata
|
||||
TAGS: bindata gogit
|
||||
TEST_LDAP: 1
|
||||
TEST_SHARD: ${{ inputs.shard }}
|
||||
TEST_TOTAL_SHARDS: ${{ inputs.total-shards }}
|
||||
|
||||
@@ -37,15 +37,22 @@ jobs:
|
||||
- uses: ./.github/actions/go-setup
|
||||
- run: make deps-backend deps-tools
|
||||
- run: TAGS="bindata" make backend
|
||||
- run: TAGS="bindata gogit" make backend
|
||||
- name: warm test compile cache (bindata)
|
||||
env:
|
||||
TAGS: bindata
|
||||
GOTEST_FLAGS: -race -list=^$$ -count=1
|
||||
run: make test-backend
|
||||
- name: warm test compile cache (bindata gogit)
|
||||
env:
|
||||
TAGS: bindata gogit
|
||||
GOTEST_FLAGS: -race -list=^$$ -count=1
|
||||
run: make test-backend
|
||||
- name: warm integration compile cache
|
||||
run: |
|
||||
TAGS="bindata" make test-integration-compile
|
||||
TAGS="bindata" GOTEST_FLAGS="-race" make test-integration-compile
|
||||
TAGS="bindata gogit" make test-integration-compile
|
||||
TAGS="bindata gogit" GOTEST_FLAGS="-race" make test-integration-compile
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -9,7 +9,7 @@ concurrency:
|
||||
group: cron-renovate
|
||||
|
||||
env:
|
||||
RENOVATE_VERSION: 44.121.4 # renovate: datasource=npm depName=renovate
|
||||
RENOVATE_VERSION: 44.109.1 # 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@6d26fcf0275dc65624cbc3d51fe78bc773f98321 # v46.3.6
|
||||
- uses: renovatebot/github-action@9fea9f0fbf80401026d11d03911d62b1f70fef1f # v46.3.3
|
||||
with:
|
||||
renovate-version: ${{ env.RENOVATE_VERSION }}
|
||||
configurationFile: renovate.json5
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: crowdin/github-action@9c23991700c0ec5256fd41089b9d9d7d540e424e # v3.3.0
|
||||
- uses: crowdin/github-action@df474cdfb9f41d6ae777118749477c2cdf7cacc8 # v3.2.0
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: go-gitea/giteabot@f48c6a15e0d384f037aea19cc05ff5e9551096b9 # v1.1.0
|
||||
- uses: go-gitea/giteabot@4c9d4d3fd913b5c35f59dcc0b004a3d11d5b22bf # v1.0.7
|
||||
with:
|
||||
github_token: ${{ secrets.GITEABOT_TOKEN }}
|
||||
gitea_fork: giteabot/gitea
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
name: giteabot-review
|
||||
|
||||
# Relays PR reviews to giteabot.yml through its workflow_run trigger, because review
|
||||
# runs on fork PRs get no secrets and a read-only token. The job itself does nothing.
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
types:
|
||||
- submitted
|
||||
- edited
|
||||
- dismissed
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
relay:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: "true"
|
||||
@@ -20,12 +20,13 @@ on:
|
||||
- closed
|
||||
- review_requested
|
||||
- review_request_removed
|
||||
# Reviews arrive through giteabot-review because fork PR review runs get no secrets
|
||||
workflow_run:
|
||||
workflows:
|
||||
- giteabot-review
|
||||
# Review events keep review-derived state such as lgtm labels and status checks
|
||||
# in sync after approvals, edits, or dismissals.
|
||||
pull_request_review:
|
||||
types:
|
||||
- requested
|
||||
- submitted
|
||||
- edited
|
||||
- dismissed
|
||||
# Periodic maintenance is still useful as a backstop for queue cleanup and
|
||||
# other housekeeping, even though main pushes now trigger it promptly.
|
||||
schedule:
|
||||
@@ -42,12 +43,12 @@ on:
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ format('{0}-{1}', github.workflow, github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number) || github.event_name == 'workflow_run' && format('review-{0}', github.event.workflow_run.head_sha) || 'maintenance') }}
|
||||
group: ${{ format('{0}-{1}', github.workflow, (github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review') && format('pr-{0}', github.event.pull_request.number) || 'maintenance') }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
giteabot:
|
||||
if: github.repository == 'go-gitea/gitea' && (github.event_name != 'workflow_run' || github.event.workflow_run.event == 'pull_request_review')
|
||||
if: github.repository == 'go-gitea/gitea'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
@@ -56,7 +57,9 @@ jobs:
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
steps:
|
||||
- uses: go-gitea/giteabot@f48c6a15e0d384f037aea19cc05ff5e9551096b9 # v1.1.0
|
||||
# pull_request_review runs without repository secrets on fork PRs, so fall
|
||||
# back to the workflow token for the non-backport checks handled here.
|
||||
- uses: go-gitea/giteabot@4c9d4d3fd913b5c35f59dcc0b004a3d11d5b22bf # v1.0.7
|
||||
with:
|
||||
github_token: ${{ secrets.GITEABOT_TOKEN }}
|
||||
github_token: ${{ secrets.GITEABOT_TOKEN || github.token }}
|
||||
checks: ${{ github.event.inputs.checks || 'labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }}
|
||||
|
||||
@@ -93,13 +93,13 @@ jobs:
|
||||
env:
|
||||
GOOS: linux
|
||||
GOARCH: arm64
|
||||
TAGS: bindata
|
||||
TAGS: bindata gogit
|
||||
- name: build-backend-windows
|
||||
run: go build -ldflags '-s -w' -o gitea-windows
|
||||
env:
|
||||
GOOS: windows
|
||||
GOARCH: amd64
|
||||
TAGS: bindata
|
||||
TAGS: bindata gogit
|
||||
- name: build-backend-386
|
||||
run: go build -ldflags '-s -w' -o gitea-linux-386
|
||||
env:
|
||||
|
||||
@@ -95,17 +95,17 @@ jobs:
|
||||
- run: make deps-backend
|
||||
- run: make backend
|
||||
env:
|
||||
TAGS: bindata
|
||||
TAGS: bindata gogit
|
||||
- run: GITEA_TEST_DATABASE=sqlite make test-migration
|
||||
env:
|
||||
TAGS: bindata
|
||||
TAGS: bindata gogit
|
||||
- name: run tests
|
||||
run: GITEA_TEST_DATABASE=sqlite make test-integration
|
||||
timeout-minutes: 50
|
||||
env:
|
||||
# sqlite driver can contain large amount of Golang code, so don't use race detector for it, otherwise, extremely slow
|
||||
GOTEST_FLAGS: -timeout=40m
|
||||
TAGS: bindata
|
||||
TAGS: bindata gogit
|
||||
|
||||
test-unit:
|
||||
if: needs.files-changed.outputs.backend == 'true'
|
||||
@@ -163,6 +163,13 @@ jobs:
|
||||
GOTEST_FLAGS: -race -timeout=20m
|
||||
TAGS: bindata
|
||||
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
|
||||
- name: unit-tests-gogit
|
||||
run: make test-backend
|
||||
env:
|
||||
GOTEST_FLAGS: -race -timeout=20m
|
||||
TAGS: bindata gogit
|
||||
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
|
||||
GITEA_TEST_CI_SKIP_EXTERNAL: true
|
||||
- run: make test-check
|
||||
|
||||
test-mysql:
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
name: release-create-tag
|
||||
run-name: Release v${{ inputs.version }} from ${{ github.ref_name }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Version to release, for example 28.0.1
|
||||
required: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
tag:
|
||||
runs-on: ubuntu-latest
|
||||
environment: release-signing
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
- uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
||||
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||
git_user_signingkey: true
|
||||
git_committer_email: teabot@gitea.io
|
||||
- env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
[[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
|
||||
git tag -s -m "v$VERSION" "v$VERSION"
|
||||
git push origin tag "v$VERSION"
|
||||
@@ -0,0 +1,149 @@
|
||||
name: release-tag-rc
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v[0-9]*-rc*"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
binary:
|
||||
runs-on: namespace-profile-gitea-release-binary
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
cache: false
|
||||
- uses: ./.github/actions/node-setup
|
||||
- run: make deps-frontend deps-backend
|
||||
- run: make release
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
- name: import gpg key
|
||||
id: import_gpg
|
||||
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
||||
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||
- name: sign binaries
|
||||
env:
|
||||
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
|
||||
GPG_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||
run: |
|
||||
for f in dist/release/*; do
|
||||
cosign sign-blob "$f" --bundle "$f.sigstore.json" --yes
|
||||
echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f"
|
||||
done
|
||||
# clean branch name to get the folder name in the object storage
|
||||
- name: Get cleaned branch name
|
||||
id: clean_name
|
||||
env:
|
||||
REF: ${{ github.ref }}
|
||||
run: |
|
||||
REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//')
|
||||
echo "Cleaned name is ${REF_NAME}"
|
||||
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
- name: upload binaries to cloudflare r2
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: auto
|
||||
CLOUDFLARE_R2_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_R2_ACCOUNT_ID }}
|
||||
CLOUDFLARE_R2_BUCKET: ${{ secrets.CLOUDFLARE_R2_BUCKET }}
|
||||
BRANCH: ${{ steps.clean_name.outputs.branch }}
|
||||
run: |
|
||||
aws s3 sync dist/release "s3://$CLOUDFLARE_R2_BUCKET/gitea/$BRANCH" --endpoint-url "https://$CLOUDFLARE_R2_ACCOUNT_ID.r2.cloudflarestorage.com" --no-progress
|
||||
- name: Install GH CLI
|
||||
uses: dev-hanz-ops/install-gh-cli-action@6089bdde54118ad7ca3d22053eb2d69387fd2779 # v0.3.0
|
||||
with:
|
||||
gh-cli-version: 2.39.1
|
||||
- name: create github release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
gh release create "$TAG" --title "$TAG" --draft --notes-from-tag dist/release/*
|
||||
|
||||
container:
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||
with:
|
||||
cache-image: false
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
id: meta
|
||||
with:
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
flavor: |
|
||||
latest=false
|
||||
# 1.2.3-rc0
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
id: meta_rootless
|
||||
with:
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
# each tag below will have the suffix of -rootless
|
||||
flavor: |
|
||||
latest=false
|
||||
suffix=-rootless
|
||||
# 1.2.3-rc0
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: build regular container image
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
- name: build rootless container image
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
push: true
|
||||
file: Dockerfile.rootless
|
||||
tags: ${{ steps.meta_rootless.outputs.tags }}
|
||||
annotations: ${{ steps.meta_rootless.outputs.annotations }}
|
||||
@@ -4,7 +4,8 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- "v[0-9]*"
|
||||
- "!v[0-9]*-*"
|
||||
- "!v[0-9]*-rc*"
|
||||
- "!v[0-9]*-dev"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -72,19 +73,12 @@ jobs:
|
||||
uses: dev-hanz-ops/install-gh-cli-action@6089bdde54118ad7ca3d22053eb2d69387fd2779 # v0.3.0
|
||||
with:
|
||||
gh-cli-version: 2.39.1
|
||||
- id: range
|
||||
run: |
|
||||
previous=$(git tag --list --sort=-v:refname | grep -xE 'v[0-9]+\.[0-9]+\.[0-9]+' | grep -A1 -xF "$GITHUB_REF_NAME" | tail -1) # highest stable version below this one
|
||||
echo "range=$previous..$GITHUB_SHA" >> "$GITHUB_OUTPUT"
|
||||
- uses: orhun/git-cliff-action@a9a95522b26fe6403f7bb24031f21fb573d0f5ff # v4.9.1
|
||||
with:
|
||||
args: --tag ${{ github.ref_name }} ${{ steps.range.outputs.range }}
|
||||
- name: create github release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
gh release create "$TAG" --title "$TAG" --notes-file git-cliff/CHANGELOG.md dist/release/*
|
||||
gh release create "$TAG" --title "$TAG" --notes-from-tag dist/release/*
|
||||
|
||||
container:
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
- In `options/locale`, only edit `locale_en-US.json`, other locales are synced automatically
|
||||
- In TS, use `!` instead of `?.`/`??` when a value always exists
|
||||
- In Go, prefer to use modern language features wherever possible
|
||||
- In Go, function-name prefixes in errors like `fmt.Errorf("Foo: %w", err)` must always name the function they are in
|
||||
- Write sizes as multiplications like `64 * 1024`, not bit shifts like `64 << 10`
|
||||
- Prefer `tw-*` utilities over inline `style` and `flex-*` helpers over per-child `tw-ml-*`/`tw-mr-*` margins, falling back to `tw-*` where specificity requires `!important`
|
||||
- Run `make fmt` after `.go` edits, `make tidy` after `go.mod` edits, `make generate-swagger` after API changes, and lint what changed with `make lint-go`, `lint-js`, `lint-css` or `lint-templates`
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+6614
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -169,7 +169,7 @@ In the PR title, describe the problem you are fixing, not how you are fixing it.
|
||||
Use the first comment as a summary of your PR. \
|
||||
In the PR summary, you can describe exactly how you are fixing this problem.
|
||||
|
||||
PR titles must follow the [Conventional Commits](https://www.conventionalcommits.org/) format, because PRs are squash-merged and the PR title becomes the resulting commit message and release notes entry:
|
||||
PR titles must follow the [Conventional Commits](https://www.conventionalcommits.org/) format, because PRs are squash-merged and the PR title becomes the resulting commit message:
|
||||
|
||||
```text
|
||||
type(scope)!: subject
|
||||
|
||||
@@ -6,7 +6,7 @@ SHASUM ?= shasum -a 256
|
||||
|
||||
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.14.0 # renovate: datasource=go
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.2 # renovate: datasource=go
|
||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.17 # renovate: datasource=go
|
||||
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 # renovate: datasource=go
|
||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.36.6 # renovate: datasource=go
|
||||
@@ -136,7 +136,7 @@ WEB_DIRS := web_src/js web_src/css
|
||||
|
||||
ESLINT_FILES := web_src/js tools *.ts tests/e2e
|
||||
STYLELINT_FILES := web_src/css web_src/js/components/*.vue
|
||||
SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) templates options/locale/locale_en-US.json .github $(wildcard *.go *.md *.yml *.yaml *.toml)
|
||||
SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) templates options/locale/locale_en-US.json .github $(filter-out CHANGELOG.md, $(wildcard *.go *.md *.yml *.yaml *.toml))
|
||||
EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.json
|
||||
|
||||
GO_SOURCES := $(wildcard *.go)
|
||||
|
||||
@@ -120,7 +120,7 @@ See [app.example.ini](https://github.com/go-gitea/gitea/blob/main/custom/conf/ap
|
||||
|
||||
**Where can I find the security patches?**
|
||||
|
||||
Check the [release notes](https://github.com/go-gitea/gitea/releases) and [security advisories](https://github.com/go-gitea/gitea/security/advisories) for security patches.
|
||||
In the [release log](https://github.com/go-gitea/gitea/releases) or the [change log](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md), search for the keyword `SECURITY` to find the security patches.
|
||||
|
||||
(more FAQs are listed in [FAQ documentation](https://docs.gitea.com/help/faq))
|
||||
|
||||
|
||||
+1
-1
@@ -125,7 +125,7 @@ Gitea 的发音是 [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY),就像 "gi-tea
|
||||
|
||||
**在哪里可以找到安全补丁?**
|
||||
|
||||
在 [发布日志](https://github.com/go-gitea/gitea/releases) 中,搜索关键词 `SECURITY` 以找到安全补丁。
|
||||
在 [发布日志](https://github.com/go-gitea/gitea/releases) 或 [变更日志](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) 中,搜索关键词 `SECURITY` 以找到安全补丁。
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
+1
-1
@@ -125,7 +125,7 @@ Gitea 的發音是 [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY),就像 "gi-tea
|
||||
|
||||
**在哪裡可以找到安全補丁?**
|
||||
|
||||
在 [發佈日誌](https://github.com/go-gitea/gitea/releases) 中,搜索關鍵詞 `SECURITY` 以找到安全補丁。
|
||||
在 [發佈日誌](https://github.com/go-gitea/gitea/releases) 或 [變更日誌](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) 中,搜索關鍵詞 `SECURITY` 以找到安全補丁。
|
||||
|
||||
## 許可證
|
||||
|
||||
|
||||
Generated
+45
File diff suppressed because one or more lines are too long
-10
@@ -1,10 +0,0 @@
|
||||
[git]
|
||||
commit_parsers = [
|
||||
{ message = "^(chore|ci)(\\([\\w/.-]+\\))?!?: ", skip = true },
|
||||
{ message = "^feat", group = "Features" },
|
||||
{ message = "^enhance", group = "Enhancements" },
|
||||
{ message = "^perf", group = "Performance" },
|
||||
{ message = "^fix", group = "Bug Fixes" },
|
||||
{ message = "^docs", group = "Documentation" },
|
||||
{ message = ".*", group = "Miscellaneous" },
|
||||
]
|
||||
+8
-2
@@ -13,11 +13,13 @@ import (
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/dump"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/storage"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"gitea.com/go-chi/session"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -247,8 +249,12 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
|
||||
log.Info("Packing data directory...%s", setting.AppDataPath)
|
||||
|
||||
var excludes []string
|
||||
if setting.SessionConfig.Provider == "file" {
|
||||
excludes = append(excludes, setting.SessionConfig.ProviderConfig)
|
||||
if setting.SessionConfig.OriginalProvider == "file" {
|
||||
var opts session.Options
|
||||
if err = json.Unmarshal([]byte(setting.SessionConfig.ProviderConfig), &opts); err != nil {
|
||||
return err
|
||||
}
|
||||
excludes = append(excludes, opts.ProviderConfig)
|
||||
}
|
||||
|
||||
if cmd.IsSet("skip-index") && cmd.Bool("skip-index") {
|
||||
|
||||
+37
-14
@@ -213,6 +213,7 @@ Gitea or set your environment appropriately.`, "")
|
||||
refFullNames := make([]git.RefName, hookBatchSize)
|
||||
count := 0
|
||||
total := 0
|
||||
lastline := 0
|
||||
|
||||
out := io.Discard
|
||||
if setting.Git.VerbosePush {
|
||||
@@ -225,6 +226,8 @@ Gitea or set your environment appropriately.`, "")
|
||||
}
|
||||
}
|
||||
|
||||
supportProcReceive := git.DefaultFeatures().SupportProcReceive
|
||||
|
||||
for scanner.Scan() {
|
||||
// TODO: support news feeds for wiki
|
||||
if isWiki {
|
||||
@@ -237,23 +240,37 @@ Gitea or set your environment appropriately.`, "")
|
||||
}
|
||||
|
||||
total++
|
||||
oldCommitIDs[count] = oldCommitID
|
||||
newCommitIDs[count] = newCommitID
|
||||
refFullNames[count] = refFullName
|
||||
count++
|
||||
fmt.Fprintf(out, "*")
|
||||
lastline++
|
||||
|
||||
if count >= hookBatchSize {
|
||||
fmt.Fprintf(out, " Checking %d references\n", count)
|
||||
// If the ref is a branch or tag, check if it's protected
|
||||
// if supportProcReceive all ref should be checked because
|
||||
// permission check was delayed
|
||||
if supportProcReceive || refFullName.IsBranch() || refFullName.IsTag() {
|
||||
oldCommitIDs[count] = oldCommitID
|
||||
newCommitIDs[count] = newCommitID
|
||||
refFullNames[count] = refFullName
|
||||
count++
|
||||
fmt.Fprintf(out, "*")
|
||||
|
||||
hookOptions.OldCommitIDs = oldCommitIDs
|
||||
hookOptions.NewCommitIDs = newCommitIDs
|
||||
hookOptions.RefFullNames = refFullNames
|
||||
extra := private.HookPreReceive(ctx, ownerName, repoName, hookOptions)
|
||||
if extra.HasError() {
|
||||
return fail(ctx, extra.UserMsg, "HookPreReceive(batch) failed: %v", extra.Error)
|
||||
if count >= hookBatchSize {
|
||||
fmt.Fprintf(out, " Checking %d references\n", count)
|
||||
|
||||
hookOptions.OldCommitIDs = oldCommitIDs
|
||||
hookOptions.NewCommitIDs = newCommitIDs
|
||||
hookOptions.RefFullNames = refFullNames
|
||||
extra := private.HookPreReceive(ctx, ownerName, repoName, hookOptions)
|
||||
if extra.HasError() {
|
||||
return fail(ctx, extra.UserMsg, "HookPreReceive(batch) failed: %v", extra.Error)
|
||||
}
|
||||
count = 0
|
||||
lastline = 0
|
||||
}
|
||||
count = 0
|
||||
} else {
|
||||
fmt.Fprintf(out, ".")
|
||||
}
|
||||
if lastline >= hookBatchSize {
|
||||
fmt.Fprintf(out, "\n")
|
||||
lastline = 0
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
@@ -271,6 +288,8 @@ Gitea or set your environment appropriately.`, "")
|
||||
if extra.HasError() {
|
||||
return fail(ctx, extra.UserMsg, "HookPreReceive(last) failed: %v", extra.Error)
|
||||
}
|
||||
} else if lastline > 0 {
|
||||
fmt.Fprintf(out, "\n")
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "Checked %d references in total\n", total)
|
||||
@@ -456,6 +475,10 @@ Gitea or set your environment appropriately.`, "")
|
||||
return nil
|
||||
}
|
||||
|
||||
if !git.DefaultFeatures().SupportProcReceive {
|
||||
return fail(ctx, "No proc-receive support", "current git version doesn't support proc-receive.")
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
repoUser := os.Getenv(repo_module.EnvRepoUsername)
|
||||
isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki))
|
||||
|
||||
+6
-4
@@ -195,10 +195,12 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
||||
}
|
||||
|
||||
if len(sshCmdArgs) < 2 {
|
||||
// for AGit Flow
|
||||
if cmd == "ssh_info" {
|
||||
cprintf(c, "%s", agit.SshInfoJson)
|
||||
return nil
|
||||
if git.DefaultFeatures().SupportProcReceive {
|
||||
// for AGit Flow
|
||||
if cmd == "ssh_info" {
|
||||
cprintf(c, "%s", agit.SshInfoJson)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fail(ctx, "Too few arguments", "Too few arguments in cmd: %s", cmd)
|
||||
}
|
||||
|
||||
+21
-1
@@ -21,8 +21,19 @@ import (
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/mholt/acmez/v3/acme"
|
||||
)
|
||||
|
||||
func acmeExternalAccountBinding() (acme.EAB, bool, error) {
|
||||
if setting.AcmeEABKID == "" && setting.AcmeEABHMAC == "" {
|
||||
return acme.EAB{}, false, nil
|
||||
}
|
||||
if setting.AcmeEABKID == "" || setting.AcmeEABHMAC == "" {
|
||||
return acme.EAB{}, false, errors.New("both ACME_EAB_KID and ACME_EAB_HMAC must be set")
|
||||
}
|
||||
return acme.EAB{KeyID: setting.AcmeEABKID, MACKey: setting.AcmeEABHMAC}, true, nil
|
||||
}
|
||||
|
||||
func getCARoot(path string) (*x509.CertPool, error) {
|
||||
r, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
@@ -66,6 +77,14 @@ func runACME(listenAddr string, m http.Handler) error {
|
||||
log.Warn("Failed to parse CA Root certificate, using default CA trust: %v", err)
|
||||
}
|
||||
}
|
||||
externalAccountBinding, hasExternalAccount, err := acmeExternalAccountBinding()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var externalAccount *acme.EAB
|
||||
if hasExternalAccount {
|
||||
externalAccount = &externalAccountBinding
|
||||
}
|
||||
// FIXME: this path is not right, it uses "AppWorkPath" incorrectly, and writes the data into "AppWorkPath/https"
|
||||
// Ideally it should migrate to AppDataPath write to "AppDataPath/https"
|
||||
// And one more thing, no idea why we should set the global default variables here
|
||||
@@ -84,6 +103,7 @@ func runACME(listenAddr string, m http.Handler) error {
|
||||
Email: setting.AcmeEmail,
|
||||
Agreed: setting.AcmeTOS,
|
||||
Profile: setting.AcmeProfile,
|
||||
ExternalAccount: externalAccount,
|
||||
DisableHTTPChallenge: !enableHTTPChallenge,
|
||||
DisableTLSALPNChallenge: !enableTLSALPNChallenge,
|
||||
ListenHost: setting.HTTPAddr,
|
||||
@@ -100,7 +120,7 @@ func runACME(listenAddr string, m http.Handler) error {
|
||||
// takes HTTPS down on restart (https://github.com/go-gitea/gitea/issues/38519).
|
||||
// Prefer keeping the existing cert and retrying renewals asynchronously.
|
||||
ctx := graceful.GetManager().ShutdownContext()
|
||||
err := magic.ManageSync(ctx, []string{setting.AppDomain})
|
||||
err = magic.ManageSync(ctx, []string{setting.AppDomain})
|
||||
if err != nil {
|
||||
cert, cacheErr := magic.CacheManagedCertificate(ctx, setting.AppDomain)
|
||||
if cacheErr != nil || cert.Expired() {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
|
||||
"github.com/mholt/acmez/v3/acme"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAcmeExternalAccountBinding(t *testing.T) {
|
||||
t.Cleanup(test.MockVariableValue(&setting.AcmeEABKID, ""))
|
||||
t.Cleanup(test.MockVariableValue(&setting.AcmeEABHMAC, ""))
|
||||
|
||||
binding, configured, err := acmeExternalAccountBinding()
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, configured)
|
||||
assert.Empty(t, binding)
|
||||
|
||||
setting.AcmeEABKID = "kid"
|
||||
_, _, err = acmeExternalAccountBinding()
|
||||
assert.ErrorContains(t, err, "both ACME_EAB_KID and ACME_EAB_HMAC must be set")
|
||||
|
||||
setting.AcmeEABHMAC = "hmac"
|
||||
binding, configured, err = acmeExternalAccountBinding()
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, configured)
|
||||
assert.Equal(t, acme.EAB{KeyID: "kid", MACKey: "hmac"}, binding)
|
||||
}
|
||||
+1
-1
@@ -87,7 +87,7 @@ echo "Checking currently installed version..."
|
||||
current=$(giteacmd --version | cut -d ' ' -f 3)
|
||||
[[ "$current" == "$giteaversion" ]] && echo "$current is already installed, stopping." && exit 0
|
||||
if [[ -z "${no_confirm:-}" ]]; then
|
||||
echo "Make sure to read the changelog first: https://github.com/go-gitea/gitea/releases"
|
||||
echo "Make sure to read the changelog first: https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md"
|
||||
echo "Are you ready to update Gitea from ${current} to ${giteaversion}? (y/N)"
|
||||
read -r confirm
|
||||
[[ "$confirm" == "y" ]] || [[ "$confirm" == "Y" ]] || exit 1
|
||||
|
||||
+18
-12
@@ -264,6 +264,11 @@
|
||||
;; ACME profile to request from the CA (e.g. "shortlived" for raw-IP certificates)
|
||||
;ACME_PROFILE =
|
||||
;;
|
||||
;; External account binding credentials; set both to enable EAB
|
||||
;; ACME_EAB_HMAC should be a base64url-encoded MAC key
|
||||
;ACME_EAB_KID =
|
||||
;ACME_EAB_HMAC =
|
||||
;;
|
||||
;; ACME live directory (not to be confused with ACME directory URL: ACME_URL)
|
||||
;; (Refer to caddy's ACME manager https://github.com/caddyserver/certmagic)
|
||||
;ACME_DIRECTORY = https
|
||||
@@ -459,7 +464,7 @@ INTERNAL_TOKEN =
|
||||
;LOGIN_REMEMBER_DAYS = 31
|
||||
;;
|
||||
;; Name of cookie used to store authentication information.
|
||||
;COOKIE_REMEMBER_NAME = gitea_remember
|
||||
;COOKIE_REMEMBER_NAME = gitea_incredible
|
||||
;;
|
||||
;; URL or path that Gitea should redirect users to *after* performing its own logout.
|
||||
;; Use this, if needed, when authentication is handled by a reverse proxy or SSO.
|
||||
@@ -536,7 +541,7 @@ INTERNAL_TOKEN =
|
||||
;CONTENT_SECURITY_POLICY_GENERAL =
|
||||
;;
|
||||
;; Egress mode toggles between strictness of outgoing requests:
|
||||
;; Lax requires non-public targets (private, loopback, link-local, CGNAT and special-use ranges) to be allowed, it allows all public ones
|
||||
;; Lax requires addresses to be allowed only if they are in private ranges, it allows all public ones
|
||||
;; Strict requires an explicit allow of all addresses
|
||||
; EGRESS_MODE = lax
|
||||
;;
|
||||
@@ -551,12 +556,10 @@ INTERNAL_TOKEN =
|
||||
;; a bracketed set of ports and ranges, | separated: *.mydomain.com:[80|443|3000-3010]
|
||||
;; all ports: *.mydomain.com:*
|
||||
;; A portless entry covers all ports in Lax mode, only 80 and 443 in Strict mode
|
||||
;; Port specs apply only where the list is consulted: in Lax mode that is non-public targets alone,
|
||||
;; public targets are allowed on every port whatever the list says. In Strict mode every
|
||||
;; Port specs apply only where the list is consulted: in Lax mode that is private, loopback and CGNAT
|
||||
;; targets alone, public targets are allowed on every port whatever the list says. In Strict mode every
|
||||
;; target is checked, so ports restrict public hosts too.
|
||||
;; Non-public targets need an IP or built-in entry, a host name entry alone never covers them.
|
||||
;; Reserved addresses (the IPv4-embedding NAT64, Teredo and 6to4 ranges, this-network, multicast and
|
||||
;; broadcast) are denied whatever the list says. To reach them configure an HTTP proxy
|
||||
;; Reserved addresses like link-local and cloud metadata are denied
|
||||
;; This list is enforced on direct connections only. When an HTTP proxy is configured, restricting the proxied target is the proxy server's responsibility.
|
||||
;ALLOWED_HOST_LIST =
|
||||
|
||||
@@ -782,6 +785,8 @@ LEVEL = Info
|
||||
;; Respond to pushes to a non-default branch with a URL for creating a Pull Request (if the repository has them enabled)
|
||||
;PULL_REQUEST_PUSH_MESSAGE = true
|
||||
;;
|
||||
;; (Go-Git only) Don't cache objects greater than this in memory. (Set to 0 to disable.)
|
||||
;LARGE_OBJECT_THRESHOLD = 1048576
|
||||
;; Set to true to forcibly set core.protectNTFS=false
|
||||
;DISABLE_CORE_PROTECT_NTFS=false
|
||||
;; Disable the usage of using partial clones for git.
|
||||
@@ -1981,7 +1986,7 @@ LEVEL = Info
|
||||
;; Either "memory", "redis", "memcache", or "twoqueue". default is "memory"
|
||||
;ADAPTER = memory
|
||||
;;
|
||||
;; For "memory" and "twoqueue", GC interval in seconds, default is 60
|
||||
;; For "memory" only, GC interval in seconds, default is 60
|
||||
;INTERVAL = 60
|
||||
;;
|
||||
;; For "redis" and "memcache", connection host address
|
||||
@@ -2012,17 +2017,19 @@ LEVEL = Info
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;
|
||||
;; Either "memory", "file", "redis" or "db", default is "file". "db" will reuse the configuration in [database]
|
||||
;; Either "memory", "file", "redis", "db", "mysql", "couchbase", "memcache" or "postgres"
|
||||
;; Default is "file". "db" will reuse the configuration in [database]
|
||||
;PROVIDER = file
|
||||
;;
|
||||
;; Provider config options
|
||||
;; memory: doesn't have any config yet
|
||||
;; file: session file path, e.g. `data/sessions`, relative paths will be made absolute against _`AppWorkPath`_.
|
||||
;; redis: default to [redis] CONN_STR
|
||||
;; mysql: go-sql-driver/mysql dsn config string, e.g. `root:password@/session_table`
|
||||
;PROVIDER_CONFIG =
|
||||
;;
|
||||
;; Session cookie name
|
||||
;COOKIE_NAME = gitea_session
|
||||
;COOKIE_NAME = i_like_gitea
|
||||
;;
|
||||
;; If you use session in https only: true or false. If not set, it defaults to `true` if the ROOT_URL is an HTTPS URL.
|
||||
;COOKIE_SECURE =
|
||||
@@ -2048,8 +2055,7 @@ LEVEL = Info
|
||||
;; How Gitea deals with missing repository avatars
|
||||
;; none = no avatar will be displayed; random = random avatar will be displayed; image = default image will be used
|
||||
;REPOSITORY_AVATAR_FALLBACK = none
|
||||
;; Image URL for the "image" fallback, used as-is, defaults to Gitea's builtin repository avatar
|
||||
;REPOSITORY_AVATAR_FALLBACK_IMAGE =
|
||||
;REPOSITORY_AVATAR_FALLBACK_IMAGE = /img/repo_default.png
|
||||
;;
|
||||
;; Max Width and Height of uploaded avatars.
|
||||
;; This is to limit the amount of RAM used when resizing the image.
|
||||
|
||||
@@ -25,6 +25,7 @@ Depending on requirements, the following build tags can be included.
|
||||
- `bindata`: Build a single monolithic binary, with all assets included. Required for distribution and production build.
|
||||
- `pam`: Enable support for PAM (Linux Pluggable Authentication Modules).
|
||||
Can be used to authenticate local users or extend authentication to methods available to PAM.
|
||||
- `gogit`: (EXPERIMENTAL) Use go-git variants of Git commands.
|
||||
|
||||
To include all assets, use the `bindata` tag:
|
||||
|
||||
@@ -32,6 +33,13 @@ To include all assets, use the `bindata` tag:
|
||||
TAGS="bindata" make build
|
||||
```
|
||||
|
||||
Tag `gogit` is used to try to resolve some Windows-specific performance problems, POSIX systems don't need it.
|
||||
You can build a Windows binary by:
|
||||
|
||||
```bash
|
||||
GOOS=windows TAGS="bindata gogit" make build
|
||||
```
|
||||
|
||||
## Changing default paths
|
||||
|
||||
Gitea will search for a number of things from the _`CustomPath`_.
|
||||
|
||||
@@ -62,10 +62,6 @@ Operations that must roll back together should run inside `db.WithTx()` (or
|
||||
Functions that participate in a transaction take a `context.Context` as their first
|
||||
parameter so the transaction can be propagated.
|
||||
|
||||
PostgreSQL, MySQL and MSSQL (via `READ_COMMITTED_SNAPSHOT`) read the last committed
|
||||
row version, so reads never wait for writers. Guard read-then-write logic with a
|
||||
conditional `UPDATE` or a lock.
|
||||
|
||||
### XORM gotchas
|
||||
|
||||
- Never call `x.Update(exemplar)` without an explicit `WHERE` clause — it updates
|
||||
|
||||
+26
-10
@@ -8,9 +8,10 @@ This document describes the release cycle, backports, versioning, and the releas
|
||||
|
||||
We backport PRs given the following circumstances:
|
||||
|
||||
1. We backport bug- and security-fixes and small enhancements. Large changes such as refactors are not backported.
|
||||
2. We never backport new features.
|
||||
3. We never backport breaking changes except when
|
||||
1. Feature freeze is active, but `<version>-rc0` has not been released yet. Here, we backport as much as possible. <!-- TODO: Is that our definition with the new backport bot? -->
|
||||
2. `rc0` has been released. Here, we only backport bug- and security-fixes, and small enhancements. Large PRs such as refactors are not backported anymore. <!-- TODO: Is that our definition with the new backport bot? -->
|
||||
3. We never backport new features.
|
||||
4. We never backport breaking changes except when
|
||||
1. The breaking change has no effect on the vast majority of users
|
||||
2. The component triggering the breaking change is marked as experimental
|
||||
|
||||
@@ -52,6 +53,7 @@ We use a release schedule so work, stabilization, and releases stay predictable.
|
||||
### Cadence
|
||||
|
||||
- Aim for a major release about every three or four months.
|
||||
- Roughly two or three months of general development, then about one month of testing and polish called the **release freeze**.
|
||||
- *Starting with v1.26 the release cycle will be more predictable and follow a more regular schedule.*
|
||||
|
||||
### Release schedule
|
||||
@@ -63,6 +65,16 @@ We will try to publish a new major version every three months:
|
||||
- v1.28.0 in September 2026
|
||||
- v1.29.0 in December 2026
|
||||
|
||||
#### How is the release handled?
|
||||
- The release manager will tag the release candidate (e.g. `v1.26.0-rc0`) and publish it for testing in the **first week of the release month**.
|
||||
- If there are no major issues, the release manager will check with the other maintainers and then tag the final release (e.g. `v1.26.0`) in the **one or two weeks following the release candidate**.
|
||||
|
||||
### Feature freeze
|
||||
|
||||
- Merge feature PRs before the freeze when you can.
|
||||
- Feature PRs still open at the freeze move to the next milestone. Watch Discord for the freeze announcement.
|
||||
- During the freeze, a **release branch** takes fixes backported from `main`. Release candidates ship for testing; the final release for that line is maintained from that branch.
|
||||
|
||||
### Patch releases
|
||||
|
||||
During a cycle we may ship patch releases for an older line. For example, if the latest release is v1.2, we can still publish v1.1.1 after v1.1.0.
|
||||
@@ -87,13 +99,17 @@ be reviewed by two maintainers and must pass the automatic tests.
|
||||
|
||||
## Releasing Gitea
|
||||
|
||||
Track each release using the [release issue template](https://github.com/go-gitea/gitea/issues/new?template=release.yaml).
|
||||
|
||||
- Before releasing, confirm all the version's milestone issues or PRs have been resolved. Then discuss the release on Discord channel #maintainers and get agreed with almost all the owners and mergers. Or you can declare the version and if nobody is against it in about several hours.
|
||||
- When creating a release branch, tag its fork point on `main` as the next version's `-dev` tag, e.g. `v30.0.0-dev` for `release/v29`.
|
||||
- In the GitHub Actions tab, open the `release-create-tag` workflow, click "Run workflow", select the release branch and enter a version such as `28.0.1`. After maintainer approval, it pushes a signed tag and CI publishes the release with generated notes.
|
||||
- Optionally send a PR to the [blog repository](https://gitea.com/gitea/blog) announcing the release.
|
||||
- Let MAJOR, MINOR and PATCH be Major, Minor and Patch version numbers, PATCH should be rc1, rc2, 0, 1, ...... MAJOR.MINOR will be kept the same as milestones on github or gitea in future.
|
||||
- Before releasing, confirm all the version's milestone issues or PRs has been resolved. Then discuss the release on Discord channel #maintainers and get agreed with almost all the owners and mergers. Or you can declare the version and if nobody is against it in about several hours.
|
||||
- If this is a big version first you have to create PR for changelog on branch `main` with PRs with label `changelog` and after it has been merged do following steps:
|
||||
- Create `-dev` tag as `git tag -s -F release.notes vMAJOR.MINOR.0-dev` and push the tag as `git push origin vMAJOR.MINOR.0-dev`.
|
||||
- When CI has finished building tag then you have to create a new branch named `release/vMAJOR.MINOR`
|
||||
- If it is bugfix version create PR for changelog on branch `release/vMAJOR.MINOR` and wait till it is reviewed and merged.
|
||||
- Add a tag as `git tag -s -F release.notes vMAJOR.MINOR.PATCH`, release.notes file could be a temporary file to only include the changelog this version which you added to `CHANGELOG.md`.
|
||||
- And then push the tag as `git push origin vMAJOR.MINOR.$`. CI will automatically create a release and upload all the compiled binary. (But currently it doesn't add the release notes automatically. Maybe we should fix that.)
|
||||
- If needed send a frontport PR for the changelog to branch `main` and update the version in `docs/config.yaml` to refer to the new version.
|
||||
- Send PR to [blog repository](https://gitea.com/gitea/blog) announcing the release.
|
||||
- Verify all release assets were correctly published through CI on dl.gitea.com and GitHub releases. Once ACKed:
|
||||
- verify the automated update of https://dl.gitea.com/gitea/version.json, where applicable to the release line
|
||||
- bump the version of https://dl.gitea.com/gitea/version.json
|
||||
- merge the blog post PR
|
||||
- announce the release in discord `#announcements`
|
||||
|
||||
+29
-49
@@ -17,7 +17,6 @@ import unescapedHtmlLiteral from './tools/eslint-rules/unescaped-html-literal.ts
|
||||
|
||||
const jsExts = ['js', 'mjs', 'cjs'] as const;
|
||||
const tsExts = ['ts', 'mts', 'cts'] as const;
|
||||
const vueExts = ['vue'] as const;
|
||||
|
||||
const restrictedGlobals = [
|
||||
{name: 'localStorage', message: 'Use `modules/user-settings.ts` instead.'},
|
||||
@@ -37,7 +36,7 @@ export default defineConfig([
|
||||
'public/assets/js',
|
||||
]),
|
||||
{
|
||||
files: [`**/*.{${[...jsExts, ...tsExts, ...vueExts].join(',')}}`],
|
||||
files: [`**/*.{${[...jsExts, ...tsExts].join(',')}}`],
|
||||
ignores: ['dist/*'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
@@ -49,9 +48,7 @@ export default defineConfig([
|
||||
ecmaFeatures: {
|
||||
impliedStrict: true,
|
||||
},
|
||||
parser: typescriptParser,
|
||||
project: true,
|
||||
extraFileExtensions: vueExts.map((ext) => `.${ext}`),
|
||||
},
|
||||
},
|
||||
linterOptions: {
|
||||
@@ -69,11 +66,8 @@ export default defineConfig([
|
||||
wc,
|
||||
},
|
||||
settings: {
|
||||
'import-x/extensions': [...jsExts, ...tsExts, ...vueExts].map((ext) => `.${ext}`),
|
||||
'import-x/parsers': {
|
||||
'@typescript-eslint/parser': [...jsExts, ...tsExts].map((ext) => `.${ext}`),
|
||||
'vue-eslint-parser': vueExts.map((ext) => `.${ext}`),
|
||||
},
|
||||
'import-x/extensions': [...jsExts, ...tsExts].map((ext) => `.${ext}`),
|
||||
'import-x/parsers': {'@typescript-eslint/parser': [...jsExts, ...tsExts].map((ext) => `.${ext}`)},
|
||||
'import-x/resolver': {'typescript': true},
|
||||
},
|
||||
rules: {
|
||||
@@ -239,7 +233,6 @@ export default defineConfig([
|
||||
'@typescript-eslint/no-unsafe-assignment': [0],
|
||||
'@typescript-eslint/no-unsafe-call': [0],
|
||||
'@typescript-eslint/no-unsafe-declaration-merging': [2],
|
||||
'@typescript-eslint/no-unsafe-enum-assignment': [0],
|
||||
'@typescript-eslint/no-unsafe-enum-comparison': [0],
|
||||
'@typescript-eslint/no-unsafe-function-type': [2],
|
||||
'@typescript-eslint/no-unsafe-member-access': [0],
|
||||
@@ -711,7 +704,6 @@ export default defineConfig([
|
||||
'unicorn/better-dom-traversing': [2],
|
||||
'unicorn/catch-error-name': [0],
|
||||
'unicorn/class-reference-in-static-methods': [2],
|
||||
'unicorn/comma-spacing': [0], // only applies to json language
|
||||
'unicorn/comment-content': [0],
|
||||
'unicorn/consistent-arrow-return-style': [0],
|
||||
'unicorn/consistent-assert': [0],
|
||||
@@ -742,10 +734,8 @@ export default defineConfig([
|
||||
'unicorn/filename-case': [0],
|
||||
'unicorn/id-match': [2],
|
||||
'unicorn/import-style': [0],
|
||||
'unicorn/indent': [0], // only applies to json and css languages
|
||||
'unicorn/isolated-functions': [2, {functions: []}],
|
||||
'unicorn/iteration-fallback-style': [2, 'fallback'],
|
||||
'unicorn/key-name-casing': [0], // only applies to json, yaml and toml languages
|
||||
'unicorn/logical-assignment-operators': [0],
|
||||
'unicorn/max-nested-calls': [0],
|
||||
'unicorn/name-replacements': [0],
|
||||
@@ -778,20 +768,21 @@ export default defineConfig([
|
||||
'unicorn/no-chained-comparison': [2],
|
||||
'unicorn/no-collection-bracket-access': [2],
|
||||
'unicorn/no-computed-property-existence-check': [0],
|
||||
'unicorn/no-conflicting-constraints': [2],
|
||||
'unicorn/no-confusing-array-splice': [2],
|
||||
'unicorn/no-confusing-array-with': [2],
|
||||
'unicorn/no-console-spaces': [0],
|
||||
'unicorn/no-constant-zero-expression': [2],
|
||||
'unicorn/no-declarations-before-early-exit': [0],
|
||||
'unicorn/no-deprecated-css-features': [0],
|
||||
'unicorn/no-document-cookie': [2],
|
||||
'unicorn/no-double-comparison': [2],
|
||||
'unicorn/no-duplicate-css-selectors': [0],
|
||||
'unicorn/no-duplicate-font-family-names': [0],
|
||||
'unicorn/no-duplicate-if-branches': [2],
|
||||
'unicorn/no-duplicate-logical-operands': [2],
|
||||
'unicorn/no-duplicate-loops': [0],
|
||||
'unicorn/no-duplicate-set-values': [2],
|
||||
'unicorn/no-empty-file': [2],
|
||||
'unicorn/no-empty-link-text': [0], // only applies to markdown language
|
||||
'unicorn/no-error-property-assignment': [2],
|
||||
'unicorn/no-exports-in-scripts': [2],
|
||||
'unicorn/no-for-each': [2],
|
||||
@@ -799,34 +790,21 @@ export default defineConfig([
|
||||
'unicorn/no-global-object-property-assignment': [0],
|
||||
'unicorn/no-immediate-mutation': [0],
|
||||
'unicorn/no-impossible-length-comparison': [2],
|
||||
'unicorn/no-incomplete-accessor-override': [2],
|
||||
'unicorn/no-incorrect-query-selector': [2],
|
||||
'unicorn/no-incorrect-template-string-interpolation': [0],
|
||||
'unicorn/no-ineffective-csp-directives': [2],
|
||||
'unicorn/no-instanceof-builtins': [2],
|
||||
'unicorn/no-invalid-argument-count': [2],
|
||||
'unicorn/no-invalid-boolean-attribute-value': [2],
|
||||
'unicorn/no-invalid-character-comparison': [2],
|
||||
'unicorn/no-invalid-dom-token': [2],
|
||||
'unicorn/no-invalid-fetch-options': [2],
|
||||
'unicorn/no-invalid-file-input-accept': [2],
|
||||
'unicorn/no-invalid-integrity': [2],
|
||||
'unicorn/no-invalid-intl-options': [2],
|
||||
'unicorn/no-invalid-property-descriptor': [2],
|
||||
'unicorn/no-invalid-media-features': [0],
|
||||
'unicorn/no-invalid-remove-event-listener': [2],
|
||||
'unicorn/no-invalid-response-options': [2],
|
||||
'unicorn/no-invalid-style-set-property': [2],
|
||||
'unicorn/no-invalid-temporal-arithmetic': [2],
|
||||
'unicorn/no-invalid-url-protocol-comparison': [2],
|
||||
'unicorn/no-invalid-well-known-symbol-methods': [2],
|
||||
'unicorn/no-javascript-url': [0], // only applies to markdown language
|
||||
'unicorn/no-keyword-prefix': [0],
|
||||
'unicorn/no-late-current-target-access': [2],
|
||||
'unicorn/no-late-event-control': [2],
|
||||
'unicorn/no-leading-empty-lines': [0], // handled by @stylistic/no-multiple-empty-lines
|
||||
'unicorn/no-lonely-if': [2],
|
||||
'unicorn/no-loop-iterable-mutation': [2],
|
||||
'unicorn/no-loss-of-precision': [0], // only applies to json, toml and css languages
|
||||
'unicorn/no-magic-array-flat-depth': [0],
|
||||
'unicorn/no-manually-wrapped-comments': [0], // too opinionated
|
||||
'unicorn/no-mismatched-map-key': [2],
|
||||
@@ -839,6 +817,7 @@ export default defineConfig([
|
||||
'unicorn/no-negated-condition': [0],
|
||||
'unicorn/no-negation-in-equality-check': [2],
|
||||
'unicorn/no-nested-ternary': [0],
|
||||
'unicorn/no-nesting-with-mixed-specificity': [0],
|
||||
'unicorn/no-new-array': [0],
|
||||
'unicorn/no-new-buffer': [2],
|
||||
'unicorn/no-non-function-verb-prefix': [0],
|
||||
@@ -847,9 +826,9 @@ export default defineConfig([
|
||||
'unicorn/no-object-as-default-parameter': [0],
|
||||
'unicorn/no-object-methods-with-collections': [2],
|
||||
'unicorn/no-optional-chaining-on-undeclared-variable': [2],
|
||||
'unicorn/no-prevent-default-in-passive-listener': [2],
|
||||
'unicorn/no-process-exit': [0],
|
||||
'unicorn/no-redundant-comparison': [2],
|
||||
'unicorn/no-redundant-nested-style-rules': [0],
|
||||
'unicorn/no-return-array-push': [2],
|
||||
'unicorn/no-selector-as-dom-name': [2],
|
||||
'unicorn/no-shorthand-property-overrides': [0], // only applies to css language
|
||||
@@ -865,6 +844,8 @@ export default defineConfig([
|
||||
'unicorn/no-typeof-undefined': [2],
|
||||
'unicorn/no-uncalled-method': [2],
|
||||
'unicorn/no-undeclared-class-members': [2],
|
||||
'unicorn/no-unknown-css-annotations': [0],
|
||||
'unicorn/no-unknown-pseudo-selectors': [0],
|
||||
'unicorn/no-unnecessary-array-flat-depth': [2],
|
||||
'unicorn/no-unnecessary-array-flat-map': [2],
|
||||
'unicorn/no-unnecessary-array-splice-count': [2],
|
||||
@@ -873,7 +854,6 @@ export default defineConfig([
|
||||
'unicorn/no-unnecessary-fetch-options': [0],
|
||||
'unicorn/no-unnecessary-global-this': [0],
|
||||
'unicorn/no-unnecessary-nested-ternary': [2],
|
||||
'unicorn/no-unnecessary-parameters': [0],
|
||||
'unicorn/no-unnecessary-polyfills': [2],
|
||||
'unicorn/no-unnecessary-slice-end': [2],
|
||||
'unicorn/no-unnecessary-splice': [2],
|
||||
@@ -885,15 +865,14 @@ export default defineConfig([
|
||||
'unicorn/no-unreadable-object-destructuring': [0],
|
||||
'unicorn/no-unsafe-buffer-conversion': [2],
|
||||
'unicorn/no-unsafe-dom-html': [0],
|
||||
'unicorn/no-unsafe-json-serialization': [2],
|
||||
'unicorn/no-unsafe-promise-all-settled-values': [2],
|
||||
'unicorn/no-unsafe-property-key': [0],
|
||||
'unicorn/no-unsafe-sqlite-interpolation': [2],
|
||||
'unicorn/no-unsafe-string-replacement': [2],
|
||||
'unicorn/no-unscoped-css-nesting-selector': [0],
|
||||
'unicorn/no-unused-builtin-method-return': [2],
|
||||
'unicorn/no-unused-iterator-helper': [2],
|
||||
'unicorn/no-unused-properties': [2],
|
||||
'unicorn/no-url-in-search-params': [2],
|
||||
'unicorn/no-useless-boolean-cast': [2],
|
||||
'unicorn/no-useless-coercion': [2],
|
||||
'unicorn/no-useless-collection-argument': [2],
|
||||
@@ -930,12 +909,12 @@ export default defineConfig([
|
||||
'unicorn/prefer-array-find': [0], // handled by @typescript-eslint/prefer-find
|
||||
'unicorn/prefer-array-flat': [2],
|
||||
'unicorn/prefer-array-flat-map': [2],
|
||||
'unicorn/prefer-array-from-async': [0], // Array.fromAsync requires ES2026
|
||||
'unicorn/prefer-array-from-async': [2],
|
||||
'unicorn/prefer-array-from-map': [2],
|
||||
'unicorn/prefer-array-from-range': [2],
|
||||
'unicorn/prefer-array-index-of': [2],
|
||||
'unicorn/prefer-array-iterable-methods': [2],
|
||||
'unicorn/prefer-array-last-methods': [0], // Array#findLast/findLastIndex requires ES2023
|
||||
'unicorn/prefer-array-last-methods': [2],
|
||||
'unicorn/prefer-array-slice': [2],
|
||||
'unicorn/prefer-array-some': [2],
|
||||
'unicorn/prefer-at': [0],
|
||||
@@ -952,7 +931,7 @@ export default defineConfig([
|
||||
'unicorn/prefer-date-now': [2],
|
||||
'unicorn/prefer-default-parameters': [0],
|
||||
'unicorn/prefer-direct-iteration': [2],
|
||||
'unicorn/prefer-dispose': [0], // `using` requires ES2027
|
||||
'unicorn/prefer-dispose': [2],
|
||||
'unicorn/prefer-dom-node-append': [2],
|
||||
'unicorn/prefer-dom-node-html-methods': [0],
|
||||
'unicorn/prefer-dom-node-remove': [2],
|
||||
@@ -961,14 +940,14 @@ export default defineConfig([
|
||||
'unicorn/prefer-early-return': [0],
|
||||
'unicorn/prefer-else-if': [2],
|
||||
'unicorn/prefer-error-is-error': [0],
|
||||
'unicorn/prefer-escaped-irregular-whitespace': [2],
|
||||
'unicorn/prefer-event-target': [2],
|
||||
'unicorn/prefer-explicit-viewport-units': [0], // only applies to css language
|
||||
'unicorn/prefer-export-from': [0],
|
||||
'unicorn/prefer-flat-math-min-max': [2],
|
||||
'unicorn/prefer-get-or-insert-computed': [0], // Map#getOrInsertComputed requires ES2026
|
||||
'unicorn/prefer-get-or-insert-computed': [2],
|
||||
'unicorn/prefer-global-number-constants': [2],
|
||||
'unicorn/prefer-global-this': [0],
|
||||
'unicorn/prefer-group-by': [0], // Object.groupBy/Map.groupBy requires ES2024
|
||||
'unicorn/prefer-group-by': [2],
|
||||
'unicorn/prefer-has-check': [2],
|
||||
'unicorn/prefer-hoisting-branch-code': [2],
|
||||
'unicorn/prefer-https': [0], // false-positives on namespace and schema URIs
|
||||
@@ -979,12 +958,11 @@ export default defineConfig([
|
||||
'unicorn/prefer-iterable-in-constructor': [2],
|
||||
'unicorn/prefer-iterator-concat': [0], // too opinionated
|
||||
'unicorn/prefer-iterator-helpers': [0],
|
||||
'unicorn/prefer-iterator-to-array': [0], // Iterator#toArray requires ES2025
|
||||
'unicorn/prefer-iterator-to-array': [2],
|
||||
'unicorn/prefer-iterator-to-array-at-end': [2],
|
||||
'unicorn/prefer-iterator-zip': [0],
|
||||
'unicorn/prefer-json-import': [0],
|
||||
'unicorn/prefer-keyboard-event-key': [2],
|
||||
'unicorn/prefer-literal-ascii': [2],
|
||||
'unicorn/prefer-location-assign': [2],
|
||||
'unicorn/prefer-logical-operator-over-ternary': [0],
|
||||
'unicorn/prefer-map-from-entries': [0],
|
||||
@@ -992,6 +970,7 @@ export default defineConfig([
|
||||
'unicorn/prefer-math-constants': [2],
|
||||
'unicorn/prefer-math-min-max': [2],
|
||||
'unicorn/prefer-math-trunc': [2],
|
||||
'unicorn/prefer-media-feature-range-syntax': [0],
|
||||
'unicorn/prefer-minimal-ternary': [0],
|
||||
'unicorn/prefer-modern-dom-apis': [0],
|
||||
'unicorn/prefer-modern-math-apis': [2],
|
||||
@@ -1010,9 +989,8 @@ export default defineConfig([
|
||||
'unicorn/prefer-optional-catch-binding': [2],
|
||||
'unicorn/prefer-path2d': [2],
|
||||
'unicorn/prefer-private-class-fields': [0],
|
||||
'unicorn/prefer-promise-static-methods': [2],
|
||||
'unicorn/prefer-promise-try': [0], // Promise.try requires ES2025
|
||||
'unicorn/prefer-promise-with-resolvers': [0], // Promise.withResolvers requires ES2024
|
||||
'unicorn/prefer-promise-try': [2],
|
||||
'unicorn/prefer-promise-with-resolvers': [2],
|
||||
'unicorn/prefer-prototype-methods': [2],
|
||||
'unicorn/prefer-query-selector': [2],
|
||||
'unicorn/prefer-queue-microtask': [2],
|
||||
@@ -1025,7 +1003,6 @@ export default defineConfig([
|
||||
'unicorn/prefer-set-methods': [0],
|
||||
'unicorn/prefer-set-size': [2],
|
||||
'unicorn/prefer-short-arrow-method': [2],
|
||||
'unicorn/prefer-short-escape-sequences': [2],
|
||||
'unicorn/prefer-simple-condition-first': [0],
|
||||
'unicorn/prefer-simple-sort-comparator': [2],
|
||||
'unicorn/prefer-simplified-conditions': [2],
|
||||
@@ -1047,7 +1024,7 @@ export default defineConfig([
|
||||
'unicorn/prefer-structured-clone': [2],
|
||||
'unicorn/prefer-switch': [0],
|
||||
'unicorn/prefer-temporal': [0],
|
||||
'unicorn/prefer-temporal-conversion': [0], // Temporal requires ES2027
|
||||
'unicorn/prefer-temporal-conversion': [2],
|
||||
'unicorn/prefer-ternary': [0],
|
||||
'unicorn/prefer-then-catch': [2],
|
||||
'unicorn/prefer-toggle-attribute': [2],
|
||||
@@ -1073,7 +1050,6 @@ export default defineConfig([
|
||||
'unicorn/require-passive-events': [2],
|
||||
'unicorn/require-post-message-target-origin': [0],
|
||||
'unicorn/require-proxy-trap-boolean-return': [2],
|
||||
'unicorn/require-text-decoder-streaming': [2],
|
||||
'unicorn/single-line-block-comment-style': [0],
|
||||
'unicorn/string-content': [0],
|
||||
'unicorn/switch-case-braces': [0],
|
||||
@@ -1120,13 +1096,17 @@ export default defineConfig([
|
||||
},
|
||||
},
|
||||
{
|
||||
files: vueExts.map((ext) => `**/*.${ext}`),
|
||||
files: ['**/*.vue'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser',
|
||||
},
|
||||
},
|
||||
extends: [
|
||||
vue.configs['flat/recommended'],
|
||||
vueScopedCss.configs.recommended,
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/no-redundant-type-constituents': [0], // types imported from .vue files resolve to any via typescript-eslint's *.vue shim
|
||||
'vue/attributes-order': [0],
|
||||
'vue/html-closing-bracket-spacing': [2, {startTag: 'never', endTag: 'never', selfClosingTag: 'never'}],
|
||||
'vue/max-attributes-per-line': [0],
|
||||
|
||||
@@ -6,22 +6,25 @@ toolchain go1.27.1
|
||||
|
||||
require (
|
||||
connectrpc.com/connect v1.21.0
|
||||
gitea.com/go-chi/binding v0.0.0-20260819122636-082915a69981
|
||||
gitea.com/go-chi/cache v0.2.1
|
||||
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
|
||||
gitea.com/go-chi/session v0.0.0-20260708011333-ebced8a7a2d6
|
||||
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
|
||||
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
|
||||
gitea.dev/actionslib v1.3.0
|
||||
gitea.dev/actionslib v1.2.1
|
||||
gitea.dev/sdk v1.2.0
|
||||
github.com/42wim/httpsig v1.2.4
|
||||
github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432
|
||||
github.com/Azure/go-ntlmssp v0.1.1
|
||||
github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347
|
||||
github.com/ProtonMail/go-crypto v1.5.2
|
||||
github.com/ProtonMail/go-crypto v1.5.0
|
||||
github.com/PuerkitoBio/goquery v1.13.0
|
||||
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/bohde/codel v0.2.0
|
||||
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c
|
||||
github.com/buildkite/terminal-to-html/v3 v3.17.1
|
||||
github.com/caddyserver/certmagic v0.25.4
|
||||
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20260812203852-971c0284dc33
|
||||
@@ -32,6 +35,7 @@ require (
|
||||
github.com/dustin/go-humanize v1.1.0
|
||||
github.com/editorconfig/editorconfig-core-go/v2 v2.6.5
|
||||
github.com/emersion/go-imap v1.2.1
|
||||
github.com/emirpasic/gods v1.18.1
|
||||
github.com/felixge/fgprof v0.9.5
|
||||
github.com/fsnotify/fsnotify v1.10.1
|
||||
github.com/getkin/kin-openapi v0.149.0
|
||||
@@ -39,6 +43,8 @@ require (
|
||||
github.com/go-chi/cors v1.2.2
|
||||
github.com/go-co-op/gocron/v2 v2.22.0
|
||||
github.com/go-enry/go-enry/v2 v2.9.6
|
||||
github.com/go-git/go-billy/v5 v5.9.1
|
||||
github.com/go-git/go-git/v5 v5.19.2
|
||||
github.com/go-ldap/ldap/v3 v3.4.14
|
||||
github.com/go-redsync/redsync/v4 v4.18.0
|
||||
github.com/go-sql-driver/mysql v1.10.1
|
||||
@@ -56,12 +62,13 @@ require (
|
||||
github.com/huandu/xstrings v1.6.1
|
||||
github.com/jhillyerd/enmime/v2 v2.5.0
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/klauspost/compress v1.20.1
|
||||
github.com/klauspost/compress v1.20.0
|
||||
github.com/lib/pq v1.12.3
|
||||
github.com/markbates/goth v1.82.0
|
||||
github.com/mattn/go-isatty v0.0.24
|
||||
github.com/mattn/go-sqlite3 v1.14.52
|
||||
github.com/meilisearch/meilisearch-go v0.36.3
|
||||
github.com/mholt/acmez/v3 v3.1.6
|
||||
github.com/mholt/archives v0.1.5
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/microsoft/go-mssqldb v1.11.2
|
||||
@@ -87,7 +94,7 @@ require (
|
||||
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.15.0
|
||||
gitlab.com/gitlab-org/api/client-go/v3 v3.13.0
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6
|
||||
golang.org/x/crypto v0.57.0
|
||||
golang.org/x/image v0.46.0
|
||||
@@ -100,16 +107,18 @@ require (
|
||||
google.golang.org/grpc v1.84.0
|
||||
google.golang.org/protobuf v1.36.12
|
||||
gopkg.in/ini.v1 v1.67.3
|
||||
modernc.org/sqlite v1.60.1
|
||||
modernc.org/sqlite v1.59.0
|
||||
mvdan.cc/xurls/v2 v2.6.0
|
||||
xorm.io/builder v0.3.13
|
||||
xorm.io/xorm v1.4.3
|
||||
xorm.io/xorm v1.4.1
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/DataDog/zstd v1.5.7 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // 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
|
||||
@@ -135,18 +144,23 @@ require (
|
||||
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.2 // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0 // 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
|
||||
github.com/boombuler/barcode v1.1.0 // indirect
|
||||
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c // indirect
|
||||
github.com/caddyserver/zerossl v0.1.5 // indirect
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/couchbase/go-couchbase v0.1.1 // indirect
|
||||
github.com/couchbase/gomemcached v0.3.4 // indirect
|
||||
github.com/couchbase/goutils v0.3.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
||||
@@ -155,6 +169,7 @@ require (
|
||||
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect
|
||||
github.com/go-enry/go-oniguruma v1.2.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.23.1 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.26.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
@@ -162,7 +177,9 @@ require (
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/flatbuffers v25.12.19+incompatible // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
@@ -172,8 +189,10 @@ require (
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/inbucket/html2text v1.0.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
@@ -181,7 +200,6 @@ require (
|
||||
github.com/markbates/going v1.0.3 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.24 // indirect
|
||||
github.com/mholt/acmez/v3 v3.1.6 // indirect
|
||||
github.com/miekg/dns v1.1.72 // indirect
|
||||
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||
@@ -201,9 +219,9 @@ require (
|
||||
github.com/olekukonko/ll v0.1.8 // indirect
|
||||
github.com/olekukonko/tablewriter v1.1.4 // indirect
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/onsi/gomega v1.34.1 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.27 // indirect
|
||||
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
@@ -214,12 +232,15 @@ require (
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.10.2 // indirect
|
||||
github.com/skeema/knownhosts v1.3.2 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/stangelandcl/ppmd v0.1.1 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/unknwon/com v1.0.1 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||
github.com/zeebo/xxh3 v1.1.0 // indirect
|
||||
@@ -230,11 +251,11 @@ require (
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/tools v0.50.0 // indirect
|
||||
golang.org/x/tools v0.49.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
|
||||
modernc.org/libc v1.77.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
modernc.org/libc v1.75.7 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.12.1 // indirect
|
||||
)
|
||||
|
||||
@@ -4,16 +4,26 @@ code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
|
||||
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
|
||||
connectrpc.com/connect v1.21.0 h1:LhqSJt7jHf5NJBo9Jq/t/9FjcYAideif0mg+qe2jCUs=
|
||||
connectrpc.com/connect v1.21.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
gitea.com/go-chi/binding v0.0.0-20260819122636-082915a69981 h1:LmdlwGbzgZFZA3bK3R1q8QrNabaSz3KpnOJBLmzhN6E=
|
||||
gitea.com/go-chi/binding v0.0.0-20260819122636-082915a69981/go.mod h1:q1SSPpkC9A0gfNnoqqZ3My6kEHIpKN6QsTI+Zx73B/o=
|
||||
gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g=
|
||||
gitea.com/go-chi/cache v0.2.1/go.mod h1:Qic0HZ8hOHW62ETGbonpwz8WYypj9NieU9659wFUJ8Q=
|
||||
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo=
|
||||
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098/go.mod h1:LjzIOHlRemuUyO7WR12fmm18VZIlCAaOt9L3yKw40pk=
|
||||
gitea.com/go-chi/session v0.0.0-20260708011333-ebced8a7a2d6 h1:YWzVGeC/8SZThrJS48ZmQYLkzssdeABxHPhbdnxPDIU=
|
||||
gitea.com/go-chi/session v0.0.0-20260708011333-ebced8a7a2d6/go.mod h1:KDvcfMUoXfATPHs2mbMoXFTXT45/FAFAS39waz9tPk0=
|
||||
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/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.3.0 h1:xZRoTL1+sK/PaglT9uXkZJ9g0nk1WLMEH4FUXdXpKKw=
|
||||
gitea.dev/actionslib v1.3.0/go.mod h1:svCefEsavx4jmn8vJ4wTG7Q0KBcGfrFmGUjrhxuNUAQ=
|
||||
gitea.dev/actionslib v1.2.1 h1:GL//K/0zIZV6h1OOksv1awvFVg0rg7fug2xWcG7mkG0=
|
||||
gitea.dev/actionslib v1.2.1/go.mod h1:1+gqOKGSEPn2IFHgY8S3GC5Ld+Hn8jF3OxiFHQ/fpx4=
|
||||
gitea.dev/sdk v1.2.0 h1:avRtJl/nKCGispgSalo9czoZM9Rto1awnE0caNAoXGo=
|
||||
gitea.dev/sdk v1.2.0/go.mod h1:rfh5oNdIK24cbCREwIn1tqWKQW+IICXFGWJyebuOAOE=
|
||||
github.com/42wim/httpsig v1.2.4 h1:mI5bH0nm4xn7K18fo1K3okNDRq8CCJ0KbBYWyA6r8lU=
|
||||
@@ -36,10 +46,13 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0 h1:Nljr4q1GRA/
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0/go.mod h1:Y33QHnf0FfdVewFFISOGe20mkZbxX4H839o955/PoeI=
|
||||
github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
|
||||
github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347 h1:3JhDl+JysaO8nhNU1XMaw35VSGjV4IEQAefaG4Lyok4=
|
||||
github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347/go.mod h1:2ErI0aycD43Ufr6CFK5lT/NrHGmoZuVbn1nlPThw69o=
|
||||
github.com/ProtonMail/go-crypto v1.5.2 h1:cucYnvqcY7UOXVD//mSyjeaPY0SSN3v5cDkYPxumINk=
|
||||
github.com/ProtonMail/go-crypto v1.5.2/go.mod h1:/RaSu30DaKO4RY+XdV/ACcCcZkGr7AhUIduq5sjzzCo=
|
||||
github.com/ProtonMail/go-crypto v1.5.0 h1:sKmuvjOgsnrtpMvZ+84MCnTJCpjxZ1qCFn076lz1yT0=
|
||||
github.com/ProtonMail/go-crypto v1.5.0/go.mod h1:/RaSu30DaKO4RY+XdV/ACcCcZkGr7AhUIduq5sjzzCo=
|
||||
github.com/PuerkitoBio/goquery v1.13.0 h1:mqHbjD7Jmnul4DTR24LKTjo1uUmHUh072kteGV+xpFM=
|
||||
github.com/PuerkitoBio/goquery v1.13.0/go.mod h1:Hip5mdBL8K2wEGKJdr27sRaNwIdDajmCwB/ExUPwW+g=
|
||||
github.com/RoaringBitmap/roaring/v2 v2.18.2 h1:oPq3Cgx//iDuJQVp6xSInAKW34J9CEwE5GmLI2z+Eic=
|
||||
@@ -62,6 +75,10 @@ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eT
|
||||
github.com/andybalholm/brotli v1.2.1/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/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -108,8 +125,8 @@ github.com/blevesearch/zapx/v16 v16.3.4 h1:hDAqA8qusZTNbPEL7//w5P65UZ2de6yhSeUaT
|
||||
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.2 h1:eF7W7HWKg3z9NrWV9pTLnNeoXaqq3Tq9DNKXVMfoCnw=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q=
|
||||
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
|
||||
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
|
||||
@@ -156,8 +173,17 @@ github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
|
||||
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/couchbase/go-couchbase v0.1.1 h1:ClFXELcKj/ojyoTYbsY34QUrrYCBi/1G749sXSCkdhk=
|
||||
github.com/couchbase/go-couchbase v0.1.1/go.mod h1:+/bddYDxXsf9qt0xpDUtRR47A2GjaXmGGAqQ/k3GJ8A=
|
||||
github.com/couchbase/gomemcached v0.3.4 h1:VGdrZUJbt5lLyI/MXnyVCZKHKYXg/vaud08lJIAeZps=
|
||||
github.com/couchbase/gomemcached v0.3.4/go.mod h1:pISAjweI42vljCumsJIo7CVhqIMIIP9g3Wfhl1JJw68=
|
||||
github.com/couchbase/goutils v0.1.2/go.mod h1:h89Ek/tiOxxqjz30nPPlwZdQbdB8BwgnuBxeoUe/ViE=
|
||||
github.com/couchbase/goutils v0.3.0 h1:rsv72B6BDjW9jmwlfiDUrdu3EpNvPuo5WLULHzQ0DLE=
|
||||
github.com/couchbase/goutils v0.3.0/go.mod h1:7Gm+D3vXfV4HS+hQWvKfy6e6ILCptGXNqBKvQXhplhk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
@@ -179,6 +205,8 @@ github.com/dustin/go-humanize v1.1.0 h1:dbKTrvD0klcbBV/h4AWJdMuZogJACoMlvWIWZ5b2
|
||||
github.com/dustin/go-humanize v1.1.0/go.mod h1:hc1CvRkJMsgxqjmjMQF3QNRAZBwY8AXBAzKYoSX9sFI=
|
||||
github.com/editorconfig/editorconfig-core-go/v2 v2.6.5 h1:MTcuJQkIFRLfNn9FfAvdO0p3FhNyaCf+IGTwy0TxX6E=
|
||||
github.com/editorconfig/editorconfig-core-go/v2 v2.6.5/go.mod h1:SizrS3EM1vFF0v/JZlJ5LfK6tXhwln2823YZwNh/vBE=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
|
||||
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
|
||||
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
|
||||
@@ -186,6 +214,8 @@ github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTe
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
|
||||
@@ -200,6 +230,8 @@ github.com/getkin/kin-openapi v0.149.0 h1:ZbhmVJ4yq5RZDUsyP8lcBcGMsjsaTqXEFt6isd
|
||||
github.com/getkin/kin-openapi v0.149.0/go.mod h1:1+BHDzstro+P5CKtPy1X4PfofnFgmRe6uvMy9+r9fKY=
|
||||
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 h1:mtDjlmloH7ytdblogrMz1/8Hqua1y8B4ID+bh3rvod0=
|
||||
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1/go.mod h1:fenKRzpXDjNpsIBhuhUzvjCKlDjKam0boRAenTE0Q6A=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8 h1:H9AZkK22UOmfX8J84ubyaZxKJZ3FMHVwn8swoMML7iQ=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
@@ -213,6 +245,14 @@ github.com/go-enry/go-enry/v2 v2.9.6 h1:np63eOtMV56zfYDHnFVgpEVOk8fr2kmylcMnAZUD
|
||||
github.com/go-enry/go-enry/v2 v2.9.6/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8=
|
||||
github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo=
|
||||
github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.9.1 h1:8U73XiOTfINdItHVa6z4Gv7ToObcZ6grkqQbLryLCdA=
|
||||
github.com/go-git/go-billy/v5 v5.9.1/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
|
||||
github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-ldap/ldap/v3 v3.4.14 h1:D6PYdEgsaVzsXyr6w/yDC06Ria4uUhWm+Rb+er8lfAs=
|
||||
@@ -257,6 +297,8 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
@@ -272,6 +314,9 @@ github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8=
|
||||
github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
|
||||
github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
|
||||
github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
@@ -298,6 +343,8 @@ github.com/google/pprof v0.0.0-20260906184651-6331bc6350fe h1:QAinXoAFJdGQYztXn3
|
||||
github.com/google/pprof v0.0.0-20260906184651-6331bc6350fe/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
@@ -334,6 +381,8 @@ github.com/huandu/xstrings v1.6.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
|
||||
github.com/inbucket/html2text v1.0.0 h1:N5kza++4uBBDJ2Z3KUnTRyPNoBcW+YfOgNiNmNB+sgs=
|
||||
github.com/inbucket/html2text v1.0.0/go.mod h1:5TrhXQKGU+LXurODaSm55Y9eXoPBRnYiOz4x2XfUoJU=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||
@@ -353,11 +402,16 @@ github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7X
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
||||
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||
@@ -472,6 +526,8 @@ github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
|
||||
github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
|
||||
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -517,8 +573,16 @@ github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
|
||||
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
|
||||
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
|
||||
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
|
||||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY=
|
||||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
|
||||
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
@@ -542,6 +606,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
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=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
@@ -554,6 +619,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/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs=
|
||||
github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
|
||||
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/v3 v3.13.0 h1:Dr6jqMfIyyFsRVn7Nz5mqLsMY+ZMpfh3a0aMs+umPVY=
|
||||
@@ -562,6 +629,8 @@ github.com/wneessen/go-mail v0.8.1 h1:tVcncj02/QySVFw3zr/kXOzZcuFQqBNT6K+Rbgm/pc
|
||||
github.com/wneessen/go-mail v0.8.1/go.mod h1:dWZ61zadzCIyvB4y1/YzC5O7MrbbzBfPkARmbosdf8w=
|
||||
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/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
@@ -583,8 +652,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.15.0 h1:67OL6f2VrQJ4sBYBLgZPpP4XEbnA5qfCBKbbn46GBoU=
|
||||
gitlab.com/gitlab-org/api/client-go/v3 v3.15.0/go.mod h1:k7uYxRoIeuSqyWZSJfAPZlf0L32OH0lUBImAl4gWUFc=
|
||||
gitlab.com/gitlab-org/api/client-go/v3 v3.13.0 h1:CTUXvcL6OrGjYNafbws5C1lJwlXrxGJHWQGMF+hCdz0=
|
||||
gitlab.com/gitlab-org/api/client-go/v3 v3.13.0/go.mod h1:k7uYxRoIeuSqyWZSJfAPZlf0L32OH0lUBImAl4gWUFc=
|
||||
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.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
@@ -612,6 +681,11 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
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.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
@@ -620,6 +694,10 @@ golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
|
||||
golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
|
||||
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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -628,8 +706,14 @@ 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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
|
||||
@@ -639,6 +723,11 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -647,22 +736,37 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-20220715151400-c0bba94af5f8/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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -670,6 +774,11 @@ 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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
@@ -678,8 +787,11 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
|
||||
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.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU=
|
||||
golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -705,6 +817,8 @@ gopkg.in/ini.v1 v1.67.3 h1:iM9Lhz5MRSGhHVGGwCuzG9KO8PoirCXj/m/qTmOJJQw=
|
||||
gopkg.in/ini.v1 v1.67.3/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -714,10 +828,10 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.29.7 h1:q+NXGJ0bK3b4TXFYQQVr9pYETGnmwFWkrUzJnMya/Tg=
|
||||
modernc.org/cc/v4 v4.29.7/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.36.1 h1:ZNIUZAryN0UgnJwtyxrdEzcFc3yD4Cu4AzjfPXsLsIE=
|
||||
modernc.org/ccgo/v4 v4.36.1/go.mod h1:rrtGc2QkS239nYb/mQNuBMyjq3/y3ZXWbBjPoV3wqzA=
|
||||
modernc.org/cc/v4 v4.29.2 h1:h6+9ciCnPKutf4I03CvheAvDLX7+IHlqR6Iy6J+cgd8=
|
||||
modernc.org/cc/v4 v4.29.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.35.0 h1:F+TUsmw09QxLzmi3aeYYGxjAXarmZaKgj3mKQHNaA8w=
|
||||
modernc.org/ccgo/v4 v4.35.0/go.mod h1:qrVGs9S3Sr2Ztcg9ve+kTAYMp5a3YvWjo+SoN06kJ5I=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
@@ -726,8 +840,8 @@ modernc.org/gc/v3 v3.1.5 h1:21ldfPfRYE31Tb7B3mwAK8gy1AxP4+dKjrOQPfqakoc=
|
||||
modernc.org/gc/v3 v3.1.5/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.77.1 h1:Ct8j47QtiZ1Enj2DtFXQtUqrPCAjdCmPjtCuvrYQ0Hs=
|
||||
modernc.org/libc v1.77.1/go.mod h1:87/pZ4L6nD1zqW4nItuS12YO7hN1igAah34xjnQo/W0=
|
||||
modernc.org/libc v1.75.7 h1:o3DTP9/0p9pKmY2WCKQaySW6wIiZhNM7wc2lUoyhfew=
|
||||
modernc.org/libc v1.75.7/go.mod h1:bO5o2ztHxBb2rjz0PgdHN0sSMw57CgxGFLZ3Qd/QpVQ=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g=
|
||||
@@ -736,8 +850,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.60.1 h1:/blz53O951KWFOso4QQvEs/Fq6cDBKLtMVrYNSeJVKw=
|
||||
modernc.org/sqlite v1.60.1/go.mod h1:1dIoEagfDE72QytD5scH1lxARtaUgKgHC/NuApA27r0=
|
||||
modernc.org/sqlite v1.59.0 h1:X1es1GpqBlS/5T+vbM4HLUdaa8OtQx468DF2vrx+38A=
|
||||
modernc.org/sqlite v1.59.0/go.mod h1:+paeT2A3iPRHkQDwG7oA6Tk0zQd5woMEI8q7orfry8k=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
@@ -748,5 +862,5 @@ pgregory.net/rapid v0.4.2 h1:lsi9jhvZTYvzVpeG93WWgimPRmiJQfGFRNTEZh1dtY0=
|
||||
pgregory.net/rapid v0.4.2/go.mod h1:UYpPVyjFHzYBGHIxLFoupi8vwk6rXNzRY9OMvVxFIOU=
|
||||
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
|
||||
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
||||
xorm.io/xorm v1.4.3 h1:MwWFWzVr+/6D07qGCDhBAfABcuT0gvqY3XmTy1215BM=
|
||||
xorm.io/xorm v1.4.3/go.mod h1:cs0ePc8O4a0jD78cNvD+0VFwhqotTvLQZv372QsDw7Q=
|
||||
xorm.io/xorm v1.4.1 h1:m7QlNd0eBGb31IV4Q/ow0Du83rtdC1CiwlvJZGvYde8=
|
||||
xorm.io/xorm v1.4.1/go.mod h1:cs0ePc8O4a0jD78cNvD+0VFwhqotTvLQZv372QsDw7Q=
|
||||
|
||||
@@ -33,7 +33,6 @@ import (
|
||||
"gitea.dev/modelmigration/v1_8"
|
||||
"gitea.dev/modelmigration/v1_9"
|
||||
"gitea.dev/modelmigration/v28"
|
||||
"gitea.dev/modelmigration/v29"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
@@ -430,10 +429,6 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(353, "Add audit event table", v28.AddAuditEventTable),
|
||||
newMigration(354, "Add Actions job queue indexes", v28.AddActionQueueIndexes),
|
||||
newMigration(355, "Add AutoMerge merged_commit_id column", v28.AddAutoMergeMergedCommitID),
|
||||
// Gitea 28.0.0 ends at migration ID number 355 (database version 356)
|
||||
|
||||
newMigration(356, "Add index on action_run commit_sha", v29.AddActionRunCommitSHAIndex),
|
||||
newMigration(357, "Normalize legacy team authorize values", v29.NormalizeLegacyTeamAuthorize),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v29
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modelmigration/migrationtest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
migrationtest.MainTest(m)
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v29
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.dev/modelmigration/base"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// AddActionRunCommitSHAIndex indexes the runs lookup by commit, which the API `head_sha` filter uses.
|
||||
func AddActionRunCommitSHAIndex(_ context.Context, x base.EngineMigration) error {
|
||||
type ActionRun struct {
|
||||
CommitSHA string `xorm:"index"`
|
||||
}
|
||||
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreDropIndices: true,
|
||||
IgnoreConstrains: true,
|
||||
}, new(ActionRun))
|
||||
return err
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v29
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.dev/modelmigration/base"
|
||||
)
|
||||
|
||||
// NormalizeLegacyTeamAuthorize sets leftover read/write authorize values to none.
|
||||
// https://github.com/go-gitea/gitea/pull/34128 made non-admin teams use team_unit (authorize=none).
|
||||
// Any positive authorize now means blanket access on every unit; migrating legacy read/write
|
||||
// to none preserves their existing team_unit-scoped access.
|
||||
func NormalizeLegacyTeamAuthorize(_ context.Context, x base.EngineMigration) error {
|
||||
// AccessModeNone=0, AccessModeRead=1, AccessModeWrite=2, AccessModeAdmin=3
|
||||
_, err := x.Exec(`
|
||||
UPDATE team SET authorize = 0
|
||||
WHERE authorize > 0 AND authorize < 3
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM team_unit WHERE team_unit.team_id = team.id
|
||||
);`)
|
||||
return err
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v29
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modelmigration/migrationtest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNormalizeLegacyTeamAuthorize(t *testing.T) {
|
||||
type Team struct {
|
||||
ID int64 `xorm:"pk"`
|
||||
Authorize int
|
||||
}
|
||||
type TeamUnit struct {
|
||||
ID int64 `xorm:"pk"`
|
||||
TeamID int64 `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
x, deferrable := migrationtest.PrepareTestEnv(t, 0, new(Team), new(TeamUnit))
|
||||
defer deferrable()
|
||||
if x == nil || t.Failed() {
|
||||
return
|
||||
}
|
||||
|
||||
_, err := x.Insert(
|
||||
&Team{ID: 1, Authorize: 4},
|
||||
&Team{ID: 2, Authorize: 3},
|
||||
&Team{ID: 3, Authorize: 2},
|
||||
&Team{ID: 4, Authorize: 1},
|
||||
&Team{ID: 5, Authorize: 0},
|
||||
|
||||
&TeamUnit{TeamID: 3},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, NormalizeLegacyTeamAuthorize(t.Context(), x))
|
||||
|
||||
get := func(id int64) int {
|
||||
tBean := &Team{ID: id}
|
||||
has, err := x.Get(tBean)
|
||||
require.NoError(t, err)
|
||||
require.True(t, has)
|
||||
return tBean.Authorize
|
||||
}
|
||||
assert.Equal(t, 4, get(1))
|
||||
assert.Equal(t, 3, get(2))
|
||||
assert.Equal(t, 0, get(3)) // has team unit, reset to none
|
||||
assert.Equal(t, 1, get(4)) // no team unit, kept
|
||||
assert.Equal(t, 0, get(5))
|
||||
}
|
||||
@@ -39,9 +39,9 @@ type ActionRun struct {
|
||||
TriggerUserID int64 `xorm:"index"`
|
||||
TriggerUser *user_model.User `xorm:"-"`
|
||||
ScheduleID int64
|
||||
Ref string `xorm:"index"` // the commit/tag/… that caused the run
|
||||
IsRefDeleted bool `xorm:"-"`
|
||||
CommitSHA string `xorm:"index"`
|
||||
Ref string `xorm:"index"` // the commit/tag/… that caused the run
|
||||
IsRefDeleted bool `xorm:"-"`
|
||||
CommitSHA string
|
||||
IsForkPullRequest bool // If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow.
|
||||
NeedApproval bool // may need approval if it's a fork pull request
|
||||
ApprovedBy int64 `xorm:"index"` // who approved
|
||||
|
||||
@@ -200,7 +200,7 @@ func (r *ActionRunner) GenerateAndFillToken() {
|
||||
// CanMatchLabels checks whether the runner's labels can match a job's "runs-on"
|
||||
// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idruns-on
|
||||
func (r *ActionRunner) CanMatchLabels(jobRunsOn []string) bool {
|
||||
return len(jobRunsOn) > 0 && !slices.ContainsFunc(jobRunsOn, func(label string) bool { return !util.SliceContainsString(r.AgentLabels, label, true) })
|
||||
return !slices.ContainsFunc(jobRunsOn, func(label string) bool { return !util.SliceContainsString(r.AgentLabels, label, true) })
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -86,5 +86,4 @@ func TestCanMatchLabelsCaseInsensitive(t *testing.T) {
|
||||
runner := &ActionRunner{AgentLabels: []string{"self-hosted", "Linux", "X64"}}
|
||||
assert.True(t, runner.CanMatchLabels([]string{"SELF-HOSTED", "linux"}))
|
||||
assert.False(t, runner.CanMatchLabels([]string{"linux", "arm64"}))
|
||||
assert.False(t, runner.CanMatchLabels(nil))
|
||||
}
|
||||
|
||||
@@ -298,12 +298,6 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
|
||||
if err := e.Where(cond).Asc("updated", "id").Limit(pickTaskBatchSize).Find(&jobs); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
// A short page means no waiting jobs remain beyond it.
|
||||
isLastPage := len(jobs) < pickTaskBatchSize
|
||||
if !isLastPage {
|
||||
last := jobs[len(jobs)-1] // read before a lost claim bumps Updated
|
||||
cursorUpdated, cursorID = last.Updated, last.ID
|
||||
}
|
||||
|
||||
for _, v := range jobs {
|
||||
if !runner.CanMatchLabels(v.RunsOn) {
|
||||
@@ -319,9 +313,12 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
|
||||
// Another runner claimed this job concurrently; try the next one.
|
||||
}
|
||||
|
||||
if isLastPage {
|
||||
// A short page means no waiting jobs remain beyond it.
|
||||
if len(jobs) < pickTaskBatchSize {
|
||||
return nil, false, nil
|
||||
}
|
||||
last := jobs[len(jobs)-1]
|
||||
cursorUpdated, cursorID = last.Updated, last.ID
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,17 +83,23 @@ func GetActivityStats(ctx context.Context, repo *repo_model.Repository, timeFrom
|
||||
|
||||
// GetActivityStatsTopAuthors returns top author stats for git commits for all branches
|
||||
func GetActivityStatsTopAuthors(ctx context.Context, repo *repo_model.Repository, timeFrom time.Time, count int) ([]*ActivityAuthorData, error) {
|
||||
authors, err := git.GetCodeActivityAuthors(ctx, repo, timeFrom)
|
||||
gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("OpenRepository: %w", err)
|
||||
}
|
||||
defer closer.Close()
|
||||
|
||||
code, err := gitRepo.GetCodeActivityStats(ctx, timeFrom, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("FillFromGit: %w", err)
|
||||
}
|
||||
if authors == nil {
|
||||
if code.Authors == nil {
|
||||
return nil, nil
|
||||
}
|
||||
users := make(map[int64]*ActivityAuthorData)
|
||||
var unknownUserID int64
|
||||
unknownUserAvatarLink := user_model.NewGhostUser().AvatarLink(ctx)
|
||||
for _, v := range authors {
|
||||
for _, v := range code.Authors {
|
||||
if len(v.Email) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"sync"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/consts"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
@@ -136,7 +135,7 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
|
||||
defer f.Close()
|
||||
|
||||
// Note: chmod command does not support in Windows.
|
||||
if !consts.IsWindows {
|
||||
if !setting.IsWindows {
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -84,7 +84,6 @@ var (
|
||||
UserTwoFactorRegenerate = define("user:twofactor:regenerate", "Regenerated two-factor authentication secret for user {scope}.")
|
||||
UserTwoFactorDisable = define("user:twofactor:disable", "Disabled two-factor authentication for user {scope}.")
|
||||
UserWebAuthAdd = define("user:webauth:add", "Added WebAuthn key {credential} for user {scope}.")
|
||||
UserWebAuthRename = define("user:webauth:rename", "Renamed WebAuthn key {previous_credential} of user {scope} to {credential}.")
|
||||
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}.")
|
||||
|
||||
@@ -564,12 +564,6 @@ func (grant *OAuth2Grant) SetNonce(ctx context.Context, nonce string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateGrantScope(ctx context.Context, grant *OAuth2Grant, newScope string) error {
|
||||
grant.Scope = newScope
|
||||
_, err := db.GetEngine(ctx).ID(grant.ID).Cols("scope").Update(grant)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetOAuth2GrantByID returns the grant with the given ID
|
||||
func GetOAuth2GrantByID(ctx context.Context, id int64) (grant *OAuth2Grant, err error) {
|
||||
grant = new(OAuth2Grant)
|
||||
|
||||
+72
-29
@@ -5,6 +5,7 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/timeutil"
|
||||
@@ -12,46 +13,49 @@ import (
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// Session represents a session compatible for go-chi session
|
||||
type Session struct {
|
||||
Key string `xorm:"pk CHAR(16)"` // the limit is from legacy go-chi/session
|
||||
Data []byte `xorm:"BLOB"` // on MySQL this has a maximum size of 64Kb
|
||||
LastAccessTime timeutil.TimeStamp `xorm:"expiry"` // last access time, the field name is from legacy go-chi/session, we don't want to change it at the moment
|
||||
Key string `xorm:"pk CHAR(16)"` // has to be Key to match with go-chi/session
|
||||
Data []byte `xorm:"BLOB"` // on MySQL this has a maximum size of 64Kb - this may need to be increased
|
||||
Expiry timeutil.TimeStamp // has to be Expiry to match with go-chi/session
|
||||
}
|
||||
|
||||
const DbSessionLastAccessTime = "expiry" // maybe we can make a deeper clean up in the future, just keep this PR focused
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(Session))
|
||||
}
|
||||
|
||||
// UpdateSession stores the data of the session with provided id, creating the session only if create is set
|
||||
func UpdateSession(ctx context.Context, key string, data []byte, create bool) error {
|
||||
session := &Session{Key: key, Data: data, LastAccessTime: timeutil.TimeStampNow()}
|
||||
update := func() (int64, error) {
|
||||
return db.GetEngine(ctx).ID(key).Cols("data", DbSessionLastAccessTime).Update(session)
|
||||
}
|
||||
if updated, err := update(); err != nil || updated > 0 || !create {
|
||||
return err
|
||||
}
|
||||
insertErr := db.Insert(ctx, session)
|
||||
if insertErr == nil {
|
||||
return nil
|
||||
}
|
||||
// the row exists if a concurrent request inserted it, or if MySQL reported an unchanged row as not updated
|
||||
if exist, err := db.Exist[Session](ctx, builder.Eq{"`key`": key}); err != nil || !exist {
|
||||
return insertErr
|
||||
}
|
||||
_, err := update()
|
||||
// UpdateSession updates the session with provided id
|
||||
func UpdateSession(ctx context.Context, key string, data []byte) error {
|
||||
_, err := db.GetEngine(ctx).ID(key).Update(&Session{
|
||||
Data: data,
|
||||
Expiry: timeutil.TimeStampNow(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func UpdateSessionLastAccessTime(ctx context.Context, key string) error {
|
||||
_, err := db.GetEngine(ctx).ID(key).Cols(DbSessionLastAccessTime).Update(&Session{LastAccessTime: timeutil.TimeStampNow()})
|
||||
return err
|
||||
// ReadSession reads the data for the provided session
|
||||
func ReadSession(ctx context.Context, key string) (*Session, error) {
|
||||
return db.WithTx2(ctx, func(ctx context.Context) (*Session, error) {
|
||||
session, exist, err := db.Get[Session](ctx, builder.Eq{"`key`": key})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !exist {
|
||||
session = &Session{
|
||||
Key: key,
|
||||
Expiry: timeutil.TimeStampNow(),
|
||||
}
|
||||
if err := db.Insert(ctx, session); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return session, nil
|
||||
})
|
||||
}
|
||||
|
||||
func GetSession(ctx context.Context, key string) (*Session, bool, error) {
|
||||
return db.Get[Session](ctx, builder.Eq{"`key`": key})
|
||||
// ExistSession checks if a session exists
|
||||
func ExistSession(ctx context.Context, key string) (bool, error) {
|
||||
return db.Exist[Session](ctx, builder.Eq{"`key`": key})
|
||||
}
|
||||
|
||||
// DestroySession destroys a session
|
||||
@@ -62,8 +66,47 @@ func DestroySession(ctx context.Context, key string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// RegenerateSession regenerates a session from the old id
|
||||
func RegenerateSession(ctx context.Context, oldKey, newKey string) (*Session, error) {
|
||||
return db.WithTx2(ctx, func(ctx context.Context) (*Session, error) {
|
||||
if has, err := db.Exist[Session](ctx, builder.Eq{"`key`": newKey}); err != nil {
|
||||
return nil, err
|
||||
} else if has {
|
||||
return nil, fmt.Errorf("session Key: %s already exists", newKey)
|
||||
}
|
||||
|
||||
if has, err := db.Exist[Session](ctx, builder.Eq{"`key`": oldKey}); err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
if err := db.Insert(ctx, &Session{
|
||||
Key: oldKey,
|
||||
Expiry: timeutil.TimeStampNow(),
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := db.Exec(ctx, "UPDATE `session` SET `key` = ? WHERE `key`=?", newKey, oldKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s, _, err := db.Get[Session](ctx, builder.Eq{"`key`": newKey})
|
||||
if err != nil {
|
||||
// is not exist, it should be impossible
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
})
|
||||
}
|
||||
|
||||
// CountSessions returns the number of sessions
|
||||
func CountSessions(ctx context.Context) (int64, error) {
|
||||
return db.GetEngine(ctx).Count(&Session{})
|
||||
}
|
||||
|
||||
// CleanupSessions cleans up expired sessions
|
||||
func CleanupSessions(ctx context.Context, maxLifetime int64) error {
|
||||
_, err := db.GetEngine(ctx).Where(DbSessionLastAccessTime+" <= ?", timeutil.TimeStampNow().Add(-maxLifetime)).Delete(&Session{})
|
||||
_, err := db.GetEngine(ctx).Where("expiry <= ?", timeutil.TimeStampNow().Add(-maxLifetime)).Delete(&Session{})
|
||||
return err
|
||||
}
|
||||
|
||||
+4
-22
@@ -150,9 +150,9 @@ func GetWebAuthnCredentialByName(ctx context.Context, uid int64, name string) (*
|
||||
}
|
||||
|
||||
// GetWebAuthnCredentialByID returns WebAuthn credential by id
|
||||
func GetWebAuthnCredentialByID(ctx context.Context, uid, id int64) (*WebAuthnCredential, error) {
|
||||
func GetWebAuthnCredentialByID(ctx context.Context, id int64) (*WebAuthnCredential, error) {
|
||||
cred := new(WebAuthnCredential)
|
||||
if found, err := db.GetEngine(ctx).Where("user_id = ?", uid).ID(id).Get(cred); err != nil {
|
||||
if found, err := db.GetEngine(ctx).ID(id).Get(cred); err != nil {
|
||||
return nil, err
|
||||
} else if !found {
|
||||
return nil, ErrWebAuthnCredentialNotExist{ID: id}
|
||||
@@ -195,26 +195,8 @@ func CreateCredential(ctx context.Context, userID int64, name string, cred *weba
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// RenameCredential renames the user's WebAuthnCredential, names are unique per user regardless of letter case
|
||||
func RenameCredential(ctx context.Context, uid, id int64, name string) (bool, error) {
|
||||
used, err := db.GetEngine(ctx).Where("user_id = ? AND lower_name = ? AND id != ?", uid, strings.ToLower(name), id).Exist(&WebAuthnCredential{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
} else if used {
|
||||
return false, util.ErrorWrapTranslatable(
|
||||
util.NewAlreadyExistErrorf("WebAuthn credential name already exists [uid: %d, name: %s]", uid, name),
|
||||
"settings.webauthn_nickname_been_used",
|
||||
)
|
||||
}
|
||||
updated, err := db.GetEngine(ctx).ID(id).Where("user_id=? AND `name`<>?", uid, name).Cols("name", "lower_name").Update(&WebAuthnCredential{
|
||||
Name: name,
|
||||
LowerName: strings.ToLower(name),
|
||||
})
|
||||
return updated > 0, err
|
||||
}
|
||||
|
||||
// DeleteCredential will delete WebAuthnCredential
|
||||
func DeleteCredential(ctx context.Context, uid, id int64) (bool, error) {
|
||||
had, err := db.GetEngine(ctx).ID(id).Where("user_id = ?", uid).Delete(&WebAuthnCredential{})
|
||||
func DeleteCredential(ctx context.Context, id, userID int64) (bool, error) {
|
||||
had, err := db.GetEngine(ctx).ID(id).Where("user_id = ?", userID).Delete(&WebAuthnCredential{})
|
||||
return had > 0, err
|
||||
}
|
||||
|
||||
@@ -16,15 +16,11 @@ import (
|
||||
func TestGetWebAuthnCredentialByID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
res, err := auth_model.GetWebAuthnCredentialByID(t.Context(), 32, 1)
|
||||
res, err := auth_model.GetWebAuthnCredentialByID(t.Context(), 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "WebAuthn credential", res.Name)
|
||||
|
||||
_, err = auth_model.GetWebAuthnCredentialByID(t.Context(), 99999, 1)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, auth_model.IsErrWebAuthnCredentialNotExist(err))
|
||||
|
||||
_, err = auth_model.GetWebAuthnCredentialByID(t.Context(), 32, 99999)
|
||||
_, err = auth_model.GetWebAuthnCredentialByID(t.Context(), 342432)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, auth_model.IsErrWebAuthnCredentialNotExist(err))
|
||||
}
|
||||
|
||||
@@ -5,9 +5,7 @@ package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
@@ -61,11 +59,6 @@ func InitEngine(ctx context.Context) error {
|
||||
xe.SetMaxIdleConns(setting.Database.MaxIdleConns)
|
||||
xe.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
|
||||
|
||||
if setting.Database.Type.IsMySQL() {
|
||||
// like PostgreSQL and MSSQL, avoids MariaDB snapshot isolation errors
|
||||
xe.SetDefaultTxOptions(&sql.TxOptions{Isolation: sql.LevelReadCommitted})
|
||||
}
|
||||
|
||||
if setting.Database.SlowQueryThreshold > 0 {
|
||||
xe.AddHook(&EngineHook{
|
||||
Threshold: setting.Database.SlowQueryThreshold,
|
||||
@@ -110,10 +103,6 @@ func InitEngineWithMigration(ctx context.Context, migrateFunc func(context.Conte
|
||||
|
||||
preprocessDatabaseCollation(xormEngine)
|
||||
|
||||
if setting.Database.Type.IsMSSQL() {
|
||||
enableMSSQLReadCommittedSnapshot(ctx, xormEngine)
|
||||
}
|
||||
|
||||
// We have to run migrateFunc here in case the user is re-running installation on a previously created DB.
|
||||
// If we do not then table schemas will be changed and there will be conflicts when the migrations run properly.
|
||||
//
|
||||
@@ -136,12 +125,3 @@ func InitEngineWithMigration(ctx context.Context, migrateFunc func(context.Conte
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// enableMSSQLReadCommittedSnapshot stops MSSQL reads waiting on writers, like PostgreSQL and MySQL
|
||||
func enableMSSQLReadCommittedSnapshot(ctx context.Context, engine EngineMigration) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second) // ALTER waits for all other connections to close
|
||||
defer cancel()
|
||||
if _, err := engine.Context(ctx).Exec("IF (SELECT is_read_committed_snapshot_on FROM sys.databases WHERE database_id = DB_ID()) = 0 ALTER DATABASE CURRENT SET READ_COMMITTED_SNAPSHOT ON"); err != nil {
|
||||
log.Error("Unable to set READ_COMMITTED_SNAPSHOT=ON: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,17 +311,17 @@ func (opts *CommitStatusOptions) ToConds() builder.Cond {
|
||||
func (opts *CommitStatusOptions) ToOrders() string {
|
||||
switch opts.SortType {
|
||||
case "oldest":
|
||||
return "created_unix ASC, `index` ASC"
|
||||
return "created_unix ASC"
|
||||
case "recentupdate":
|
||||
return "updated_unix DESC, `index` DESC"
|
||||
return "updated_unix DESC"
|
||||
case "leastupdate":
|
||||
return "updated_unix ASC, `index` ASC"
|
||||
return "updated_unix ASC"
|
||||
case "leastindex":
|
||||
return "`index` DESC"
|
||||
case "highestindex":
|
||||
return "`index` ASC"
|
||||
default:
|
||||
return "created_unix DESC, `index` DESC" // timestamps have 1s resolution, `index` keeps paging stable
|
||||
return "created_unix DESC"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,15 +32,28 @@ func TestGetCommitStatuses(t *testing.T) {
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 5, int(maxResults))
|
||||
var indexes []int64
|
||||
for _, status := range statuses {
|
||||
indexes = append(indexes, status.Index)
|
||||
}
|
||||
assert.Equal(t, []int64{5, 4, 3, 2, 1}, indexes)
|
||||
assert.Equal(t, "deploy/awesomeness", statuses[0].Context)
|
||||
assert.Equal(t, commitstatus.CommitStatusError, statuses[0].State)
|
||||
assert.Len(t, statuses, 5)
|
||||
|
||||
assert.Equal(t, "ci/awesomeness", statuses[0].Context)
|
||||
assert.Equal(t, commitstatus.CommitStatusPending, statuses[0].State)
|
||||
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[0].APIURL(t.Context()))
|
||||
|
||||
assert.Equal(t, "cov/awesomeness", statuses[1].Context)
|
||||
assert.Equal(t, commitstatus.CommitStatusWarning, statuses[1].State)
|
||||
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[1].APIURL(t.Context()))
|
||||
|
||||
assert.Equal(t, "cov/awesomeness", statuses[2].Context)
|
||||
assert.Equal(t, commitstatus.CommitStatusSuccess, statuses[2].State)
|
||||
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[2].APIURL(t.Context()))
|
||||
|
||||
assert.Equal(t, "ci/awesomeness", statuses[3].Context)
|
||||
assert.Equal(t, commitstatus.CommitStatusFailure, statuses[3].State)
|
||||
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[3].APIURL(t.Context()))
|
||||
|
||||
assert.Equal(t, "deploy/awesomeness", statuses[4].Context)
|
||||
assert.Equal(t, commitstatus.CommitStatusError, statuses[4].State)
|
||||
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[4].APIURL(t.Context()))
|
||||
|
||||
statuses, maxResults, err = db.FindAndCount[git_model.CommitStatus](t.Context(), &git_model.CommitStatusOptions{
|
||||
ListOptions: db.ListOptions{Page: 2, PageSize: 50},
|
||||
RepoID: repo1.ID,
|
||||
|
||||
@@ -123,13 +123,23 @@ func (protectBranch *ProtectedBranch) LoadRepo(ctx context.Context) (err error)
|
||||
}
|
||||
|
||||
// CanUserPush returns if some user could push to this protected branch
|
||||
func (protectBranch *ProtectedBranch) CanUserPush(ctx context.Context, user *user_model.User, permissionInRepo access_model.Permission) bool {
|
||||
func (protectBranch *ProtectedBranch) CanUserPush(ctx context.Context, user *user_model.User) bool {
|
||||
if !protectBranch.CanPush {
|
||||
return false
|
||||
}
|
||||
|
||||
if !protectBranch.EnableWhitelist {
|
||||
return permissionInRepo.CanWrite(unit.TypeCode)
|
||||
if err := protectBranch.LoadRepo(ctx); err != nil {
|
||||
log.Error("LoadRepo: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
writeAccess, err := access_model.HasAccessUnit(ctx, user, protectBranch.Repo, unit.TypeCode, perm.AccessModeWrite)
|
||||
if err != nil {
|
||||
log.Error("HasAccessUnit: %v", err)
|
||||
return false
|
||||
}
|
||||
return writeAccess
|
||||
}
|
||||
|
||||
if slices.Contains(protectBranch.WhitelistUserIDs, user.ID) {
|
||||
@@ -150,17 +160,17 @@ func (protectBranch *ProtectedBranch) CanUserPush(ctx context.Context, user *use
|
||||
|
||||
// CanUserForcePush returns if some user could force push to this protected branch
|
||||
// Since force-push extends normal push, we also check if user has regular push access
|
||||
func (protectBranch *ProtectedBranch) CanUserForcePush(ctx context.Context, user *user_model.User, permissionInRepo access_model.Permission) bool {
|
||||
func (protectBranch *ProtectedBranch) CanUserForcePush(ctx context.Context, user *user_model.User) bool {
|
||||
if !protectBranch.CanForcePush {
|
||||
return false
|
||||
}
|
||||
|
||||
if !protectBranch.EnableForcePushAllowlist {
|
||||
return protectBranch.CanUserPush(ctx, user, permissionInRepo)
|
||||
return protectBranch.CanUserPush(ctx, user)
|
||||
}
|
||||
|
||||
if slices.Contains(protectBranch.ForcePushAllowlistUserIDs, user.ID) {
|
||||
return protectBranch.CanUserPush(ctx, user, permissionInRepo)
|
||||
return protectBranch.CanUserPush(ctx, user)
|
||||
}
|
||||
|
||||
if len(protectBranch.ForcePushAllowlistTeamIDs) == 0 {
|
||||
@@ -172,7 +182,7 @@ func (protectBranch *ProtectedBranch) CanUserForcePush(ctx context.Context, user
|
||||
log.Error("IsUserInTeams: %v", err)
|
||||
return false
|
||||
}
|
||||
return in && protectBranch.CanUserPush(ctx, user, permissionInRepo)
|
||||
return in && protectBranch.CanUserPush(ctx, user)
|
||||
}
|
||||
|
||||
// IsUserMergeWhitelisted checks if some user is whitelisted to merge to this branch
|
||||
|
||||
@@ -433,13 +433,12 @@ func GetLabelsByRepoID(ctx context.Context, repoID int64, sortType string, listO
|
||||
case "reversealphabetically":
|
||||
sess.Desc("name")
|
||||
case "leastissues":
|
||||
sess.OrderBy("num_issues - num_closed_issues ASC")
|
||||
sess.Asc("num_issues")
|
||||
case "mostissues":
|
||||
sess.OrderBy("num_issues - num_closed_issues DESC")
|
||||
sess.Desc("num_issues")
|
||||
default:
|
||||
sess.Asc("name")
|
||||
}
|
||||
sess.Asc("id")
|
||||
|
||||
if listOptions.Page > 0 {
|
||||
db.SetSessionPagination(sess, &listOptions)
|
||||
@@ -509,13 +508,12 @@ func GetLabelsByOrgID(ctx context.Context, orgID int64, sortType string, listOpt
|
||||
case "reversealphabetically":
|
||||
sess.Desc("name")
|
||||
case "leastissues":
|
||||
sess.OrderBy("num_issues - num_closed_issues ASC")
|
||||
sess.Asc("num_issues")
|
||||
case "mostissues":
|
||||
sess.OrderBy("num_issues - num_closed_issues DESC")
|
||||
sess.Desc("num_issues")
|
||||
default:
|
||||
sess.Asc("name")
|
||||
}
|
||||
sess.Asc("id")
|
||||
|
||||
if listOptions.Page > 0 {
|
||||
db.SetSessionPagination(sess, &listOptions)
|
||||
|
||||
@@ -191,8 +191,6 @@ func TestGetLabelsInRepoByIDs(t *testing.T) {
|
||||
|
||||
func TestGetLabelsByRepoID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
_, err := db.GetEngine(t.Context()).ID(2).Cols("num_issues", "num_closed_issues").Update(&issues_model.Label{NumIssues: 3, NumClosedIssues: 3})
|
||||
assert.NoError(t, err)
|
||||
testSuccess := func(repoID int64, sortType string, expectedIssueIDs []int64) {
|
||||
labels, err := issues_model.GetLabelsByRepoID(t.Context(), repoID, sortType, db.ListOptions{})
|
||||
assert.NoError(t, err)
|
||||
@@ -260,8 +258,6 @@ func TestGetLabelsInOrgByIDs(t *testing.T) {
|
||||
|
||||
func TestGetLabelsByOrgID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
_, err := db.GetEngine(t.Context()).ID(3).Cols("num_issues", "num_closed_issues").Update(&issues_model.Label{NumIssues: 3, NumClosedIssues: 3})
|
||||
assert.NoError(t, err)
|
||||
testSuccess := func(orgID int64, sortType string, expectedIssueIDs []int64) {
|
||||
labels, err := issues_model.GetLabelsByOrgID(t.Context(), orgID, sortType, db.ListOptions{})
|
||||
assert.NoError(t, err)
|
||||
@@ -275,7 +271,7 @@ func TestGetLabelsByOrgID(t *testing.T) {
|
||||
testSuccess(3, "reversealphabetically", []int64{4, 3})
|
||||
testSuccess(3, "default", []int64{3, 4})
|
||||
|
||||
_, err = issues_model.GetLabelsByOrgID(t.Context(), 0, "leastissues", db.ListOptions{})
|
||||
_, err := issues_model.GetLabelsByOrgID(t.Context(), 0, "leastissues", db.ListOptions{})
|
||||
assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
|
||||
|
||||
_, err = issues_model.GetLabelsByOrgID(t.Context(), -1, "leastissues", db.ListOptions{})
|
||||
|
||||
@@ -407,21 +407,6 @@ func (pr *PullRequest) GetGitHeadRefName() string { // TODO: make it return RefN
|
||||
return git.RefNameFromPullIndex(pr.Index).String()
|
||||
}
|
||||
|
||||
func (pr *PullRequest) GetInstructionsCliArgs() (ret struct {
|
||||
BaseBranchArg string
|
||||
HeadBranchArg string
|
||||
LocalBranchArg string
|
||||
},
|
||||
) {
|
||||
ret.BaseBranchArg = util.ShellEscape(pr.BaseBranch)
|
||||
ret.HeadBranchArg = util.ShellEscape(pr.HeadBranch)
|
||||
ret.LocalBranchArg = ret.HeadBranchArg
|
||||
if pr.HeadRepo != nil && pr.HeadRepoID != pr.BaseRepoID {
|
||||
ret.LocalBranchArg = util.ShellEscape(pr.HeadRepo.OwnerName) + "-" + ret.HeadBranchArg
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// GetReviewCommentsCount returns the number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR)
|
||||
func (pr *PullRequest) GetReviewCommentsCount(ctx context.Context) int {
|
||||
opts := FindCommentsOptions{
|
||||
|
||||
@@ -50,7 +50,7 @@ func (c *commitChecker) IsCommitIDExisting(commitID string) bool {
|
||||
c.gitRepo, c.gitRepoCloser = r, closer
|
||||
}
|
||||
|
||||
exist = c.gitRepo.IsReferenceExist(c.ctx, commitID)
|
||||
exist = c.gitRepo.IsReferenceExist(c.ctx, commitID) // Don't use IsObjectExist since it doesn't support short hashes with gogit edition.
|
||||
c.commitCache[commitID] = exist
|
||||
return exist
|
||||
}
|
||||
|
||||
@@ -78,13 +78,13 @@ func (m *PushMirror) GetRemoteName() string {
|
||||
return m.RemoteName
|
||||
}
|
||||
|
||||
// UpdatePushMirrorSyncStatus updates the sync status (last update time and last error) of the push-mirror
|
||||
func UpdatePushMirrorSyncStatus(ctx context.Context, m *PushMirror) error {
|
||||
_, err := db.GetEngine(ctx).ID(m.ID).Cols("last_update", "last_error").Update(m)
|
||||
// UpdatePushMirror updates the push-mirror
|
||||
func UpdatePushMirror(ctx context.Context, m *PushMirror) error {
|
||||
_, err := db.GetEngine(ctx).ID(m.ID).AllCols().Update(m)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdatePushMirrorInterval updates the sync interval of the push-mirror
|
||||
// UpdatePushMirrorInterval updates the push-mirror
|
||||
func UpdatePushMirrorInterval(ctx context.Context, m *PushMirror) error {
|
||||
_, err := db.GetEngine(ctx).ID(m.ID).Cols("interval").Update(m)
|
||||
return err
|
||||
|
||||
@@ -60,6 +60,7 @@ func SyncDirs(srcPath, destPath string) error {
|
||||
}
|
||||
|
||||
// the keep file is used to keep the directory in a git repository, it doesn't need to be synced
|
||||
// and go-git doesn't work with the ".keep" file (it would report errors like "ref is empty")
|
||||
const keepFile = ".keep"
|
||||
|
||||
// find and delete all untracked files
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/actionslib/pkg/expreval"
|
||||
@@ -127,7 +128,7 @@ func Parse(content []byte, options ...ParseOption) ([]*SingleWorkflow, error) {
|
||||
}
|
||||
}
|
||||
// Keep accepting empty exclude mappings for workflow compatibility, although GitHub rejects them.
|
||||
matrixes, err := (&model.Job{Strategy: job.Strategy.actStrategy()}).MatrixCombinations()
|
||||
matrixes, err := (&model.Job{Strategy: job.Strategy.actStrategy()}).GetMatrixes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getMatrixes: %w", err)
|
||||
}
|
||||
@@ -168,7 +169,7 @@ func ExpandMatrixWithNeeds(jobID string, job *Job, gitCtx *model.GithubContext,
|
||||
if err := job.Strategy.resolve(expreval.New(NewInterpeter(jobID, nil, nil, gitCtx, results, vars, inputs).Evaluate)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matrixes, err := (&model.Job{Strategy: job.Strategy.actStrategy()}).MatrixCombinations()
|
||||
matrixes, err := (&model.Job{Strategy: job.Strategy.actStrategy()}).GetMatrixes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getMatrixes: %w", err)
|
||||
}
|
||||
@@ -223,24 +224,34 @@ func replaceScalars(node *yaml.Node, replace func(string) string) {
|
||||
|
||||
// buildMatrixCombos builds one Job per matrix combination from src, baking the combination into the
|
||||
// strategy and interpolating the name, runs-on and continue-on-error with it.
|
||||
func buildMatrixCombos(jobID string, src *Job, matrixes []model.MatrixCombination, gitCtx *model.GithubContext, results map[string]*JobResult, vars map[string]string, inputs map[string]any) ([]*Job, error) {
|
||||
func buildMatrixCombos(jobID string, src *Job, matrixes []map[string]any, gitCtx *model.GithubContext, results map[string]*JobResult, vars map[string]string, inputs map[string]any) ([]*Job, error) {
|
||||
srcRunsOn := model.RunsOnFromNode(src.RawRunsOn)
|
||||
order, names := make([]int, len(matrixes)), make([]string, len(matrixes))
|
||||
for index, matrix := range matrixes {
|
||||
order[index], names[index] = index, matrixName(matrix.NameValues)
|
||||
order[index], names[index] = index, matrixName(matrix)
|
||||
}
|
||||
slices.SortStableFunc(order, func(a, b int) int { return strings.Compare(names[a], names[b]) })
|
||||
combos := make([]*Job, 0, len(matrixes))
|
||||
var err error
|
||||
for _, index := range order {
|
||||
matrix := matrixes[index].Values
|
||||
matrix := matrixes[index]
|
||||
combo := src.Clone()
|
||||
if combo.Name == "" {
|
||||
combo.Name = jobID
|
||||
}
|
||||
combo.Strategy.RawMatrix = encodeMatrix(matrix)
|
||||
replaceScalars(&combo.Strategy.RawMatrix, escapeExpressions)
|
||||
if src.Strategy.RawMatrix.Kind != 0 {
|
||||
combo.Strategy.JobIndex, combo.Strategy.JobTotal = index, len(matrixes)
|
||||
}
|
||||
evaluator := expreval.New(NewInterpeter(jobID, &combo.Strategy, matrix, gitCtx, results, vars, inputs).Evaluate)
|
||||
if combo.Name, err = jobName(combo.Name, jobID, names[index], evaluator, len(matrix) > 0 || gitCtx != nil); err != nil {
|
||||
if len(matrix) == 0 && gitCtx != nil {
|
||||
combo.Name, err = evaluator.Interpolate(combo.Name)
|
||||
combo.Name = escapeExpressions(combo.Name)
|
||||
} else {
|
||||
combo.Name, err = nameWithMatrix(combo.Name, matrix, evaluator)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("interpolate name for job %q: %w", jobID, err)
|
||||
}
|
||||
if gitCtx != nil { // callers without one don't read runs-on
|
||||
@@ -248,15 +259,14 @@ func buildMatrixCombos(jobID string, src *Job, matrixes []model.MatrixCombinatio
|
||||
if err := evaluator.EvaluateYamlNode(&rawRunsOn); err != nil {
|
||||
return nil, fmt.Errorf("interpolate runs-on for job %q: %w", jobID, err)
|
||||
}
|
||||
if rawRunsOn.Kind != 0 && runsOnProblem(&rawRunsOn) != "" {
|
||||
combo.RawRunsOn = rawRunsOn
|
||||
} else {
|
||||
runsOn := model.RunsOnFromNode(rawRunsOn)
|
||||
for i := range runsOn {
|
||||
runsOn[i] = escapeExpressions(runsOn[i])
|
||||
}
|
||||
combo.RawRunsOn = model.RunsOnNode(runsOn, "")
|
||||
runsOn := model.RunsOnFromNode(rawRunsOn)
|
||||
if len(runsOn) == 0 && len(srcRunsOn) > 0 { // match no runner rather than every runner
|
||||
runsOn = []string{""}
|
||||
}
|
||||
for i := range runsOn {
|
||||
runsOn[i] = escapeExpressions(runsOn[i])
|
||||
}
|
||||
combo.RawRunsOn = model.RunsOnNode(runsOn, "")
|
||||
}
|
||||
if err := evaluator.EvaluateYamlNode(&combo.RawContinueOnError); err != nil {
|
||||
return nil, fmt.Errorf("evaluate continue-on-error for job %q: %w", jobID, err)
|
||||
@@ -314,36 +324,29 @@ func encodeMatrix(matrix map[string]any) yaml.Node {
|
||||
return node
|
||||
}
|
||||
|
||||
// jobName trims names, gives plain text and lone string literals the suffix, and blank names the job ID
|
||||
func jobName(name, jobID, suffix string, evaluator expreval.Evaluator, evaluate bool) (string, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
if literal, ok := expreval.Literal(name); ok {
|
||||
if literal == "" {
|
||||
literal = jobID
|
||||
}
|
||||
return escapeExpressions(literal + suffix), nil
|
||||
}
|
||||
if !evaluate {
|
||||
func nameWithMatrix(name string, m map[string]any, evaluator expreval.Evaluator) (string, error) {
|
||||
if len(m) == 0 {
|
||||
return name, nil
|
||||
}
|
||||
|
||||
name, err := evaluator.Interpolate(name)
|
||||
if name = strings.TrimSpace(name); name == "" {
|
||||
name = jobID
|
||||
if !strings.Contains(name, "${{") || !strings.Contains(name, "}}") {
|
||||
return escapeExpressions(name + " " + matrixName(m)), nil
|
||||
}
|
||||
|
||||
name, err := evaluator.Interpolate(name)
|
||||
return escapeExpressions(name), err
|
||||
}
|
||||
|
||||
// matrixName formats the name suffix, skipping null and empty values
|
||||
func matrixName(values []any) string {
|
||||
var names []string
|
||||
for _, value := range values {
|
||||
if name := exprparser.CoerceToString(value); name != "" {
|
||||
names = append(names, name)
|
||||
}
|
||||
func matrixName(m map[string]any) string {
|
||||
ks := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
ks = append(ks, k)
|
||||
}
|
||||
if len(names) == 0 {
|
||||
return ""
|
||||
sort.Strings(ks)
|
||||
vs := make([]string, 0, len(m))
|
||||
for _, v := range ks {
|
||||
vs = append(vs, fmt.Sprint(m[v]))
|
||||
}
|
||||
return " (" + strings.Join(names, ", ") + ")"
|
||||
|
||||
return fmt.Sprintf("(%s)", strings.Join(vs, ", "))
|
||||
}
|
||||
|
||||
@@ -290,68 +290,17 @@ func TestParseInterpolatesRunName(t *testing.T) {
|
||||
assert.Empty(t, result[0].RunName)
|
||||
}
|
||||
|
||||
func TestParseRunsOnFromJSONKeepsWhatGitHubRejectsForTheJobToFail(t *testing.T) {
|
||||
func TestParseRunsOnFromJSONArray(t *testing.T) {
|
||||
content := []byte("on: push\njobs:\n build:\n runs-on: ${{ fromJSON(vars.RUNNER) }}\n steps: [{run: echo}]\n")
|
||||
_, err := Parse(content)
|
||||
require.NoError(t, err)
|
||||
for runner, want := range map[string][]string{`["self-hosted", "linux"]`: {"self-hosted", "linux"}, "[]": {}, "{}": {}} {
|
||||
for runner, want := range map[string][]string{`["self-hosted", "linux"]`: {"self-hosted", "linux"}, "[]": {""}} {
|
||||
result, err := Parse(content, WithGitContext(&model.GithubContext{}), WithVars(map[string]string{"RUNNER": runner}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result, 1)
|
||||
_, job := result[0].Job()
|
||||
assert.Equal(t, want, job.RunsOn(), runner)
|
||||
}
|
||||
for runner, problem := range map[string]string{`["a"]`: "", `""`: "Unexpected value ''", `[["a"]]`: "A sequence was not expected"} {
|
||||
result, err := Parse(content, WithGitContext(&model.GithubContext{}), WithVars(map[string]string{"RUNNER": runner}))
|
||||
require.NoError(t, err)
|
||||
payload, err := result[0].Marshal()
|
||||
require.NoError(t, err)
|
||||
_, job, err := ParseRawSingleWorkflow(payload)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, problem, job.RunsOnProblem(), runner)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseJobNames(t *testing.T) {
|
||||
result, err := Parse([]byte(`on: push
|
||||
jobs:
|
||||
scalars:
|
||||
strategy: {matrix: {value: [[1.0, true, false, 0, 1000000000000000, '', null, {os: linux}], ['', null]]}}
|
||||
steps: [{run: echo}]
|
||||
trimmed:
|
||||
name: ' Trimmed '
|
||||
strategy: {matrix: {v: [a]}}
|
||||
steps: [{run: echo}]
|
||||
blank:
|
||||
name: ' '
|
||||
steps: [{run: echo}]
|
||||
folded:
|
||||
name: "${{ ' Folded' }}"
|
||||
strategy: {matrix: {v: [a]}}
|
||||
steps: [{run: echo}]
|
||||
computed:
|
||||
name: ${{ format(' {0} ', matrix.missing) }}
|
||||
strategy: {matrix: {v: [a]}}
|
||||
steps: [{run: echo}]
|
||||
padded:
|
||||
name: ${{ format(' {0} ', matrix.v) }}
|
||||
strategy: {matrix: {v: [a]}}
|
||||
steps: [{run: echo}]
|
||||
`), WithGitContext(&model.GithubContext{}))
|
||||
require.NoError(t, err)
|
||||
names := map[string][]string{}
|
||||
for _, parsed := range result {
|
||||
id, job := parsed.Job()
|
||||
names[id] = append(names[id], job.DisplayName())
|
||||
}
|
||||
assert.Equal(t, map[string][]string{
|
||||
"scalars": {"scalars", "scalars (1, true, false, 0, 1E+15, linux)"},
|
||||
"trimmed": {"Trimmed (a)"},
|
||||
"blank": {"blank"},
|
||||
"folded": {" Folded (a)"},
|
||||
"computed": {"computed"},
|
||||
"padded": {"a"},
|
||||
}, names)
|
||||
}
|
||||
|
||||
func TestJobFieldsWithoutMatrix(t *testing.T) {
|
||||
@@ -509,37 +458,6 @@ func TestReadWorkflowJobConditionContexts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateWorkflowStaticJobKindAndRunsOnLikeGitHub(t *testing.T) {
|
||||
for job, want := range map[string]string{
|
||||
"{runs-on: x, steps: [{run: echo}]}": "",
|
||||
"{uses: o/r/.gitea/workflows/c.yml@main}": "",
|
||||
"{runs-on: []}": "",
|
||||
"{runs-on: {}}": "",
|
||||
"{runs-on: {group: org/g, labels: [a, 1]}}": "",
|
||||
"{runs-on: {group: '${{ vars.G }}'}}": "",
|
||||
"{steps: [{run: echo}]}": "Required property is missing: runs-on",
|
||||
"{with: {}}": "Required property is missing: uses",
|
||||
"{runs-on: x, uses: o/r/.gitea/workflows/c.yml@main}": "Unexpected value 'uses'",
|
||||
"{Runs-On: x}": "Unexpected value 'Runs-On'",
|
||||
"{runs-on: ~}": "runs-on: Unexpected value ''",
|
||||
"{runs-on: ['']}": "runs-on: Unexpected value ''",
|
||||
"{runs-on: [[a]]}": "runs-on: A sequence was not expected",
|
||||
"{runs-on: {labels: {a: b}}}": "runs-on: A mapping was not expected",
|
||||
"{runs-on: {foo: x}}": "runs-on: Unexpected value 'foo'",
|
||||
"{runs-on: {group: org/}}": "runs-on: Invalid runs-on group name 'org/'.",
|
||||
"{runs-on: {group: a/b/c}}": "runs-on: Invalid runs-on group name 'a/b/c'. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.",
|
||||
"{if: true}": "There's not enough info to determine what you meant. Add one of these properties: " +
|
||||
"cancel-timeout-minutes, container, continue-on-error, defaults, env, environment, outputs, runs-on, secrets, services, snapshot, steps, timeout-minutes, uses, with",
|
||||
} {
|
||||
_, err := ValidateWorkflowStatic([]byte("on: push\njobs:\n build: " + job + "\n"))
|
||||
if want == "" {
|
||||
assert.NoError(t, err, job)
|
||||
} else {
|
||||
assert.EqualError(t, err, "job build: "+want, job)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectsUnevaluatedMatrixFilters(t *testing.T) {
|
||||
for _, filter := range []string{"include", "exclude"} {
|
||||
t.Run(filter, func(t *testing.T) {
|
||||
|
||||
@@ -174,14 +174,6 @@ func (j *Job) EraseNeeds() *Job {
|
||||
return j
|
||||
}
|
||||
|
||||
// RunsOnProblem returns github.com's error for the job's runs-on, "" if valid.
|
||||
func (j *Job) RunsOnProblem() string {
|
||||
if j.RawRunsOn.Kind == 0 {
|
||||
return ""
|
||||
}
|
||||
return runsOnProblem(&j.RawRunsOn)
|
||||
}
|
||||
|
||||
// RunsOn returns the labels Gitea matches runners against, unescaped like DisplayName.
|
||||
func (j *Job) RunsOn() []string {
|
||||
runsOn := model.RunsOnFromNode(j.RawRunsOn)
|
||||
|
||||
@@ -7,13 +7,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/actionslib/pkg/expreval"
|
||||
"gitea.dev/actionslib/pkg/exprparser"
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
// jobConditionContexts are what github.com gives `jobs.<job_id>.if`, which it decides before the matrix, plus the `gitea` alias.
|
||||
@@ -24,7 +21,7 @@ func ValidateWorkflowStatic(content []byte) ([]*Event, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Keep unknown and case-distinct keys outside of jobs accepted for existing Gitea workflows.
|
||||
// Keep unknown and case-distinct keys accepted for existing Gitea workflows.
|
||||
workflow, err := readWorkflowDoc(doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -36,9 +33,6 @@ func ValidateWorkflowStatic(content []byte) ([]*Event, error) {
|
||||
if err := validateWorkflowStructure(workflow); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateJobKinds(doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var header struct {
|
||||
RunName string `yaml:"run-name"`
|
||||
}
|
||||
@@ -82,6 +76,7 @@ func validateWorkflowStructure(workflow *model.Workflow) error {
|
||||
if job == nil {
|
||||
return fmt.Errorf("job %q has no configuration", id)
|
||||
}
|
||||
// a job without runs-on is accepted and runs on any runner, github.com rejects it
|
||||
for _, dependency := range job.Needs() {
|
||||
if _, ok := workflow.Jobs[dependency]; !ok {
|
||||
return fmt.Errorf("job %q needs unknown job %q", id, dependency)
|
||||
@@ -115,116 +110,3 @@ func validateWorkflowStructure(workflow *model.Workflow) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// job keys of github.com's workflow schema, by the kind of job allowing them
|
||||
var (
|
||||
stepsJobKeys = []string{"cancel-timeout-minutes", "container", "continue-on-error", "defaults", "env", "environment", "outputs", "runs-on", "services", "snapshot", "steps", "timeout-minutes"}
|
||||
callerJobKeys = []string{"secrets", "uses", "with"}
|
||||
sharedJobKeys = []string{"concurrency", "if", "name", "needs", "permissions", "strategy"}
|
||||
)
|
||||
|
||||
// validateJobKinds applies github.com's job kinds, decided by the first kind-specific key.
|
||||
func validateJobKinds(doc *yaml.Node) error {
|
||||
jobs := mappingValue(doc.Content[0], "jobs")
|
||||
for i := 0; i+1 < len(jobs.Content); i += 2 {
|
||||
id, job := jobs.Content[i].Value, jobs.Content[i+1]
|
||||
var required string
|
||||
for j := 0; j+1 < len(job.Content); j += 2 {
|
||||
key := job.Content[j].Value
|
||||
isStepsKey, isCallerKey := slices.Contains(stepsJobKeys, key), slices.Contains(callerJobKeys, key)
|
||||
switch {
|
||||
case required == "runs-on" && isCallerKey, required == "uses" && isStepsKey, !isStepsKey && !isCallerKey && !slices.Contains(sharedJobKeys, key):
|
||||
return fmt.Errorf("job %s: Unexpected value '%s'", id, key)
|
||||
case required == "" && isStepsKey:
|
||||
required = "runs-on"
|
||||
case required == "" && isCallerKey:
|
||||
required = "uses"
|
||||
}
|
||||
}
|
||||
if required == "" {
|
||||
keys := slices.Concat(stepsJobKeys, callerJobKeys)
|
||||
slices.Sort(keys)
|
||||
return fmt.Errorf("job %s: There's not enough info to determine what you meant. Add one of these properties: %s", id, strings.Join(keys, ", "))
|
||||
}
|
||||
value := mappingValue(job, required)
|
||||
if value == nil {
|
||||
return fmt.Errorf("job %s: Required property is missing: %s", id, required)
|
||||
}
|
||||
if required != "runs-on" {
|
||||
continue
|
||||
}
|
||||
if problem := runsOnProblem(value); problem != "" {
|
||||
return fmt.Errorf("job %s: runs-on: %s", id, problem)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runsOnProblem returns github.com's schema error for a runs-on, "" if valid.
|
||||
func runsOnProblem(node *yaml.Node) string {
|
||||
if node.Kind != yaml.MappingNode {
|
||||
return runsOnLabelsProblem(node)
|
||||
}
|
||||
for i := 0; i+1 < len(node.Content); i += 2 {
|
||||
var problem string
|
||||
switch key := node.Content[i].Value; key {
|
||||
case "labels":
|
||||
problem = runsOnLabelsProblem(node.Content[i+1])
|
||||
case "group":
|
||||
problem = runsOnGroupProblem(node.Content[i+1])
|
||||
default:
|
||||
problem = fmt.Sprintf("Unexpected value '%s'", key)
|
||||
}
|
||||
if problem != "" {
|
||||
return problem
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func runsOnLabelsProblem(node *yaml.Node) string {
|
||||
if node.Kind != yaml.SequenceNode {
|
||||
return nonEmptyStringProblem(node)
|
||||
}
|
||||
for _, label := range node.Content {
|
||||
if problem := nonEmptyStringProblem(label); problem != "" {
|
||||
return problem
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func runsOnGroupProblem(node *yaml.Node) string {
|
||||
if problem := nonEmptyStringProblem(node); problem != "" || hasExpression(node.Value) {
|
||||
return problem
|
||||
}
|
||||
switch prefix, name, found := strings.Cut(node.Value, "/"); {
|
||||
case found && name == "":
|
||||
return fmt.Sprintf("Invalid runs-on group name '%s'.", node.Value)
|
||||
case found && (strings.Contains(name, "/") || !slices.Contains([]string{"org", "organization", "ent", "enterprise"}, prefix)):
|
||||
return fmt.Sprintf("Invalid runs-on group name '%s'. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.", node.Value)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// nonEmptyStringProblem mirrors github.com's non-empty-string, which also accepts non-string scalars.
|
||||
func nonEmptyStringProblem(node *yaml.Node) string {
|
||||
switch {
|
||||
case node.Kind == yaml.SequenceNode:
|
||||
return "A sequence was not expected"
|
||||
case node.Kind == yaml.MappingNode:
|
||||
return "A mapping was not expected"
|
||||
case node.Value == "" || node.ShortTag() == "!!null":
|
||||
return "Unexpected value ''"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func mappingValue(node *yaml.Node, key string) *yaml.Node {
|
||||
for i := 0; i+1 < len(node.Content); i += 2 {
|
||||
if node.Content[i].Value == key {
|
||||
return node.Content[i+1]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -30,23 +30,18 @@ jobs:
|
||||
|
||||
func TestReadWorkflowEventsStaticErrors(t *testing.T) {
|
||||
for content, static := range map[string]bool{
|
||||
"on: push\njobs: {}": true,
|
||||
"on: push\njobs: {test: {runs-on: x, needs: absent}}": true,
|
||||
"on: push\njobs: {one: {runs-on: x, needs: two}, two: {runs-on: x, needs: one}}": true,
|
||||
"on: push\njobs: {test: {runs-on: x, strategy: {matrix: {os: []}}}}": true,
|
||||
"on: push\nrun-name: ${{ secrets.TOKEN }}\njobs: {test: {runs-on: x}}": true,
|
||||
"on: push\njobs: {test: {steps: [{run: echo}]}}": true,
|
||||
"on: push\nrun-name: ${{ fromJSON(inputs.x) }}\njobs: {test: {runs-on: x, steps: [{run: echo}]}}": false,
|
||||
"on: push\njobs: {}": true,
|
||||
"on: push\njobs: {test: {needs: absent}}": true,
|
||||
"on: push\njobs: {one: {needs: two}, two: {needs: one}}": true,
|
||||
"on: push\njobs: {test: {strategy: {matrix: {os: []}}}}": true,
|
||||
"on: push\nrun-name: ${{ secrets.TOKEN }}\njobs: {test: {}}": true,
|
||||
"on: push\nrun-name: ${{ fromJSON(inputs.x) }}\njobs: {test: {steps: [{run: echo}]}}": false,
|
||||
} {
|
||||
_, gotStatic, err := readWorkflowEvents([]byte(content))
|
||||
require.Error(t, err, content)
|
||||
assert.Equal(t, static, gotStatic, content)
|
||||
}
|
||||
for _, content := range []string{
|
||||
"on: push\njobs: {test: {runs-on: x, steps: [{run: echo}]}}",
|
||||
"on: push\nrun-name: ${{ github.ref }}\njobs: {test: {runs-on: x}}",
|
||||
"on: push\njobs: {call: {uses: ./.gitea/workflows/called.yml}}",
|
||||
} {
|
||||
for _, content := range []string{"on: push\njobs: {test: {steps: [{run: echo}]}}", "on: push\nrun-name: ${{ github.ref }}\njobs: {test: {}}"} {
|
||||
_, _, err := readWorkflowEvents([]byte(content))
|
||||
assert.NoError(t, err, content)
|
||||
}
|
||||
|
||||
@@ -102,11 +102,11 @@ func NewEmbeddedFS(data []byte) fs.ReadDirFS {
|
||||
efs := &embeddedFS{data: data, files: make(map[string]*embeddedFileInfo)}
|
||||
efs.meta = sync.OnceValue(func() *EmbeddedMeta {
|
||||
var meta EmbeddedMeta
|
||||
_, metaJSON, ok := bytes.CutLast(data, []byte{'\n'})
|
||||
if !ok {
|
||||
p := bytes.LastIndexByte(data, '\n')
|
||||
if p < 0 {
|
||||
return &meta
|
||||
}
|
||||
if err := json.Unmarshal(metaJSON, &meta); err != nil {
|
||||
if err := json.Unmarshal(data[p+1:], &meta); err != nil {
|
||||
panic("embedded file is not valid")
|
||||
}
|
||||
return &meta
|
||||
|
||||
Vendored
+3
-1
@@ -13,6 +13,8 @@ import (
|
||||
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
_ "gitea.com/go-chi/cache/memcache" //nolint:depguard // memcache plugin for cache, it is required for config "ADAPTER=memcache"
|
||||
)
|
||||
|
||||
var defaultCache StringCache
|
||||
@@ -81,7 +83,7 @@ func GetCache() StringCache {
|
||||
|
||||
// GetString returns the key value from cache with callback when no key exists in cache
|
||||
func GetString(key string, getFunc func() (string, error)) (string, error) {
|
||||
if defaultCache == nil || setting.CacheService.TTL <= 0 {
|
||||
if defaultCache == nil || setting.CacheService.TTL == 0 {
|
||||
return getFunc()
|
||||
}
|
||||
cached, exist := defaultCache.Get(key)
|
||||
|
||||
Vendored
-70
@@ -1,70 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/bradfitz/gomemcache/memcache"
|
||||
)
|
||||
|
||||
const memcacheMaxRelativeTTL = 30 * 24 * 60 * 60
|
||||
|
||||
type memcacheCache struct {
|
||||
client *memcache.Client
|
||||
}
|
||||
|
||||
func newMemcacheCache(conn string) (backend, error) {
|
||||
if conn == "" {
|
||||
return nil, errors.New("cache adapter memcache requires [cache] HOST, e.g. 127.0.0.1:11211")
|
||||
}
|
||||
servers := &memcache.ServerList{}
|
||||
if err := servers.SetServers(strings.Split(conn, ";")...); err != nil {
|
||||
return nil, fmt.Errorf("invalid memcache HOST %q: %w", conn, err)
|
||||
}
|
||||
return &memcacheCache{client: memcache.NewFromSelector(servers)}, nil
|
||||
}
|
||||
|
||||
// memcacheExpiration converts a TTL beyond memcached's 30-day relative limit into an absolute unix time
|
||||
func memcacheExpiration(ttl int64) int32 {
|
||||
if ttl > memcacheMaxRelativeTTL {
|
||||
ttl += timeNow().Unix()
|
||||
}
|
||||
return int32(min(ttl, math.MaxInt32))
|
||||
}
|
||||
|
||||
func (c *memcacheCache) Get(key string) (string, bool) {
|
||||
item, err := c.client.Get(key)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return string(item.Value), true
|
||||
}
|
||||
|
||||
func (c *memcacheCache) Put(key, value string, ttl int64) error {
|
||||
return c.client.Set(&memcache.Item{Key: key, Value: []byte(value), Expiration: memcacheExpiration(ttl)})
|
||||
}
|
||||
|
||||
func (c *memcacheCache) Delete(key string) error {
|
||||
if err := c.client.Delete(key); err != nil && !errors.Is(err, memcache.ErrCacheMiss) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *memcacheCache) IsExist(key string) bool {
|
||||
_, err := c.client.Get(key)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (c *memcacheCache) Ping() error {
|
||||
const key = "__gitea_cache_ping"
|
||||
if err := c.Put(key, "ping", 10); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Delete(key)
|
||||
}
|
||||
Vendored
+134
-19
@@ -4,44 +4,159 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/nosql"
|
||||
|
||||
"gitea.com/go-chi/cache" //nolint:depguard // we wrap this package here
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type redisCache struct {
|
||||
client redis.UniversalClient
|
||||
prefix string
|
||||
// RedisCacher represents a redis cache adapter implementation.
|
||||
type RedisCacher struct {
|
||||
c redis.UniversalClient
|
||||
prefix string
|
||||
hsetName string
|
||||
occupyMode bool
|
||||
}
|
||||
|
||||
func newRedisCache(conn string) backend {
|
||||
uri := nosql.ToRedisURI(conn)
|
||||
return &redisCache{
|
||||
client: nosql.GetManager().GetRedisClient(uri.String()),
|
||||
prefix: uri.Query().Get("prefix"),
|
||||
// toStr convert string/int/int64 interface to string. it's only used by the RedisCacher.Put internally
|
||||
func toStr(v any) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
return v
|
||||
case []byte:
|
||||
return string(v)
|
||||
case int:
|
||||
return strconv.FormatInt(int64(v), 10)
|
||||
case int64:
|
||||
return strconv.FormatInt(v, 10)
|
||||
default:
|
||||
return fmt.Sprint(v) // as what the old com.ToStr does in most cases
|
||||
}
|
||||
}
|
||||
|
||||
func (c *redisCache) Get(key string) (string, bool) {
|
||||
value, err := c.client.Get(graceful.GetManager().HammerContext(), c.prefix+key).Result()
|
||||
return value, err == nil
|
||||
// Put puts value (string type) into cache with key and expire time.
|
||||
// If expired is 0, it lives forever.
|
||||
func (c *RedisCacher) Put(key string, val any, expire int64) error {
|
||||
// this function is not well-designed, it only puts string values into cache
|
||||
key = c.prefix + key
|
||||
if expire == 0 {
|
||||
if err := c.c.Set(graceful.GetManager().HammerContext(), key, toStr(val), 0).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
dur := time.Duration(expire) * time.Second
|
||||
if err := c.c.Set(graceful.GetManager().HammerContext(), key, toStr(val), dur).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.occupyMode {
|
||||
return nil
|
||||
}
|
||||
return c.c.HSet(graceful.GetManager().HammerContext(), c.hsetName, key, "0").Err()
|
||||
}
|
||||
|
||||
func (c *redisCache) Put(key, value string, ttl int64) error {
|
||||
return c.client.Set(graceful.GetManager().HammerContext(), c.prefix+key, value, time.Duration(ttl)*time.Second).Err()
|
||||
// Get gets cached value by given key.
|
||||
func (c *RedisCacher) Get(key string) any {
|
||||
val, err := c.c.Get(graceful.GetManager().HammerContext(), c.prefix+key).Result()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (c *redisCache) Delete(key string) error {
|
||||
return c.client.Del(graceful.GetManager().HammerContext(), c.prefix+key).Err()
|
||||
// Delete deletes cached value by given key.
|
||||
func (c *RedisCacher) Delete(key string) error {
|
||||
key = c.prefix + key
|
||||
if err := c.c.Del(graceful.GetManager().HammerContext(), key).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.occupyMode {
|
||||
return nil
|
||||
}
|
||||
return c.c.HDel(graceful.GetManager().HammerContext(), c.hsetName, key).Err()
|
||||
}
|
||||
|
||||
func (c *redisCache) IsExist(key string) bool {
|
||||
return c.client.Exists(graceful.GetManager().HammerContext(), c.prefix+key).Val() == 1
|
||||
// Incr increases cached int-type value by given key as a counter.
|
||||
func (c *RedisCacher) Incr(key string) error {
|
||||
if !c.IsExist(key) {
|
||||
return fmt.Errorf("key '%s' not exist", key)
|
||||
}
|
||||
return c.c.Incr(graceful.GetManager().HammerContext(), c.prefix+key).Err()
|
||||
}
|
||||
|
||||
func (c *redisCache) Ping() error {
|
||||
return c.client.Ping(graceful.GetManager().HammerContext()).Err()
|
||||
// Decr decreases cached int-type value by given key as a counter.
|
||||
func (c *RedisCacher) Decr(key string) error {
|
||||
if !c.IsExist(key) {
|
||||
return fmt.Errorf("key '%s' not exist", key)
|
||||
}
|
||||
return c.c.Decr(graceful.GetManager().HammerContext(), c.prefix+key).Err()
|
||||
}
|
||||
|
||||
// IsExist returns true if cached value exists.
|
||||
func (c *RedisCacher) IsExist(key string) bool {
|
||||
if c.c.Exists(graceful.GetManager().HammerContext(), c.prefix+key).Val() == 1 {
|
||||
return true
|
||||
}
|
||||
|
||||
if !c.occupyMode {
|
||||
c.c.HDel(graceful.GetManager().HammerContext(), c.hsetName, c.prefix+key)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Flush deletes all cached data.
|
||||
func (c *RedisCacher) Flush() error {
|
||||
if c.occupyMode {
|
||||
return c.c.FlushDB(graceful.GetManager().HammerContext()).Err()
|
||||
}
|
||||
|
||||
keys, err := c.c.HKeys(graceful.GetManager().HammerContext(), c.hsetName).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = c.c.Del(graceful.GetManager().HammerContext(), keys...).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.c.Del(graceful.GetManager().HammerContext(), c.hsetName).Err()
|
||||
}
|
||||
|
||||
// StartAndGC starts GC routine based on config string settings.
|
||||
// AdapterConfig: network=tcp,addr=:6379,password=macaron,db=0,pool_size=100,idle_timeout=180,hset_name=MacaronCache,prefix=cache:
|
||||
func (c *RedisCacher) StartAndGC(opts cache.Options) error {
|
||||
c.hsetName = "MacaronCache"
|
||||
c.occupyMode = opts.OccupyMode
|
||||
|
||||
uri := nosql.ToRedisURI(opts.AdapterConfig)
|
||||
|
||||
c.c = nosql.GetManager().GetRedisClient(uri.String())
|
||||
|
||||
for k, v := range uri.Query() {
|
||||
switch k {
|
||||
case "hset_name":
|
||||
c.hsetName = v[0]
|
||||
case "prefix":
|
||||
c.prefix = v[0]
|
||||
}
|
||||
}
|
||||
|
||||
return c.c.Ping(graceful.GetManager().HammerContext()).Err()
|
||||
}
|
||||
|
||||
// Ping tests if the cache is alive.
|
||||
func (c *RedisCacher) Ping() error {
|
||||
return c.c.Ping(graceful.GetManager().HammerContext()).Err()
|
||||
}
|
||||
|
||||
func init() {
|
||||
cache.Register("redis", &RedisCacher{})
|
||||
}
|
||||
|
||||
Vendored
-95
@@ -8,12 +8,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/nosql"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func createTestCache() {
|
||||
@@ -35,98 +32,6 @@ func TestNewContext(t *testing.T) {
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, con)
|
||||
|
||||
_, err = NewStringCache(setting.Cache{Adapter: "memcache"})
|
||||
assert.ErrorContains(t, err, "requires [cache] HOST")
|
||||
}
|
||||
|
||||
func TestStringCacheAdapters(t *testing.T) {
|
||||
now := time.Now()
|
||||
defer test.MockVariableValue(&timeNow, func() time.Time { return now })()
|
||||
cases := []struct {
|
||||
adapter string
|
||||
conns func(t *testing.T) (string, string)
|
||||
}{
|
||||
{adapter: "memory", conns: func(*testing.T) (string, string) { return "", "" }},
|
||||
{adapter: "twoqueue", conns: func(*testing.T) (string, string) { return "100", `{"size":100}` }},
|
||||
{adapter: "redis", conns: func(t *testing.T) (string, string) {
|
||||
conn := test.PrepareTestRedis(t) + "?prefix=gitea-test-cache-"
|
||||
return conn + "first:", conn + "second:"
|
||||
}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.adapter, func(t *testing.T) {
|
||||
firstConn, secondConn := tc.conns(t)
|
||||
first, err := NewStringCache(setting.Cache{Adapter: tc.adapter, Conn: firstConn, Interval: -1})
|
||||
require.NoError(t, err)
|
||||
second, err := NewStringCache(setting.Cache{Adapter: tc.adapter, Conn: secondConn, Interval: -1})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, first.Ping())
|
||||
|
||||
require.NoError(t, first.Put("key", "value", 0))
|
||||
value, ok := first.Get("key")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "value", value)
|
||||
assert.True(t, first.IsExist("key"))
|
||||
assert.False(t, second.IsExist("key"))
|
||||
|
||||
require.NoError(t, first.Delete("key"))
|
||||
_, ok = first.Get("key")
|
||||
assert.False(t, ok)
|
||||
assert.False(t, first.IsExist("key"))
|
||||
require.NoError(t, first.Delete("key"))
|
||||
|
||||
require.NoError(t, first.Put("expiring", "value", 10))
|
||||
if tc.adapter == "redis" {
|
||||
uri := nosql.ToRedisURI(firstConn)
|
||||
ttl := nosql.GetManager().GetRedisClient(uri.String()).TTL(t.Context(), uri.Query().Get("prefix")+"expiring").Val()
|
||||
assert.Positive(t, ttl)
|
||||
assert.LessOrEqual(t, ttl, 10*time.Second)
|
||||
require.NoError(t, first.Delete("expiring"))
|
||||
return
|
||||
}
|
||||
now = now.Add(9 * time.Second)
|
||||
assert.True(t, first.IsExist("expiring"))
|
||||
now = now.Add(time.Second)
|
||||
assert.False(t, first.IsExist("expiring"))
|
||||
_, ok = first.Get("expiring")
|
||||
assert.False(t, ok)
|
||||
require.NoError(t, first.Put("expiring", "renewed", 0))
|
||||
value, ok = first.Get("expiring")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "renewed", value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNegativeItemTTLDisablesOnlyItemTTLCaching(t *testing.T) {
|
||||
createTestCache()
|
||||
defer test.MockVariableValue(&setting.CacheService.TTL, -1)()
|
||||
|
||||
require.NoError(t, defaultCache.Put("key", "stale", 0))
|
||||
require.NoError(t, defaultCache.Put("key", "value", setting.CacheService.TTLSeconds()))
|
||||
assert.False(t, defaultCache.IsExist("key"))
|
||||
|
||||
calls := 0
|
||||
for range 2 {
|
||||
data, err := GetString("key", func() (string, error) {
|
||||
calls++
|
||||
return "value", nil
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "value", data)
|
||||
}
|
||||
assert.Equal(t, 2, calls)
|
||||
assert.False(t, defaultCache.IsExist("key"))
|
||||
|
||||
require.NoError(t, defaultCache.Put("captcha", "value", 600))
|
||||
assert.True(t, defaultCache.IsExist("captcha"))
|
||||
}
|
||||
|
||||
func TestMemcacheExpiration(t *testing.T) {
|
||||
defer test.MockVariableValue(&timeNow, func() time.Time { return time.Unix(1000, 0) })()
|
||||
assert.EqualValues(t, memcacheMaxRelativeTTL, memcacheExpiration(memcacheMaxRelativeTTL))
|
||||
assert.EqualValues(t, 1000+memcacheMaxRelativeTTL+1, memcacheExpiration(memcacheMaxRelativeTTL+1))
|
||||
}
|
||||
|
||||
func TestTest(t *testing.T) {
|
||||
|
||||
Vendored
+169
-87
@@ -4,123 +4,205 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/json"
|
||||
|
||||
mc "gitea.com/go-chi/cache" //nolint:depguard // we wrap this package here
|
||||
lru "github.com/hashicorp/golang-lru/v2"
|
||||
)
|
||||
|
||||
const twoQueueDefaultSize = 50000
|
||||
|
||||
type twoQueueCache struct {
|
||||
cache *lru.TwoQueueCache[string, memoryItem] // wrap the existing thread-safe 2Q (two-queue) cache directly
|
||||
// TwoQueueCache represents a LRU 2Q cache adapter implementation
|
||||
type TwoQueueCache struct {
|
||||
lock sync.Mutex
|
||||
cache *lru.TwoQueueCache[string, any]
|
||||
interval int
|
||||
}
|
||||
|
||||
type twoQueueCacheConfig struct {
|
||||
Size int `json:"size"`
|
||||
RecentRatio float64 `json:"recent_ratio"`
|
||||
GhostRatio float64 `json:"ghost_ratio"`
|
||||
// TwoQueueCacheConfig describes the configuration for TwoQueueCache
|
||||
type TwoQueueCacheConfig struct {
|
||||
Size int `ini:"SIZE" json:"size"`
|
||||
RecentRatio float64 `ini:"RECENT_RATIO" json:"recent_ratio"`
|
||||
GhostRatio float64 `ini:"GHOST_RATIO" json:"ghost_ratio"`
|
||||
}
|
||||
|
||||
func newTwoQueueCache(conn string, gcInterval time.Duration) (backend, error) {
|
||||
lruCache, err := newTwoQueueLRU(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cache := &twoQueueCache{cache: lruCache}
|
||||
startGC(gcInterval, cache.deleteExpired)
|
||||
return cache, nil
|
||||
// MemoryItem represents a memory cache item.
|
||||
type MemoryItem struct {
|
||||
Val any
|
||||
Created int64
|
||||
Timeout int64
|
||||
}
|
||||
|
||||
func newTwoQueueLRU(conn string) (*lru.TwoQueueCache[string, memoryItem], error) {
|
||||
if conn == "" {
|
||||
return lru.New2Q[string, memoryItem](twoQueueDefaultSize)
|
||||
}
|
||||
if size, err := strconv.Atoi(conn); err == nil {
|
||||
return lru.New2Q[string, memoryItem](size)
|
||||
}
|
||||
if !json.Valid([]byte(conn)) {
|
||||
return nil, fmt.Errorf("invalid two-queue cache HOST %q, expected a size or a JSON config", conn)
|
||||
}
|
||||
config := twoQueueCacheConfig{
|
||||
Size: twoQueueDefaultSize,
|
||||
RecentRatio: lru.Default2QRecentRatio,
|
||||
GhostRatio: lru.Default2QGhostEntries,
|
||||
}
|
||||
_ = json.Unmarshal([]byte(conn), &config)
|
||||
return lru.New2QParams[string, memoryItem](config.Size, config.RecentRatio, config.GhostRatio)
|
||||
func (item *MemoryItem) hasExpired() bool {
|
||||
return item.Timeout > 0 &&
|
||||
(time.Now().Unix()-item.Created) >= item.Timeout
|
||||
}
|
||||
|
||||
func (c *twoQueueCache) Get(key string) (string, bool) {
|
||||
item, ok := c.cache.Get(key)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
if item.expired(timeNow()) {
|
||||
c.cache.Remove(key)
|
||||
return "", false
|
||||
}
|
||||
return item.value, true
|
||||
}
|
||||
var _ mc.Cache = &TwoQueueCache{}
|
||||
|
||||
func (c *twoQueueCache) Put(key, value string, ttl int64) error {
|
||||
item := newMemoryItem(value, ttl)
|
||||
// Put puts value into cache with key and expire time.
|
||||
func (c *TwoQueueCache) Put(key string, val any, timeout int64) error {
|
||||
item := &MemoryItem{
|
||||
Val: val,
|
||||
Created: time.Now().Unix(),
|
||||
Timeout: timeout,
|
||||
}
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.cache.Add(key, item)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *twoQueueCache) Delete(key string) error {
|
||||
// Get gets cached value by given key.
|
||||
func (c *TwoQueueCache) Get(key string) any {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
cached, ok := c.cache.Get(key)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
item, ok := cached.(*MemoryItem)
|
||||
|
||||
if !ok || item.hasExpired() {
|
||||
c.cache.Remove(key)
|
||||
return nil
|
||||
}
|
||||
|
||||
return item.Val
|
||||
}
|
||||
|
||||
// Delete deletes cached value by given key.
|
||||
func (c *TwoQueueCache) Delete(key string) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.cache.Remove(key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *twoQueueCache) IsExist(key string) bool {
|
||||
item, ok := c.cache.Peek(key)
|
||||
return ok && !item.expired(timeNow())
|
||||
// Incr increases cached int-type value by given key as a counter.
|
||||
func (c *TwoQueueCache) Incr(key string) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
cached, ok := c.cache.Get(key)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
item, ok := cached.(*MemoryItem)
|
||||
|
||||
if !ok || item.hasExpired() {
|
||||
c.cache.Remove(key)
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
item.Val, err = mc.Incr(item.Val)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *twoQueueCache) Ping() error {
|
||||
// Decr decreases cached int-type value by given key as a counter.
|
||||
func (c *TwoQueueCache) Decr(key string) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
cached, ok := c.cache.Get(key)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
item, ok := cached.(*MemoryItem)
|
||||
|
||||
if !ok || item.hasExpired() {
|
||||
c.cache.Remove(key)
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
item.Val, err = mc.Decr(item.Val)
|
||||
return err
|
||||
}
|
||||
|
||||
// IsExist returns true if cached value exists.
|
||||
func (c *TwoQueueCache) IsExist(key string) bool {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
cached, ok := c.cache.Peek(key)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
item, ok := cached.(*MemoryItem)
|
||||
if !ok || item.hasExpired() {
|
||||
c.cache.Remove(key)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Flush deletes all cached data.
|
||||
func (c *TwoQueueCache) Flush() error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.cache.Purge()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *twoQueueCache) deleteExpired() {
|
||||
now := timeNow()
|
||||
for _, key := range c.cache.Keys() {
|
||||
if item, ok := c.cache.Peek(key); ok && item.expired(now) {
|
||||
// here might be a slight data-race: the item might have been removed or updated by another goroutine between the Peek and Remove calls,
|
||||
// but it's acceptable since the cache is not guaranteed to be 100% accurate and any item can be evicted at any time
|
||||
c.cache.Remove(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type memoryItem struct {
|
||||
value string
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
func newMemoryItem(value string, ttl int64) memoryItem {
|
||||
item := memoryItem{value: value}
|
||||
if ttl > 0 {
|
||||
item.expiresAt = timeNow().Add(time.Duration(ttl) * time.Second)
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func (item memoryItem) expired(now time.Time) bool {
|
||||
return !item.expiresAt.IsZero() && !now.Before(item.expiresAt)
|
||||
}
|
||||
|
||||
func startGC(interval time.Duration, deleteExpired func()) {
|
||||
if interval <= 0 {
|
||||
func (c *TwoQueueCache) checkAndInvalidate(key string) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
cached, ok := c.cache.Peek(key)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
for range time.Tick(interval) {
|
||||
deleteExpired()
|
||||
}
|
||||
}()
|
||||
item, ok := cached.(*MemoryItem)
|
||||
if !ok || item.hasExpired() {
|
||||
c.cache.Remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TwoQueueCache) startGC() {
|
||||
if c.interval < 0 {
|
||||
return
|
||||
}
|
||||
for _, key := range c.cache.Keys() {
|
||||
c.checkAndInvalidate(key)
|
||||
}
|
||||
time.AfterFunc(time.Duration(c.interval)*time.Second, c.startGC)
|
||||
}
|
||||
|
||||
// StartAndGC starts GC routine based on config string settings.
|
||||
func (c *TwoQueueCache) StartAndGC(opts mc.Options) error {
|
||||
var err error
|
||||
size := 50000
|
||||
if opts.AdapterConfig != "" {
|
||||
size, err = strconv.Atoi(opts.AdapterConfig)
|
||||
}
|
||||
if err != nil {
|
||||
if !json.Valid([]byte(opts.AdapterConfig)) {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg := &TwoQueueCacheConfig{
|
||||
Size: 50000,
|
||||
RecentRatio: lru.Default2QRecentRatio,
|
||||
GhostRatio: lru.Default2QGhostEntries,
|
||||
}
|
||||
_ = json.Unmarshal([]byte(opts.AdapterConfig), cfg)
|
||||
c.cache, err = lru.New2QParams[string, any](cfg.Size, cfg.RecentRatio, cfg.GhostRatio)
|
||||
} else {
|
||||
c.cache, err = lru.New2Q[string, any](size)
|
||||
}
|
||||
c.interval = opts.Interval
|
||||
if c.interval > 0 {
|
||||
go c.startGC()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Ping tests if the cache is alive.
|
||||
func (c *TwoQueueCache) Ping() error {
|
||||
return mc.GenericPing(c)
|
||||
}
|
||||
|
||||
func init() {
|
||||
mc.Register("twoqueue", &TwoQueueCache{})
|
||||
}
|
||||
|
||||
Vendored
+37
-34
@@ -5,14 +5,13 @@ package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
chi_cache "gitea.com/go-chi/cache" //nolint:depguard // we wrap this package here
|
||||
)
|
||||
|
||||
type GetJSONError struct {
|
||||
@@ -31,57 +30,57 @@ type StringCache interface {
|
||||
Ping() error
|
||||
|
||||
Get(key string) (string, bool)
|
||||
Put(key, value string, ttl int64) error // ttl in seconds, 0 never expires, negative removes the key
|
||||
Put(key, value string, ttl int64) error
|
||||
Delete(key string) error
|
||||
IsExist(key string) bool
|
||||
|
||||
PutJSON(key string, v any, ttl int64) error
|
||||
GetJSON(key string, ptr any) (exist bool, err *GetJSONError)
|
||||
}
|
||||
|
||||
type backend interface {
|
||||
Get(key string) (string, bool)
|
||||
Put(key, value string, ttl int64) error
|
||||
Delete(key string) error
|
||||
IsExist(key string) bool
|
||||
Ping() error
|
||||
ChiCache() chi_cache.Cache
|
||||
}
|
||||
|
||||
type stringCache struct {
|
||||
backend
|
||||
chiCache chi_cache.Cache
|
||||
}
|
||||
|
||||
func NewStringCache(cacheConfig setting.Cache) (StringCache, error) {
|
||||
cacheBackend, err := newBackend(cacheConfig)
|
||||
adapter := util.IfZero(cacheConfig.Adapter, "memory")
|
||||
interval := util.IfZero(cacheConfig.Interval, 60)
|
||||
cc, err := chi_cache.NewCacher(chi_cache.Options{
|
||||
Adapter: adapter,
|
||||
AdapterConfig: cacheConfig.Conn,
|
||||
Interval: interval,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &stringCache{backend: cacheBackend}, nil
|
||||
return &stringCache{chiCache: cc}, nil
|
||||
}
|
||||
|
||||
func newBackend(cacheConfig setting.Cache) (backend, error) {
|
||||
gcInterval := time.Duration(util.IfZero(cacheConfig.Interval, 60)) * time.Second
|
||||
switch adapter := util.IfZero(cacheConfig.Adapter, "memory"); adapter {
|
||||
case "memory":
|
||||
// the old "memory" adapter doesn't have a limit, which can lead to OOM
|
||||
// now, use two-queue cache for in-memory cache with items limit, at most a few GB of memory will be used
|
||||
return newTwoQueueCache(strconv.FormatInt(10*1024*1024, 10), gcInterval)
|
||||
case "twoqueue":
|
||||
return newTwoQueueCache(cacheConfig.Conn, gcInterval)
|
||||
case "redis":
|
||||
return newRedisCache(cacheConfig.Conn), nil
|
||||
case "memcache":
|
||||
return newMemcacheCache(cacheConfig.Conn)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown cache adapter %q", adapter)
|
||||
func (sc *stringCache) Ping() error {
|
||||
return sc.chiCache.Ping()
|
||||
}
|
||||
|
||||
func (sc *stringCache) Get(key string) (string, bool) {
|
||||
v := sc.chiCache.Get(key)
|
||||
if v == nil {
|
||||
return "", false
|
||||
}
|
||||
s, ok := v.(string)
|
||||
return s, ok
|
||||
}
|
||||
|
||||
func (sc *stringCache) Put(key, value string, ttl int64) error {
|
||||
if ttl < 0 {
|
||||
return sc.backend.Delete(key)
|
||||
}
|
||||
return sc.backend.Put(key, value, ttl)
|
||||
return sc.chiCache.Put(key, value, ttl)
|
||||
}
|
||||
|
||||
func (sc *stringCache) Delete(key string) error {
|
||||
return sc.chiCache.Delete(key)
|
||||
}
|
||||
|
||||
func (sc *stringCache) IsExist(key string) bool {
|
||||
return sc.chiCache.IsExist(key)
|
||||
}
|
||||
|
||||
const cachedErrorPrefix = "<CACHED-ERROR>:"
|
||||
@@ -98,7 +97,7 @@ func (sc *stringCache) PutJSON(key string, v any, ttl int64) error {
|
||||
}
|
||||
s = util.UnsafeBytesToString(b)
|
||||
}
|
||||
return sc.Put(key, s, ttl)
|
||||
return sc.chiCache.Put(key, s, ttl)
|
||||
}
|
||||
|
||||
func (sc *stringCache) GetJSON(key string, ptr any) (exist bool, getErr *GetJSONError) {
|
||||
@@ -115,3 +114,7 @@ func (sc *stringCache) GetJSON(key string, ptr any) (exist bool, getErr *GetJSON
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (sc *stringCache) ChiCache() chi_cache.Cache {
|
||||
return sc.chiCache
|
||||
}
|
||||
|
||||
@@ -1,517 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors.
|
||||
// Copyright 2018-2024 The Ruby Citation File Format Developers. Licensed under the Apache License, Version 2.0
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package citation formats CITATION.cff files, ported from the formatters of ruby-cff 1.3.0
|
||||
package citation
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"maps"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"go.yaml.in/yaml/v4"
|
||||
"golang.org/x/text/runes"
|
||||
"golang.org/x/text/transform"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
type date struct{ time.Time }
|
||||
|
||||
var dateSeparators = strings.NewReplacer("/", "-", ". ", " ", ".", "-", ",", "")
|
||||
|
||||
func (d *date) UnmarshalYAML(node *yaml.Node) error {
|
||||
value := dateSeparators.Replace(node.Value)
|
||||
for _, layout := range []string{"2006-1-2", "2-1-2006", "2 Jan 2006", "2 January 2006", "Jan 2 2006", "January 2 2006"} {
|
||||
if parsed, err := time.Parse(layout, value); err == nil {
|
||||
d.Time = parsed
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type license string
|
||||
|
||||
func (l *license) UnmarshalYAML(node *yaml.Node) error {
|
||||
*l = license(node.Value)
|
||||
if node.Kind != yaml.ScalarNode {
|
||||
*l = license(inspectNode(node))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type actor struct {
|
||||
Name string `yaml:"name"`
|
||||
Alias string `yaml:"alias"`
|
||||
FamilyNames string `yaml:"family-names"`
|
||||
GivenNames string `yaml:"given-names"`
|
||||
NameParticle string `yaml:"name-particle"`
|
||||
NameSuffix string `yaml:"name-suffix"`
|
||||
Affiliation string `yaml:"affiliation"`
|
||||
City string `yaml:"city"`
|
||||
Region string `yaml:"region"`
|
||||
Country string `yaml:"country"`
|
||||
DateStart date `yaml:"date-start"`
|
||||
DateEnd date `yaml:"date-end"`
|
||||
}
|
||||
|
||||
func (a *actor) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
case yaml.ScalarNode:
|
||||
a.Name = node.Value
|
||||
return nil
|
||||
case yaml.SequenceNode:
|
||||
return nil
|
||||
}
|
||||
type plainActor actor
|
||||
return node.Load((*plainActor)(a), yaml.WithUniqueKeys(false))
|
||||
}
|
||||
|
||||
type metadata struct {
|
||||
Type string `yaml:"type"`
|
||||
Title string `yaml:"title"`
|
||||
Authors []actor `yaml:"authors"`
|
||||
Version string `yaml:"version"`
|
||||
DOI string `yaml:"doi"`
|
||||
URL string `yaml:"url"`
|
||||
RepositoryCode string `yaml:"repository-code"`
|
||||
License license `yaml:"license"`
|
||||
DateReleased date `yaml:"date-released"`
|
||||
}
|
||||
|
||||
type reference struct {
|
||||
metadata `yaml:",inline"`
|
||||
isTopLevel bool
|
||||
Editors []actor `yaml:"editors"`
|
||||
EditorsSeries []actor `yaml:"editors-series"`
|
||||
DatePublished date `yaml:"date-published"`
|
||||
Year string `yaml:"year"`
|
||||
Month string `yaml:"month"`
|
||||
Status string `yaml:"status"`
|
||||
Journal string `yaml:"journal"`
|
||||
Volume string `yaml:"volume"`
|
||||
Issue string `yaml:"issue"`
|
||||
Start string `yaml:"start"`
|
||||
End string `yaml:"end"`
|
||||
ISBN string `yaml:"isbn"`
|
||||
Notes string `yaml:"notes"`
|
||||
CollectionTitle string `yaml:"collection-title"`
|
||||
ThesisType string `yaml:"thesis-type"`
|
||||
Publisher actor `yaml:"publisher"`
|
||||
Institution *actor `yaml:"institution"`
|
||||
Conference actor `yaml:"conference"`
|
||||
}
|
||||
|
||||
const (
|
||||
MaxContentSize = 256 * 1024 // parsing takes up to ~1000x the input, largest real-world file found is 80 KiB
|
||||
maxAliasExpansion = 64 * 1024 // nodes plus value bytes aliases may add
|
||||
)
|
||||
|
||||
// FormatCFF returns the APA and BibTeX citations of a CITATION.cff file, both empty if it has no title or authors
|
||||
func FormatCFF(content string) (apa, bibtex string) {
|
||||
var node yaml.Node
|
||||
// the parser copies %TAG prefixes into every node
|
||||
if len(content) > MaxContentSize || strings.Contains(content, "%TAG") || yaml.Unmarshal([]byte(content), &node) != nil || aliasExpansion(&node) > maxAliasExpansion {
|
||||
return "", ""
|
||||
}
|
||||
retagTimestamps(&node)
|
||||
var file struct {
|
||||
TopLevel metadata `yaml:",inline"`
|
||||
PreferredCitation *reference `yaml:"preferred-citation"`
|
||||
}
|
||||
if node.Load(&file, yaml.WithUniqueKeys(false)) != nil {
|
||||
return "", ""
|
||||
}
|
||||
ref := file.PreferredCitation
|
||||
if ref == nil {
|
||||
ref = &reference{metadata: file.TopLevel, isTopLevel: true}
|
||||
}
|
||||
if ref.Title == "" || len(ref.Authors) == 0 {
|
||||
return "", ""
|
||||
}
|
||||
return ref.formatAPA(), ref.formatBibTeX()
|
||||
}
|
||||
|
||||
func retagTimestamps(node *yaml.Node) {
|
||||
if node.ShortTag() == "!!timestamp" {
|
||||
node.Tag = "!!str"
|
||||
}
|
||||
for _, child := range node.Content {
|
||||
retagTimestamps(child)
|
||||
}
|
||||
}
|
||||
|
||||
func aliasExpansion(root *yaml.Node) int {
|
||||
anchors := map[*yaml.Node]int{}
|
||||
added := 0
|
||||
var expandedSize func(node, parent *yaml.Node) int
|
||||
expandedSize = func(node, parent *yaml.Node) int {
|
||||
if node.Kind == yaml.AliasNode {
|
||||
size, walked := anchors[node.Alias]
|
||||
if !walked && (node.Alias != parent || parent.Kind != yaml.SequenceNode) { // decoders never expand a sequence listing itself
|
||||
size = maxAliasExpansion + 1
|
||||
}
|
||||
added = min(added+size, maxAliasExpansion+1)
|
||||
return size
|
||||
}
|
||||
size := 1 + len(node.Value)
|
||||
for _, child := range node.Content {
|
||||
size = min(size+expandedSize(child, node), maxAliasExpansion+1)
|
||||
}
|
||||
if node.Anchor != "" {
|
||||
anchors[node] = size
|
||||
}
|
||||
return size
|
||||
}
|
||||
expandedSize(root, nil)
|
||||
return added
|
||||
}
|
||||
|
||||
func inspectNode(node *yaml.Node) string {
|
||||
var parts []string
|
||||
switch node.Kind {
|
||||
case yaml.AliasNode:
|
||||
if node.Alias.Kind == yaml.ScalarNode { // collection aliases can be recursive
|
||||
return inspectNode(node.Alias)
|
||||
}
|
||||
case yaml.SequenceNode:
|
||||
for _, child := range node.Content {
|
||||
parts = append(parts, inspectNode(child))
|
||||
}
|
||||
return "[" + strings.Join(parts, ", ") + "]"
|
||||
case yaml.MappingNode:
|
||||
for i := 0; i < len(node.Content); i += 2 {
|
||||
parts = append(parts, inspectNode(node.Content[i])+" => "+inspectNode(node.Content[i+1]))
|
||||
}
|
||||
return "{" + strings.Join(parts, ", ") + "}"
|
||||
}
|
||||
if node.ShortTag() == "!!null" {
|
||||
return "nil"
|
||||
}
|
||||
return strconv.Quote(node.Value)
|
||||
}
|
||||
|
||||
var statusNotes = map[string]string{
|
||||
"advance-online": "Advance online publication",
|
||||
"in-preparation": "Manuscript in preparation.",
|
||||
"submitted": "Manuscript submitted for publication.",
|
||||
}
|
||||
|
||||
func joinNonEmpty(sep string, parts ...string) string {
|
||||
return strings.Join(util.SliceRemoveAll(parts, ""), sep)
|
||||
}
|
||||
|
||||
func (r *reference) conferenceDates() (start, end date) {
|
||||
if r.Type == "conference-paper" {
|
||||
return r.Conference.DateStart, r.Conference.DateEnd
|
||||
}
|
||||
return date{}, date{}
|
||||
}
|
||||
|
||||
func (r *reference) monthAndYear() (month, year string) {
|
||||
when, _ := r.conferenceDates()
|
||||
if when.IsZero() {
|
||||
if r.Status == "in-press" {
|
||||
return "", "in press"
|
||||
}
|
||||
if r.Year != "" {
|
||||
return r.Month, r.Year
|
||||
}
|
||||
when = cmp.Or(r.DateReleased, r.DatePublished)
|
||||
}
|
||||
if when.IsZero() {
|
||||
return "", ""
|
||||
}
|
||||
return strconv.Itoa(int(when.Month())), strconv.Itoa(when.Year())
|
||||
}
|
||||
|
||||
func (r *reference) pages(dash string) string {
|
||||
if r.Start == "" || r.End == "" || r.Start == r.End {
|
||||
return r.Start
|
||||
}
|
||||
return r.Start + dash + r.End
|
||||
}
|
||||
|
||||
func (r *reference) volume() string {
|
||||
if r.Volume == "" || r.Issue == "" {
|
||||
return r.Volume
|
||||
}
|
||||
return r.Volume + "(" + r.Issue + ")"
|
||||
}
|
||||
|
||||
func (r *reference) institution() string {
|
||||
if r.Institution == nil {
|
||||
return r.Authors[0].Affiliation
|
||||
}
|
||||
return r.Institution.Name
|
||||
}
|
||||
|
||||
func (r *reference) formatAPA() string {
|
||||
authors := make([]string, 0, len(r.Authors))
|
||||
for _, author := range r.Authors {
|
||||
authors = append(authors, apaAuthor(author))
|
||||
}
|
||||
date := r.apaDate()
|
||||
if date != "" {
|
||||
date = "(" + date + ")"
|
||||
}
|
||||
version := ""
|
||||
if r.Version != "" {
|
||||
version = " (Version " + r.Version + ")"
|
||||
}
|
||||
url := cmp.Or(r.RepositoryCode, r.URL)
|
||||
if r.DOI != "" {
|
||||
url = "https://doi.org/" + r.DOI
|
||||
}
|
||||
return joinNonEmpty(". ", combineAuthors(authors), date, r.Title+version+r.apaTypeLabel(), r.apaPublicationData(), url)
|
||||
}
|
||||
|
||||
func apaAuthor(author actor) string {
|
||||
if author.Name != "" {
|
||||
return author.Name
|
||||
}
|
||||
name := cmp.Or(author.FamilyNames, author.GivenNames, author.Alias)
|
||||
if author.FamilyNames != "" && author.GivenNames != "" {
|
||||
name += ", " + initials(author.GivenNames) + "."
|
||||
}
|
||||
if author.NameParticle != "" {
|
||||
name = author.NameParticle + " " + name
|
||||
}
|
||||
if author.NameSuffix != "" {
|
||||
name += ", " + author.NameSuffix
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func initials(names string) string {
|
||||
parts := splitWords(names)
|
||||
for i, part := range parts {
|
||||
first, _ := utf8.DecodeRuneInString(part)
|
||||
parts[i] = util.ToTitleCase(string(first))
|
||||
}
|
||||
return strings.Join(parts, ". ")
|
||||
}
|
||||
|
||||
func splitWords(text string) []string {
|
||||
return strings.FieldsFunc(text, func(char rune) bool { return char <= unicode.MaxASCII && unicode.IsSpace(char) })
|
||||
}
|
||||
|
||||
func combineAuthors(authors []string) string {
|
||||
if len(authors) == 1 {
|
||||
return strings.TrimSuffix(authors[0], ".")
|
||||
}
|
||||
return strings.TrimSuffix(strings.Join(authors[:len(authors)-1], ", ")+", & "+authors[len(authors)-1], ".")
|
||||
}
|
||||
|
||||
func (r *reference) apaDate() string {
|
||||
start, end := r.conferenceDates()
|
||||
if start.IsZero() || end.IsZero() || !start.Before(end.Time) {
|
||||
_, year := r.monthAndYear()
|
||||
return year
|
||||
}
|
||||
endLayout := "2"
|
||||
if end.Month() != start.Month() {
|
||||
endLayout = "January 2"
|
||||
}
|
||||
if end.Year() != start.Year() {
|
||||
endLayout = "2006, " + endLayout
|
||||
}
|
||||
return start.Format("2006, January 2") + "–" + end.Format(endLayout)
|
||||
}
|
||||
|
||||
func (r *reference) apaTypeLabel() string {
|
||||
switch {
|
||||
case strings.Contains(r.Type, "data"):
|
||||
return " [Data set]"
|
||||
case strings.Contains(r.Type, "conference"):
|
||||
return " [Conference paper]"
|
||||
case !r.isTopLevel && !strings.Contains(r.Type, "software"):
|
||||
return ""
|
||||
}
|
||||
return " [Computer software]"
|
||||
}
|
||||
|
||||
func (r *reference) apaPublicationData() string {
|
||||
switch r.Type {
|
||||
case "article":
|
||||
return joinNonEmpty(", ", r.Journal, r.volume(), r.pages("–"), statusNotes[r.Status])
|
||||
case "book":
|
||||
return r.Publisher.Name
|
||||
case "conference-paper":
|
||||
return joinNonEmpty(", ", r.CollectionTitle, r.volume(), r.pages("–"))
|
||||
case "report":
|
||||
return r.institution()
|
||||
case "phdthesis":
|
||||
return "[" + cmp.Or(r.ThesisType, "Doctoral dissertation") + ", " + r.institution() + "]"
|
||||
case "mastersthesis":
|
||||
return "[" + cmp.Or(r.ThesisType, "Master's thesis") + ", " + r.institution() + "]"
|
||||
case "unpublished":
|
||||
return statusNotes[r.Status]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var bibtexTypeFields = map[string][]string{
|
||||
"article": {"journal", "note", "number", "pages", "volume"},
|
||||
"book": {"address", "editor", "isbn", "number", "pages", "publisher", "volume"},
|
||||
"booklet": {"address"},
|
||||
"inproceedings": {"address", "booktitle", "editor", "pages", "publisher", "series"},
|
||||
"manual": {"address"},
|
||||
"mastersthesis": {"address", "school", "type"},
|
||||
"misc": {"pages"},
|
||||
"phdthesis": {"address", "school", "type"},
|
||||
"proceedings": {"address", "booktitle", "editor", "pages", "publisher", "series"},
|
||||
"software": {"license", "version"},
|
||||
"techreport": {"address", "institution", "number"},
|
||||
"unpublished": {"note"},
|
||||
}
|
||||
|
||||
var globalVars = sync.OnceValue(func() (ret struct {
|
||||
bibtexEscaper *strings.Replacer
|
||||
keyLetters *strings.Replacer
|
||||
keyUnsafeChars *regexp.Regexp
|
||||
bibtexPattern *regexp.Regexp
|
||||
},
|
||||
) {
|
||||
ret.bibtexEscaper = strings.NewReplacer("&", `\&`, "%", `\%`, "$", `\$`, "#", `\#`, "_", `\_`, "{", `\{`, "}", `\}`)
|
||||
ret.keyLetters = strings.NewReplacer(
|
||||
"Æ", "AE", "æ", "ae", "Ð", "D", "ð", "d", "Ø", "O", "ø", "o", "Þ", "Th", "þ", "th", "ß", "ss", "×", "x",
|
||||
"Đ", "D", "đ", "d", "Ħ", "H", "ħ", "h", "ı", "i", "IJ", "IJ", "ij", "ij", "ĸ", "k", "Ŀ", "L", "ŀ", "l",
|
||||
"Ł", "L", "ł", "l", "ʼn", "'n", "Ŋ", "NG", "ŋ", "ng", "Œ", "OE", "œ", "oe", "Ŧ", "T", "ŧ", "t",
|
||||
)
|
||||
ret.keyUnsafeChars = regexp.MustCompile(`[^a-zA-Z0-9-]+`)
|
||||
|
||||
// https://www.acm.org/publications/authors/bibtex-formatting
|
||||
ret.bibtexPattern = regexp.MustCompile(`(?m)^\s*@?\w+\s*{`) // a simple and quick check, no need to be strict
|
||||
return ret
|
||||
})
|
||||
|
||||
func keyToASCII() transform.Transformer {
|
||||
return transform.Chain(
|
||||
runes.Remove(runes.Predicate(func(char rune) bool {
|
||||
return char > unicode.MaxASCII && (char < 'À' || char > 'ž') && char != 'ệ'
|
||||
})),
|
||||
norm.NFD,
|
||||
runes.Remove(runes.Predicate(func(char rune) bool { return char > unicode.MaxASCII })),
|
||||
)
|
||||
}
|
||||
|
||||
func (r *reference) formatBibTeX() string {
|
||||
place := r.Publisher
|
||||
if r.Type == "conference-paper" {
|
||||
place = r.Conference
|
||||
}
|
||||
editors := r.Editors
|
||||
if len(editors) == 0 {
|
||||
editors = r.EditorsSeries
|
||||
}
|
||||
bibtexEscaper := globalVars().bibtexEscaper
|
||||
typeFields := map[string]string{
|
||||
"address": joinNonEmpty(", ", place.City, place.Region, place.Country),
|
||||
"booktitle": bibtexEscaper.Replace(r.CollectionTitle),
|
||||
"editor": bibtexActors(editors),
|
||||
"institution": bibtexEscaper.Replace(r.institution()),
|
||||
"isbn": bibtexEscaper.Replace(r.ISBN),
|
||||
"journal": bibtexEscaper.Replace(r.Journal),
|
||||
"license": bibtexEscaper.Replace(string(r.License)),
|
||||
"note": statusNotes[r.Status],
|
||||
"number": r.Issue,
|
||||
"pages": r.pages("--"),
|
||||
"publisher": bibtexEscaper.Replace(r.Publisher.Name),
|
||||
"school": bibtexEscaper.Replace(r.institution()),
|
||||
"series": bibtexEscaper.Replace(r.Conference.Name),
|
||||
"type": r.ThesisType,
|
||||
"version": bibtexEscaper.Replace(r.Version),
|
||||
"volume": bibtexEscaper.Replace(r.Volume),
|
||||
}
|
||||
entryType := bibtexType(r.Type)
|
||||
fields := map[string]string{
|
||||
"author": bibtexActors(r.Authors),
|
||||
"title": "{" + bibtexEscaper.Replace(r.Title) + "}",
|
||||
"doi": bibtexEscaper.Replace(r.DOI),
|
||||
}
|
||||
for _, name := range bibtexTypeFields[entryType] {
|
||||
fields[name] = typeFields[name]
|
||||
}
|
||||
month, year := r.monthAndYear()
|
||||
if num, _ := strconv.Atoi(month); num >= 1 && num <= 12 {
|
||||
fields["month"] = strings.ToLower(time.Month(num).String()[:3])
|
||||
}
|
||||
fields["year"] = year
|
||||
fields["url"] = cmp.Or(r.RepositoryCode, r.URL)
|
||||
fields["note"] = cmp.Or(fields["note"], r.Notes)
|
||||
maps.DeleteFunc(fields, func(_, value string) bool { return value == "" })
|
||||
|
||||
lines := []string{bibtexKey(fields)}
|
||||
for _, name := range slices.Sorted(maps.Keys(fields)) {
|
||||
value := fields[name]
|
||||
if name != "month" {
|
||||
value = "{" + value + "}"
|
||||
}
|
||||
lines = append(lines, name+" = "+value)
|
||||
}
|
||||
return "@" + entryType + "{" + strings.Join(lines, ",\n") + "\n}"
|
||||
}
|
||||
|
||||
func bibtexType(cffType string) string {
|
||||
if cffType == "" || strings.Contains(cffType, "software") {
|
||||
return "software"
|
||||
}
|
||||
switch cffType {
|
||||
case "article", "book", "manual", "unpublished", "phdthesis", "mastersthesis":
|
||||
return cffType
|
||||
case "conference", "proceedings":
|
||||
return "proceedings"
|
||||
case "conference-paper":
|
||||
return "inproceedings"
|
||||
case "magazine-article", "newspaper-article":
|
||||
return "article"
|
||||
case "pamphlet":
|
||||
return "booklet"
|
||||
case "report":
|
||||
return "techreport"
|
||||
}
|
||||
return "misc"
|
||||
}
|
||||
|
||||
func bibtexActors(actors []actor) string {
|
||||
bibtexEscaper := globalVars().bibtexEscaper
|
||||
names := make([]string, 0, len(actors))
|
||||
for _, entry := range actors {
|
||||
switch {
|
||||
case entry.Name != "":
|
||||
names = append(names, "{"+bibtexEscaper.Replace(entry.Name)+"}")
|
||||
case entry.FamilyNames == "" && entry.GivenNames == "":
|
||||
names = append(names, bibtexEscaper.Replace(entry.Alias))
|
||||
default:
|
||||
family := entry.FamilyNames
|
||||
if entry.NameParticle != "" {
|
||||
family = entry.NameParticle + " " + family
|
||||
}
|
||||
names = append(names, joinNonEmpty(", ", family, entry.NameSuffix, entry.GivenNames))
|
||||
}
|
||||
}
|
||||
return strings.Join(names, " and ")
|
||||
}
|
||||
|
||||
func bibtexKey(fields map[string]string) string {
|
||||
author, _, _ := strings.Cut(fields["author"], ",")
|
||||
titleWords := splitWords(fields["title"])
|
||||
key := joinNonEmpty("_", author, strings.Join(titleWords[:min(3, len(titleWords))], "_"), fields["year"])
|
||||
key, _, _ = transform.String(keyToASCII(), globalVars().keyLetters.Replace(key))
|
||||
return strings.Trim(globalVars().keyUnsafeChars.ReplaceAllString(key, "_"), "_")
|
||||
}
|
||||
|
||||
func IsLikelyBibTeX(content string) bool {
|
||||
return globalVars().bibtexPattern.MatchString(content)
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package citation
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFormatCFF(t *testing.T) {
|
||||
cases := []struct{ cff, apa, bibtex string }{
|
||||
{
|
||||
cff: `title: Overridden
|
||||
message: &mit MIT
|
||||
authors: [{family-names: Haines, given-names: Robert}, {name: "The {curly_braces} Collective"}]
|
||||
title: "Software that uses the following symbols: &, %, $, #"
|
||||
version: 2024-01-16
|
||||
license: [*mit, Apache-2.0]
|
||||
date-released: 2024-01-16
|
||||
`,
|
||||
apa: "Haines, R., & The {curly_braces} Collective. (2024). Software that uses the following symbols: &, %, $, # (Version 2024-01-16) [Computer software]",
|
||||
bibtex: `@software{Haines_Software_that_uses_2024,
|
||||
author = {Haines, Robert and {The \{curly\_braces\} Collective}},
|
||||
license = {["MIT", "Apache-2.0"]},
|
||||
month = jan,
|
||||
title = {{Software that uses the following symbols: \&, \%, \$, \#}},
|
||||
version = {2024-01-16},
|
||||
year = {2024}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
cff: `authors:
|
||||
- family-names: Smith
|
||||
given-names: Arfon M.
|
||||
title: "Software citation principles"
|
||||
license: MIT
|
||||
preferred-citation:
|
||||
authors:
|
||||
- family-names: Smith
|
||||
given-names: A. M.
|
||||
- name: "FORCE11 Software Citation Working Group"
|
||||
doi: "10.7717/peerj-cs.86"
|
||||
journal: "PeerJ Computer Science"
|
||||
month: 9
|
||||
start: e86
|
||||
title: "Software citation principles"
|
||||
type: article
|
||||
volume: 2
|
||||
issue: 123
|
||||
year: 2016
|
||||
publisher:
|
||||
- name: The Open Journal
|
||||
`,
|
||||
apa: "Smith, A. M., & FORCE11 Software Citation Working Group. (2016). Software citation principles. PeerJ Computer Science, 2(123), e86. https://doi.org/10.7717/peerj-cs.86",
|
||||
bibtex: `@article{Smith_Software_citation_principles_2016,
|
||||
author = {Smith, A. M. and {FORCE11 Software Citation Working Group}},
|
||||
doi = {10.7717/peerj-cs.86},
|
||||
journal = {PeerJ Computer Science},
|
||||
month = sep,
|
||||
number = {123},
|
||||
pages = {e86},
|
||||
title = {{Software citation principles}},
|
||||
volume = {2},
|
||||
year = {2016}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
cff: `preferred-citation:
|
||||
type: conference-paper
|
||||
version: 1.10
|
||||
title: "Über tools"
|
||||
authors:
|
||||
- family-names: Ørsted
|
||||
given-names: ǰan christian
|
||||
name-particle: van
|
||||
name-suffix: Jr.
|
||||
collection-title: "Proceedings of X & Y"
|
||||
conference:
|
||||
name: "Conf_2020"
|
||||
city: Berlin
|
||||
country: DE
|
||||
date-start: 30.06.2020
|
||||
date-end: 2020-07-02
|
||||
start: 10
|
||||
end: 20
|
||||
editors:
|
||||
- affiliation: Press
|
||||
editors-series:
|
||||
- family-names: Editor
|
||||
given-names: Eve
|
||||
`,
|
||||
apa: "van Ørsted, J̌. C., Jr. (2020, June 30–July 2). Über tools (Version 1.10) [Conference paper]. Proceedings of X & Y, 10–20",
|
||||
bibtex: `@inproceedings{van_Orsted_Uber_tools_2020,
|
||||
address = {Berlin, DE},
|
||||
author = {van Ørsted, Jr., ǰan christian},
|
||||
booktitle = {Proceedings of X \& Y},
|
||||
month = jun,
|
||||
pages = {10--20},
|
||||
series = {Conf\_2020},
|
||||
title = {{Über tools}},
|
||||
year = {2020}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
cff: `thesis: &thesis {type: phdthesis, title: Thesis}
|
||||
preferred-citation:
|
||||
<<: *thesis
|
||||
authors:
|
||||
- family-names: Nguyễn
|
||||
given-names: Sam
|
||||
affiliation: "Uni_A"
|
||||
institution: {city: Hanoi}
|
||||
license: &loop [*loop]
|
||||
status: in-press
|
||||
notes: A note
|
||||
`,
|
||||
apa: "Nguyễn, S. (in press). Thesis. [Doctoral dissertation, ]",
|
||||
bibtex: `@phdthesis{Nguyn_Thesis_in_press,
|
||||
author = {Nguyễn, Sam},
|
||||
note = {A note},
|
||||
title = {{Thesis}},
|
||||
year = {in press}
|
||||
}`,
|
||||
},
|
||||
{cff: "title: No authors\nauthor:\n - name: Typo\n"},
|
||||
{cff: "title: T\nauthors: [{name: A}]\nmessage: " + strings.Repeat("x", MaxContentSize)},
|
||||
{cff: "title: T\nauthors: [{name: A}]\nmessage: &s " + strings.Repeat("x", maxAliasExpansion/2) + "\nlicense: [*s, *s]\n"},
|
||||
{cff: "%TAG !e! tag:example.com,2000:\n---\ntitle: T\nauthors: [{name: A}]\n"},
|
||||
{cff: "preferred-citation: &m {title: T, name: A, authors: [*m]}\n"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
apa, bibtex := FormatCFF(tc.cff)
|
||||
assert.Equal(t, tc.apa, apa)
|
||||
assert.Equal(t, tc.bibtex, bibtex)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsLikelyBibTeX(t *testing.T) {
|
||||
assert.True(t, IsLikelyBibTeX("@article{key, title={Title}}"))
|
||||
assert.True(t, IsLikelyBibTeX("Inproceedings\n{\n}\n"))
|
||||
assert.True(t, IsLikelyBibTeX("% comment\n\n@misc{key}\n% comment\n"))
|
||||
assert.False(t, IsLikelyBibTeX("not bib {}"))
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package consts
|
||||
|
||||
import "runtime"
|
||||
|
||||
const IsWindows = runtime.GOOS == "windows"
|
||||
@@ -16,8 +16,8 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/consts"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/timeutil"
|
||||
|
||||
"github.com/mholt/archives"
|
||||
@@ -184,7 +184,7 @@ func (dumper *Dumper) Close() error {
|
||||
|
||||
func (dumper *Dumper) normalizeFilePath(absPath string) string {
|
||||
absPath = filepath.Clean(absPath)
|
||||
if consts.IsWindows {
|
||||
if setting.IsWindows {
|
||||
absPath = strings.ToLower(absPath)
|
||||
}
|
||||
return absPath
|
||||
|
||||
@@ -98,7 +98,6 @@ func NewWebhookPolicy() *policy.Policy {
|
||||
}
|
||||
p = policy.NewPolicy("webhook", policyMode(setting.Security.EgressMode),
|
||||
policy.WithAllow(setting.Webhook.AllowedHostList, "security.ALLOWED_HOST_LIST"),
|
||||
policy.WithLocalNeedsIPAllow(),
|
||||
policy.WithProxy(selectProxy))
|
||||
|
||||
return p
|
||||
@@ -107,7 +106,6 @@ func NewWebhookPolicy() *policy.Policy {
|
||||
func NewSecurityPolicy(usage string) *policy.Policy {
|
||||
return policy.NewPolicy(usage, policyMode(setting.Security.EgressMode),
|
||||
policy.WithAllow(setting.Security.AllowedHostList, "security.ALLOWED_HOST_LIST"),
|
||||
policy.WithLocalNeedsIPAllow(),
|
||||
policy.WithProxy(proxy.Proxy()))
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,10 @@
|
||||
package egress
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modules/egress/policy"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
|
||||
@@ -73,27 +70,6 @@ func TestWebhookPolicyProxy(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookPolicyNeedsIPAllow(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Webhook.AllowedHostList, "localhost")()
|
||||
defer test.MockVariableValue(&setting.Security.EgressMode, "lax")()
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = ln.Close() })
|
||||
dial := func() error {
|
||||
tcpAddr, ok := ln.Addr().(*net.TCPAddr)
|
||||
require.True(t, ok)
|
||||
target := net.JoinHostPort("localhost", strconv.Itoa(tcpAddr.Port))
|
||||
conn, err := NewWebhookPolicy().NewDialContext()(t.Context(), "tcp", target)
|
||||
if err == nil {
|
||||
_ = conn.Close()
|
||||
}
|
||||
return err
|
||||
}
|
||||
assert.ErrorIs(t, dial(), policy.ErrDenied) // a host name entry doesn't cover the loopback address
|
||||
setting.Webhook.AllowedHostList = "loopback"
|
||||
assert.NoError(t, dial()) // an IP entry does
|
||||
}
|
||||
|
||||
func TestSecurityPolicy(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Security.AllowedHostList, "avatars.example.com")()
|
||||
defer test.MockVariableValue(&setting.Security.EgressMode, "lax")()
|
||||
|
||||
@@ -411,43 +411,37 @@ var cgnatRange = netip.MustParsePrefix("100.64.0.0/10") // RFC 6598
|
||||
// reservedRanges are never dialable, based on https://microsoft.github.io/AntiSSRF/ipaddressranges.html
|
||||
var reservedRanges = func() (ranges []netip.Prefix) {
|
||||
for _, cidr := range []string{
|
||||
"0.0.0.0/8", // "this network"
|
||||
"168.63.129.16/32", // Azure WireServer
|
||||
"192.88.99.0/24", // 6to4 relay anycast
|
||||
"224.0.0.0/4", // multicast
|
||||
"240.0.0.0/4", // reserved, incl. limited broadcast
|
||||
"::/96", // IPv4-compatible, embeds IPv4
|
||||
"::ffff:0:0:0/96", // IPv4-translated, embeds IPv4
|
||||
"64:ff9b::/96", // wkp NAT64
|
||||
"64:ff9b:1::/48", // local-use NAT64
|
||||
"2001::/32", // Teredo, embeds IPv4
|
||||
"2002::/16", // 6to4, embeds IPv4
|
||||
"ff00::/8", // multicast
|
||||
} {
|
||||
ranges = append(ranges, netip.MustParsePrefix(cidr))
|
||||
}
|
||||
return ranges
|
||||
}()
|
||||
|
||||
// restrictedRanges are dialable if they have been explicitly allowed.
|
||||
var restrictedRanges = func() (ranges []netip.Prefix) {
|
||||
for _, cidr := range []string{
|
||||
"192.0.0.0/24", // IETF protocol assignments
|
||||
"192.0.2.0/24", // TEST-NET-1
|
||||
"192.31.196.0/24", // AS112
|
||||
"192.52.193.0/24", // AMT
|
||||
"192.175.48.0/24", // AS112
|
||||
"198.18.0.0/15", // benchmarking
|
||||
"198.51.100.0/24", // TEST-NET-2
|
||||
"203.0.113.0/24", // TEST-NET-3
|
||||
"100::/64", // discard-only
|
||||
"100:0:0:1::/64", // dummy
|
||||
"2001::/23", // IETF protocol assignments
|
||||
"2001:db8::/32", // documentation
|
||||
"2620:4f:8000::/48", // AS112
|
||||
"3fff::/20", // documentation
|
||||
"5f00::/16", // SRv6 SIDs
|
||||
"fec0::/10", // site-local
|
||||
"0.0.0.0/8", // "this network"
|
||||
"100.100.100.200/32", // Alibaba Cloud metadata
|
||||
"168.63.129.16/32", // Azure WireServer
|
||||
"169.254.0.0/16", // link-local, cloud metadata endpoints
|
||||
"192.0.0.0/24", // IETF protocol assignments
|
||||
"192.0.2.0/24", // TEST-NET-1
|
||||
"192.31.196.0/24", // AS112
|
||||
"192.52.193.0/24", // AMT
|
||||
"192.88.99.0/24", // 6to4 relay anycast
|
||||
"192.175.48.0/24", // AS112
|
||||
"198.18.0.0/15", // benchmarking
|
||||
"198.51.100.0/24", // TEST-NET-2
|
||||
"203.0.113.0/24", // TEST-NET-3
|
||||
"224.0.0.0/4", // multicast
|
||||
"240.0.0.0/4", // reserved, incl. limited broadcast
|
||||
"::/96", // IPv4-compatible, embeds IPv4
|
||||
"::ffff:0:0:0/96", // IPv4-translated, embeds IPv4
|
||||
"64:ff9b::/96", // wkp NAT64
|
||||
"64:ff9b:1::/48", // local-use NAT64
|
||||
"100::/64", // discard-only
|
||||
"100:0:0:1::/64", // dummy
|
||||
"2001::/23", // IETF protocol assignments, incl. Teredo and ORCHID
|
||||
"2001:db8::/32", // documentation
|
||||
"2002::/16", // 6to4, embeds IPv4
|
||||
"2620:4f:8000::/48", // AS112
|
||||
"3fff::/20", // documentation
|
||||
"5f00::/16", // SRv6 SIDs
|
||||
"fd00:ec2::254/128", // AWS IMDS
|
||||
"fe80::/10", // link-local
|
||||
"fec0::/10", // site-local
|
||||
"ff00::/8", // multicast
|
||||
} {
|
||||
ranges = append(ranges, netip.MustParsePrefix(cidr))
|
||||
}
|
||||
@@ -457,14 +451,10 @@ var restrictedRanges = func() (ranges []netip.Prefix) {
|
||||
// classifyAddr reports the class of a canonical address.
|
||||
func classifyAddr(ip netip.Addr) addrClass {
|
||||
switch {
|
||||
case ip.Zone() != "" || !ip.IsLoopback() && inRange(reservedRanges, ip):
|
||||
case ip.Zone() != "" || !ip.IsLoopback() && slices.ContainsFunc(reservedRanges, func(p netip.Prefix) bool { return p.Contains(ip) }):
|
||||
return classReserved
|
||||
case ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || cgnatRange.Contains(ip) || inRange(restrictedRanges, ip):
|
||||
case ip.IsPrivate() || ip.IsLoopback() || cgnatRange.Contains(ip):
|
||||
return classRestricted
|
||||
}
|
||||
return classPublic
|
||||
}
|
||||
|
||||
func inRange(p []netip.Prefix, ip netip.Addr) bool {
|
||||
return slices.ContainsFunc(p, func(p netip.Prefix) bool { return p.Contains(ip) })
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func WithBlock(hostList, key string) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithLocalNeedsIPAllow requires non-public targets (private, loopback, link-local, CGNAT and special-use ranges) to match an IP allow entry (CIDR or named range), a host name match is not enough.
|
||||
// WithLocalNeedsIPAllow requires private, loopback and CGNAT targets to match an IP allow entry (CIDR or named range), a host name match is not enough.
|
||||
func WithLocalNeedsIPAllow() Option {
|
||||
return func(p *Policy) {
|
||||
p.localNeedsIPAllow = true
|
||||
@@ -140,9 +140,9 @@ func (p *Policy) allowCheck(host string, ip netip.AddrPort, class addrClass) err
|
||||
return p.notAllowedError(denyTarget(host, ip))
|
||||
}
|
||||
if !hostnameOk {
|
||||
return fmt.Errorf("%s needs an explicit IP allow entry (non-public address)", denyTarget(host, ip))
|
||||
return fmt.Errorf("%s needs an explicit IP allow entry (private/loopback/CGNAT)", denyTarget(host, ip))
|
||||
}
|
||||
return fmt.Errorf("%s needs an explicit allow entry (non-public address)", denyTarget(host, ip))
|
||||
return fmt.Errorf("%s needs an explicit allow entry (private/loopback/CGNAT)", denyTarget(host, ip))
|
||||
}
|
||||
|
||||
func (p *Policy) blockReason(host string, ip netip.AddrPort) error {
|
||||
|
||||
@@ -33,16 +33,8 @@ func TestCheckAddr(t *testing.T) {
|
||||
{name: "allow host", allow: "example.com", host: "example.com", ip: "8.8.8.8", want: true},
|
||||
{name: "allow cidr", allow: "10.0.0.0/8", ip: "10.0.0.5", want: true},
|
||||
{name: "block overrides allow", allow: "10.0.0.0/8", block: "10.0.0.5/32", ip: "10.0.0.5"},
|
||||
{name: "non cloud link-local is default denied", ip: "::ffff:169.254.1.2"},
|
||||
{name: "link-local allowed by cidr", allow: "169.254.0.0/16", ip: "169.254.169.254", want: true},
|
||||
{name: "link-local allowed ipv4-mapped", allow: "169.254.0.0/16", ip: "::ffff:169.254.169.254", want: true},
|
||||
{name: "restricted range allowed by cidr", allow: "192.0.2.0/24", ip: "192.0.2.1", want: true},
|
||||
{name: "ula metadata allowed by private", allow: "private", ip: "fd00:ec2::254", want: true},
|
||||
{name: "reserved denied despite allow", allow: "168.63.129.16/32", ip: "168.63.129.16"},
|
||||
{name: "reserved denied ipv4-mapped", allow: "168.63.129.16/32", ip: "::ffff:168.63.129.16"},
|
||||
{name: "nat64 reserved denied despite allow", allow: "64:ff9b::/96", ip: "64:ff9b::a9fe:a9fe"},
|
||||
{name: "teredo reserved denied despite allow", allow: "2001::/23", ip: "2001::1"},
|
||||
{name: "protocol assignment allowed by cidr", allow: "2001::/23", ip: "2001:3::1", want: true},
|
||||
{name: "reserved denied by cidr", allow: "169.254.0.0/16", ip: "169.254.169.254"},
|
||||
{name: "reserved denied ipv4-mapped", allow: "169.254.0.0/16", ip: "::ffff:169.254.169.254"},
|
||||
{name: "local gate ignores host", allow: "example.com", host: "example.com", ip: "10.0.0.5", localNeedsIPAllow: true},
|
||||
{name: "local gate accepts builtin", allow: "private", ip: "100.64.0.1", localNeedsIPAllow: true, want: true},
|
||||
{name: "local gate accepts cidr", allow: "10.0.0.0/24", ip: "10.0.0.5", localNeedsIPAllow: true, want: true},
|
||||
@@ -54,8 +46,7 @@ func TestCheckAddr(t *testing.T) {
|
||||
{name: "strict rejects unmatched host", allow: "example.com", host: "other.com", ip: "8.8.8.8", strict: true},
|
||||
{name: "strict allows matched host", allow: "example.com", host: "example.com", ip: "8.8.8.8", strict: true, want: true},
|
||||
{name: "strict block overrides allow", allow: "10.0.0.0/8", block: "10.0.0.5/32", ip: "10.0.0.5", strict: true},
|
||||
{name: "strict reserved denied despite allow", allow: "168.63.129.16/32", ip: "168.63.129.16", strict: true},
|
||||
{name: "strict link-local allowed by cidr", allow: "169.254.0.0/16", ip: "169.254.169.254", strict: true, want: true},
|
||||
{name: "strict reserved denied by cidr", allow: "169.254.0.0/16", ip: "169.254.169.254", strict: true},
|
||||
{name: "strict local gate ignores host", allow: "example.com", host: "example.com", ip: "10.0.0.5", localNeedsIPAllow: true, strict: true},
|
||||
{name: "strict local gate accepts builtin", allow: "private", ip: "100.64.0.1", localNeedsIPAllow: true, strict: true, want: true},
|
||||
} {
|
||||
@@ -72,7 +63,7 @@ func TestCheckAddr(t *testing.T) {
|
||||
mode = Strict
|
||||
}
|
||||
err := NewPolicy("test", mode, opts...).checkAddr(tc.host, netip.AddrPortFrom(addr, 80))
|
||||
assert.Equal(t, tc.want, err == nil, "%s (%s): %v", tc.name, tc.ip, err)
|
||||
assert.Equal(t, tc.want, err == nil, "%s: %v", tc.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,9 +115,8 @@ func TestCheckHostIPs(t *testing.T) {
|
||||
|
||||
builtins := NewPolicy("test", Lax, WithAllow("private, loopback", ""))
|
||||
assert.NoError(t, builtins.checkHostIPs(hostURL(t, "http://example.com"), ips("8.8.8.8", "100.64.0.1", "::1")))
|
||||
assert.NoError(t, builtins.checkHostIPs(hostURL(t, "http://example.com"), ips("100.100.100.200"))) // cloud metadata is opt-in with its containing range
|
||||
for _, ip := range []string{
|
||||
"0.1.2.3", "168.63.129.16", "169.254.169.254", "192.0.2.1", "192.88.99.1", "198.18.0.1",
|
||||
"0.1.2.3", "100.100.100.200", "168.63.129.16", "169.254.169.254", "192.0.2.1", "192.88.99.1", "198.18.0.1",
|
||||
"198.51.100.1", "203.0.113.1", "::7f00:1", "::ffff:0:a00:5", "64:ff9b::a9fe:a9fe", "64:ff9b::808:808", "2001::1", "2001:db8::1",
|
||||
"2002::1", "fe80::1",
|
||||
} {
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
)
|
||||
|
||||
// BatchChecker provides a reader for check-attribute content that can be long running
|
||||
@@ -121,17 +120,12 @@ func (c *BatchChecker) CheckPath(path string) (rs *Attributes, err error) {
|
||||
return fmt.Errorf("CheckPath timeout: %s", debugMsg)
|
||||
}
|
||||
|
||||
timeout := time.NewTimer(5 * time.Second)
|
||||
defer timeout.Stop()
|
||||
|
||||
rs = NewAttributes()
|
||||
for i := 0; i < c.attributesNum; i++ {
|
||||
select {
|
||||
case <-timeout.C:
|
||||
case <-time.After(5 * time.Second):
|
||||
// there is no "hang" problem now. This code is just used to catch other potential problems.
|
||||
err = reportTimeout()
|
||||
setting.PanicInDevOrTesting("Unexpected timeout, need to investigate: %v", err)
|
||||
return nil, err
|
||||
return nil, reportTimeout()
|
||||
case attr, ok := <-c.stdOut.ReadAttribute():
|
||||
if !ok {
|
||||
return nil, c.ctx.Err()
|
||||
|
||||
@@ -15,6 +15,11 @@ func TestReadingBlameOutputSha256(t *testing.T) {
|
||||
setting.AppDataPath = t.TempDir()
|
||||
ctx := t.Context()
|
||||
|
||||
if DefaultFeatures().UsingGogit {
|
||||
t.Skip("Skipping test since gogit does not support sha256")
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("Without .git-blame-ignore-revs", func(t *testing.T) {
|
||||
storage := mockRepository("repo5_pulls_sha256")
|
||||
repo, err := OpenRepository(ctx, storage)
|
||||
|
||||
+2
-96
@@ -12,10 +12,11 @@ import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
// This file contains common functions between the gogit and !gogit variants for git Blobs
|
||||
|
||||
// Name returns name of the tree entry this blob object was created from (or empty string)
|
||||
func (b *Blob) Name() string {
|
||||
return b.name
|
||||
@@ -113,98 +114,3 @@ loop:
|
||||
_ = encoder.Close()
|
||||
return base64buf.String(), nil
|
||||
}
|
||||
|
||||
// Blob represents a Git object.
|
||||
type Blob struct {
|
||||
ID ObjectID
|
||||
|
||||
gotSize bool
|
||||
size int64
|
||||
name string
|
||||
repo *Repository
|
||||
}
|
||||
|
||||
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
|
||||
// Calling the Close function on the result will discard all unread output.
|
||||
func (b *Blob) DataAsync(ctx context.Context) (_ io.ReadCloser, retErr error) {
|
||||
batch, cancel, err := b.repo.CatFileBatch()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
// if there was an error, cancel the batch right away,
|
||||
// otherwise let the caller close it
|
||||
if retErr != nil {
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
info, contentReader, err := batch.QueryContent(b.ID.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.gotSize = true
|
||||
b.size = info.Size
|
||||
return &blobReader{
|
||||
rd: contentReader,
|
||||
n: info.Size,
|
||||
cancel: cancel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Size returns the uncompressed size of the blob
|
||||
func (b *Blob) Size(ctx context.Context) int64 {
|
||||
if b.gotSize {
|
||||
return b.size
|
||||
}
|
||||
|
||||
batch, cancel, err := b.repo.CatFileBatch()
|
||||
if err != nil {
|
||||
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.LogString(), err)
|
||||
return 0
|
||||
}
|
||||
defer cancel()
|
||||
info, err := batch.QueryInfo(b.ID.String())
|
||||
if err != nil {
|
||||
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.LogString(), err)
|
||||
return 0
|
||||
}
|
||||
b.gotSize = true
|
||||
b.size = info.Size
|
||||
return b.size
|
||||
}
|
||||
|
||||
type blobReader struct {
|
||||
rd BufferedReader
|
||||
n int64
|
||||
cancel func()
|
||||
}
|
||||
|
||||
func (b *blobReader) Read(p []byte) (n int, err error) {
|
||||
if b.n <= 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
if int64(len(p)) > b.n {
|
||||
p = p[0:b.n]
|
||||
}
|
||||
n, err = b.rd.Read(p)
|
||||
b.n -= int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Close implements io.Closer
|
||||
func (b *blobReader) Close() error {
|
||||
if b.rd == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer b.cancel()
|
||||
|
||||
if err := DiscardFull(b.rd, b.n+1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.rd = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
// Blob represents a Git object.
|
||||
type Blob struct {
|
||||
ID ObjectID
|
||||
repo *Repository
|
||||
name string
|
||||
}
|
||||
|
||||
func (b *Blob) gogitEncodedObj() (plumbing.EncodedObject, error) {
|
||||
return b.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, plumbing.Hash(b.ID.RawValue()))
|
||||
}
|
||||
|
||||
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
|
||||
// Calling the Close function on the result will discard all unread output.
|
||||
func (b *Blob) DataAsync(_ context.Context) (io.ReadCloser, error) {
|
||||
obj, err := b.gogitEncodedObj()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj.Reader()
|
||||
}
|
||||
|
||||
// Size returns the uncompressed size of the blob
|
||||
func (b *Blob) Size(_ context.Context) int64 {
|
||||
obj, err := b.gogitEncodedObj()
|
||||
if err != nil {
|
||||
log.Error("Error getting gogit encoded object for blob %s(%s): %v", b.name, b.ID.String(), err)
|
||||
return 0
|
||||
}
|
||||
return obj.Size()
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
// Blob represents a Git object.
|
||||
type Blob struct {
|
||||
ID ObjectID
|
||||
|
||||
gotSize bool
|
||||
size int64
|
||||
name string
|
||||
repo *Repository
|
||||
}
|
||||
|
||||
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
|
||||
// Calling the Close function on the result will discard all unread output.
|
||||
func (b *Blob) DataAsync(ctx context.Context) (_ io.ReadCloser, retErr error) {
|
||||
batch, cancel, err := b.repo.CatFileBatch()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
// if there was an error, cancel the batch right away,
|
||||
// otherwise let the caller close it
|
||||
if retErr != nil {
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
info, contentReader, err := batch.QueryContent(b.ID.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.gotSize = true
|
||||
b.size = info.Size
|
||||
return &blobReader{
|
||||
rd: contentReader,
|
||||
n: info.Size,
|
||||
cancel: cancel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Size returns the uncompressed size of the blob
|
||||
func (b *Blob) Size(ctx context.Context) int64 {
|
||||
if b.gotSize {
|
||||
return b.size
|
||||
}
|
||||
|
||||
batch, cancel, err := b.repo.CatFileBatch()
|
||||
if err != nil {
|
||||
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.LogString(), err)
|
||||
return 0
|
||||
}
|
||||
defer cancel()
|
||||
info, err := batch.QueryInfo(b.ID.String())
|
||||
if err != nil {
|
||||
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.LogString(), err)
|
||||
return 0
|
||||
}
|
||||
b.gotSize = true
|
||||
b.size = info.Size
|
||||
return b.size
|
||||
}
|
||||
|
||||
type blobReader struct {
|
||||
rd BufferedReader
|
||||
n int64
|
||||
cancel func()
|
||||
}
|
||||
|
||||
func (b *blobReader) Read(p []byte) (n int, err error) {
|
||||
if b.n <= 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
if int64(len(p)) > b.n {
|
||||
p = p[0:b.n]
|
||||
}
|
||||
n, err = b.rd.Read(p)
|
||||
b.n -= int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Close implements io.Closer
|
||||
func (b *blobReader) Close() error {
|
||||
if b.rd == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer b.cancel()
|
||||
|
||||
if err := DiscardFull(b.rd, b.n+1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.rd = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
)
|
||||
|
||||
func convertPGPSignature(c *object.Commit) *CommitSignature {
|
||||
if c.PGPSignature == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var w strings.Builder
|
||||
var err error
|
||||
|
||||
if _, err = fmt.Fprintf(&w, "tree %s\n", c.TreeHash.String()); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, parent := range c.ParentHashes {
|
||||
if _, err = fmt.Fprintf(&w, "parent %s\n", parent.String()); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = fmt.Fprint(&w, "author "); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = c.Author.Encode(&w); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err = fmt.Fprint(&w, "\ncommitter "); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = c.Committer.Encode(&w); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.Encoding != "" && c.Encoding != "UTF-8" {
|
||||
if _, err = fmt.Fprintf(&w, "\nencoding %s\n", c.Encoding); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = fmt.Fprintf(&w, "\n\n%s", c.Message); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &CommitSignature{
|
||||
Signature: c.PGPSignature,
|
||||
Payload: w.String(),
|
||||
}
|
||||
}
|
||||
|
||||
func convertCommit(c *object.Commit) *Commit {
|
||||
return &Commit{
|
||||
ID: ParseGogitHash(c.Hash),
|
||||
TreeID: ParseGogitHash(c.TreeHash),
|
||||
CommitMessage: CommitMessage{MessageRaw: c.Message},
|
||||
Committer: &c.Committer,
|
||||
Author: &c.Author,
|
||||
Signature: convertPGPSignature(c),
|
||||
Parents: ParseGogitHashArray(c.ParentHashes),
|
||||
}
|
||||
}
|
||||
@@ -113,36 +113,3 @@ func getLastCommitForPathsByCache(ctx context.Context, commitID, treePath string
|
||||
|
||||
return results, unHitEntryPaths, nil
|
||||
}
|
||||
|
||||
// GetLastCommitForPaths returns last commit information
|
||||
func GetLastCommitForPaths(ctx context.Context, gitRepo *Repository, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) {
|
||||
// We read backwards from the commit to obtain all of the commits
|
||||
revs, err := walkGitLog(ctx, gitRepo, commit, treePath, paths...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
commitsMap := map[string]*Commit{}
|
||||
commitsMap[commit.ID.String()] = commit
|
||||
|
||||
commitCommits := map[string]*Commit{}
|
||||
for path, commitID := range revs {
|
||||
if len(commitID) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
c, ok := commitsMap[commitID]
|
||||
if ok {
|
||||
commitCommits[path] = c
|
||||
continue
|
||||
}
|
||||
|
||||
c, err := gitRepo.GetCommit(ctx, commitID) // Ensure the commit exists in the repository
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commitCommits[path] = c
|
||||
}
|
||||
|
||||
return commitCommits, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path"
|
||||
|
||||
"github.com/emirpasic/gods/trees/binaryheap"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph"
|
||||
)
|
||||
|
||||
type commitAndPaths struct {
|
||||
commit cgobject.CommitNode
|
||||
// Paths that are still on the branch represented by commit
|
||||
paths []string
|
||||
// Set of hashes for the paths
|
||||
hashes map[string]plumbing.Hash
|
||||
}
|
||||
|
||||
func getCommitTree(c cgobject.CommitNode, treePath string) (*object.Tree, error) {
|
||||
tree, err := c.Tree()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Optimize deep traversals by focusing only on the specific tree
|
||||
if treePath != "" {
|
||||
tree, err = tree.Tree(treePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return tree, nil
|
||||
}
|
||||
|
||||
func getFileHashes(c cgobject.CommitNode, treePath string, paths []string) (map[string]plumbing.Hash, error) {
|
||||
tree, err := getCommitTree(c, treePath)
|
||||
if err == object.ErrDirectoryNotFound {
|
||||
// The whole tree didn't exist, so return empty map
|
||||
return make(map[string]plumbing.Hash), nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hashes := make(map[string]plumbing.Hash)
|
||||
for _, path := range paths {
|
||||
if path != "" {
|
||||
entry, err := tree.FindEntry(path)
|
||||
if err == nil {
|
||||
hashes[path] = entry.Hash
|
||||
}
|
||||
} else {
|
||||
hashes[path] = tree.Hash
|
||||
}
|
||||
}
|
||||
|
||||
return hashes, nil
|
||||
}
|
||||
|
||||
// GetLastCommitForPaths returns last commit information
|
||||
func GetLastCommitForPaths(ctx context.Context, gitRepo *Repository, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) {
|
||||
commitNodeIndex, closer := gitRepo.CommitNodeIndex()
|
||||
defer closer()
|
||||
|
||||
c, err := commitNodeIndex.Get(plumbing.Hash(commit.ID.RawValue()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return getLastCommitForPathsByCommitNode(ctx, gitRepo, c, treePath, paths)
|
||||
}
|
||||
|
||||
func getLastCommitForPathsByCommitNode(ctx context.Context, gitRepo *Repository, c cgobject.CommitNode, treePath string, paths []string) (map[string]*Commit, error) {
|
||||
refSha := c.ID().String()
|
||||
|
||||
// We do a tree traversal with nodes sorted by commit time
|
||||
heap := binaryheap.NewWith(func(a, b any) int {
|
||||
if a.(*commitAndPaths).commit.CommitTime().Before(b.(*commitAndPaths).commit.CommitTime()) { //nolint:forcetypeassert // this heap only ever holds *commitAndPaths
|
||||
return 1
|
||||
}
|
||||
return -1
|
||||
})
|
||||
|
||||
resultNodes := make(map[string]cgobject.CommitNode)
|
||||
initialHashes, err := getFileHashes(c, treePath, paths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Start search from the root commit and with full set of paths
|
||||
heap.Push(&commitAndPaths{c, paths, initialHashes})
|
||||
heaploop:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
break heaploop
|
||||
}
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
cIn, ok := heap.Pop()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
current := cIn.(*commitAndPaths) //nolint:forcetypeassert // this heap only ever holds *commitAndPaths
|
||||
|
||||
// Load the parent commits for the one we are currently examining
|
||||
numParents := current.commit.NumParents()
|
||||
var parents []cgobject.CommitNode
|
||||
for i := range numParents {
|
||||
parent, err := current.commit.ParentNode(i)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
parents = append(parents, parent)
|
||||
}
|
||||
|
||||
// Examine the current commit and set of interesting paths
|
||||
pathUnchanged := make([]bool, len(current.paths))
|
||||
parentHashes := make([]map[string]plumbing.Hash, len(parents))
|
||||
for j, parent := range parents {
|
||||
parentHashes[j], err = getFileHashes(parent, treePath, current.paths)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
for i, path := range current.paths {
|
||||
if parentHashes[j][path] == current.hashes[path] {
|
||||
pathUnchanged[i] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var remainingPaths []string
|
||||
for i, pth := range current.paths {
|
||||
// The results could already contain some newer change for the same path,
|
||||
// so don't override that and bail out on the file early.
|
||||
if resultNodes[pth] == nil {
|
||||
if pathUnchanged[i] {
|
||||
// The path existed with the same hash in at least one parent so it could
|
||||
// not have been changed in this commit directly.
|
||||
remainingPaths = append(remainingPaths, pth)
|
||||
} else {
|
||||
// There are few possible cases how can we get here:
|
||||
// - The path didn't exist in any parent, so it must have been created by
|
||||
// this commit.
|
||||
// - The path did exist in the parent commit, but the hash of the file has
|
||||
// changed.
|
||||
// - We are looking at a merge commit and the hash of the file doesn't
|
||||
// match any of the hashes being merged. This is more common for directories,
|
||||
// but it can also happen if a file is changed through conflict resolution.
|
||||
resultNodes[pth] = current.commit
|
||||
if err := gitRepo.LastCommitCache.Put(refSha, path.Join(treePath, pth), current.commit.ID().String()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(remainingPaths) > 0 {
|
||||
// Add the parent nodes along with remaining paths to the heap for further
|
||||
// processing.
|
||||
for j, parent := range parents {
|
||||
// Combine remainingPath with paths available on the parent branch
|
||||
// and make union of them
|
||||
remainingPathsForParent := make([]string, 0, len(remainingPaths))
|
||||
newRemainingPaths := make([]string, 0, len(remainingPaths))
|
||||
for _, path := range remainingPaths {
|
||||
if parentHashes[j][path] == current.hashes[path] {
|
||||
remainingPathsForParent = append(remainingPathsForParent, path)
|
||||
} else {
|
||||
newRemainingPaths = append(newRemainingPaths, path)
|
||||
}
|
||||
}
|
||||
|
||||
if remainingPathsForParent != nil {
|
||||
heap.Push(&commitAndPaths{parent, remainingPathsForParent, parentHashes[j]})
|
||||
}
|
||||
|
||||
if len(newRemainingPaths) == 0 {
|
||||
break
|
||||
}
|
||||
remainingPaths = newRemainingPaths
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post-processing
|
||||
result := make(map[string]*Commit)
|
||||
for path, commitNode := range resultNodes {
|
||||
commit, err := commitNode.Commit()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[path] = convertCommit(commit)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// GetLastCommitForPaths returns last commit information
|
||||
func GetLastCommitForPaths(ctx context.Context, gitRepo *Repository, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) {
|
||||
// We read backwards from the commit to obtain all of the commits
|
||||
revs, err := walkGitLog(ctx, gitRepo, commit, treePath, paths...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
commitsMap := map[string]*Commit{}
|
||||
commitsMap[commit.ID.String()] = commit
|
||||
|
||||
commitCommits := map[string]*Commit{}
|
||||
for path, commitID := range revs {
|
||||
if len(commitID) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
c, ok := commitsMap[commitID]
|
||||
if ok {
|
||||
commitCommits[path] = c
|
||||
continue
|
||||
}
|
||||
|
||||
c, err := gitRepo.GetCommit(ctx, commitID) // Ensure the commit exists in the repository
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commitCommits[path] = c
|
||||
}
|
||||
|
||||
return commitCommits, nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/git/gitrepo"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEntries_GetCommitsInfo_ContextErr(t *testing.T) {
|
||||
repoPath, _ := filepath.Abs(filepath.Join(testReposDir, "repo1_bare"))
|
||||
repo, err := OpenRepository(t.Context(), gitrepo.RepositoryManaged("dummy", repoPath))
|
||||
require.NoError(t, err)
|
||||
defer repo.Close()
|
||||
|
||||
commit, err := repo.GetCommit(t.Context(), "feaf4ba6bc635fec442f46ddd4512416ec43c2c2")
|
||||
require.NoError(t, err)
|
||||
entries, err := commit.Tree().ListEntries(t.Context(), repo)
|
||||
require.NoError(t, err)
|
||||
|
||||
countCommitInfosCommit := func(infos []CommitInfo) (nilCommits, nonNilCommits int) {
|
||||
for _, info := range infos {
|
||||
nilCommits += util.Iif(info.Commit == nil, 1, 0)
|
||||
nonNilCommits += util.Iif(info.Commit != nil, 1, 0)
|
||||
}
|
||||
return nilCommits, nonNilCommits
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer test.MockVariableValue(&walkGitLogDebugBeforeNext)()
|
||||
|
||||
walkGitLogDebugBeforeNext = cancel
|
||||
commitInfos, _, err := entries.GetCommitsInfo(ctx, time.Second, "/any/repo-link", repo, commit, "")
|
||||
assert.NoError(t, err)
|
||||
nilCommits, nonNilCommits := countCommitInfosCommit(commitInfos)
|
||||
assert.Equal(t, 0, nonNilCommits) // no commit info due to canceled (or deadline-exceeded) context
|
||||
assert.Equal(t, 3, nilCommits)
|
||||
|
||||
walkGitLogDebugBeforeNext = nil
|
||||
commitInfos, _, err = entries.GetCommitsInfo(t.Context(), time.Second, "/any/repo-link", repo, commit, "")
|
||||
assert.NoError(t, err)
|
||||
nilCommits, nonNilCommits = countCommitInfosCommit(commitInfos)
|
||||
assert.Equal(t, 3, nonNilCommits)
|
||||
assert.Equal(t, 0, nilCommits)
|
||||
}
|
||||
@@ -4,14 +4,11 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/git/gitrepo"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -167,40 +164,3 @@ func TestEntries_GetCommitsInfo(t *testing.T) {
|
||||
assert.Nil(t, cisf.SubmoduleWebLinkTree(t.Context()))
|
||||
})
|
||||
}
|
||||
|
||||
func TestEntries_GetCommitsInfo_ContextErr(t *testing.T) {
|
||||
repoPath, _ := filepath.Abs(filepath.Join(testReposDir, "repo1_bare"))
|
||||
repo, err := OpenRepository(t.Context(), gitrepo.RepositoryManaged("dummy", repoPath))
|
||||
require.NoError(t, err)
|
||||
defer repo.Close()
|
||||
|
||||
commit, err := repo.GetCommit(t.Context(), "feaf4ba6bc635fec442f46ddd4512416ec43c2c2")
|
||||
require.NoError(t, err)
|
||||
entries, err := commit.Tree().ListEntries(t.Context(), repo)
|
||||
require.NoError(t, err)
|
||||
|
||||
countCommitInfosCommit := func(infos []CommitInfo) (nilCommits, nonNilCommits int) {
|
||||
for _, info := range infos {
|
||||
nilCommits += util.Iif(info.Commit == nil, 1, 0)
|
||||
nonNilCommits += util.Iif(info.Commit != nil, 1, 0)
|
||||
}
|
||||
return nilCommits, nonNilCommits
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer test.MockVariableValue(&walkGitLogDebugBeforeNext)()
|
||||
|
||||
walkGitLogDebugBeforeNext = cancel
|
||||
commitInfos, _, err := entries.GetCommitsInfo(ctx, time.Second, "/any/repo-link", repo, commit, "")
|
||||
assert.NoError(t, err)
|
||||
nilCommits, nonNilCommits := countCommitInfosCommit(commitInfos)
|
||||
assert.Equal(t, 0, nonNilCommits) // no commit info due to canceled (or deadline-exceeded) context
|
||||
assert.Equal(t, 3, nilCommits)
|
||||
|
||||
walkGitLogDebugBeforeNext = nil
|
||||
commitInfos, _, err = entries.GetCommitsInfo(t.Context(), time.Second, "/any/repo-link", repo, commit, "")
|
||||
assert.NoError(t, err)
|
||||
nilCommits, nonNilCommits = countCommitInfosCommit(commitInfos)
|
||||
assert.Equal(t, 3, nonNilCommits)
|
||||
assert.Equal(t, 0, nilCommits)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
|
||||
@@ -46,11 +46,11 @@ func (sf *CommitSubmoduleFile) getWebLinkInTargetRepo(ctx context.Context, moreL
|
||||
return &SubmoduleWebLink{RepoWebLink: targetLink, CommitWebLink: targetLink + moreLinkPath}
|
||||
}
|
||||
if !sf.parsed {
|
||||
sf.parsed = true
|
||||
parsedURL, err := giturl.ParseRepositoryURL(ctx, sf.refURL)
|
||||
if err != nil {
|
||||
return nil // do not mark as parsed, otherwise later calls would return a link with an empty target
|
||||
return nil
|
||||
}
|
||||
sf.parsed = true
|
||||
sf.parsedTargetLink = giturl.MakeRepositoryWebLink(parsedURL)
|
||||
}
|
||||
return &SubmoduleWebLink{RepoWebLink: sf.parsedTargetLink, CommitWebLink: sf.parsedTargetLink + moreLinkPath}
|
||||
|
||||
@@ -37,11 +37,4 @@ func TestCommitSubmoduleLink(t *testing.T) {
|
||||
assert.Equal(t, "/subpath/user/repo", wl.RepoWebLink)
|
||||
assert.Equal(t, "/subpath/user/repo/compare/1111...2222", wl.CommitWebLink)
|
||||
})
|
||||
|
||||
t.Run("UnparsableURL", func(t *testing.T) {
|
||||
// both calls share one instance on purpose: the second one used to see the cached parse result
|
||||
sf := NewCommitSubmoduleFile("/any/repo-link", "full-path", "git@github.com:", "aaaa")
|
||||
assert.Nil(t, sf.SubmoduleWebLinkTree(t.Context()))
|
||||
assert.Nil(t, sf.SubmoduleWebLinkCompare(t.Context(), "1111", "2222"))
|
||||
})
|
||||
}
|
||||
|
||||
+11
-5
@@ -8,9 +8,9 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/consts"
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/setting"
|
||||
)
|
||||
@@ -73,9 +73,15 @@ func syncGitConfig(ctx context.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// set support for AGit flow
|
||||
if err := configAddNonExist(ctx, "receive.procReceiveRefs", "refs/for"); err != nil {
|
||||
return err
|
||||
if DefaultFeatures().SupportProcReceive {
|
||||
// set support for AGit flow
|
||||
if err := configAddNonExist(ctx, "receive.procReceiveRefs", "refs/for"); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := configUnsetAll(ctx, "receive.procReceiveRefs", "refs/for"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Due to CVE-2022-24765, git now denies access to git directories which are not owned by current user.
|
||||
@@ -90,7 +96,7 @@ func syncGitConfig(ctx context.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
if consts.IsWindows {
|
||||
if runtime.GOOS == "windows" {
|
||||
if err := configSet(ctx, "core.longpaths", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func ForceFastImportWithInit(ctx context.Context, repoLocalPath string, commits
|
||||
dirEntries, err := os.ReadDir(repoLocalPath)
|
||||
if os.IsNotExist(err) || (err == nil && len(dirEntries) == 0) {
|
||||
_ = os.MkdirAll(repoLocalPath, 0o755)
|
||||
err := InitRepositoryLocal(ctx, repoLocalPath, initOpt.Bare, util.IfZero(initOpt.ObjectFormat, "sha1"), "")
|
||||
err := InitRepositoryLocal(ctx, repoLocalPath, initOpt.Bare, util.IfZero(initOpt.ObjectFormat, "sha1"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user