mirror of
https://github.com/go-gitea/gitea.git
synced 2026-07-15 21:51:12 +00:00
Compare commits
92 Commits
v1.28.0-de
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11363e2f0c | ||
|
|
82edc3da01 | ||
|
|
ed678b9d45 | ||
|
|
ce683b34c6 | ||
|
|
fa50ad2aa9 | ||
|
|
c86eb7081b | ||
|
|
506075c480 | ||
|
|
9fa2bff5fd | ||
|
|
880ddb5724 | ||
|
|
7c629e1ba7 | ||
|
|
b6904c9730 | ||
|
|
9c08df8bc8 | ||
|
|
0e4d93537a | ||
|
|
da5a004fc4 | ||
|
|
ed9b02985a | ||
|
|
f12a0a9183 | ||
|
|
d15cfa363a | ||
|
|
f69e15afe7 | ||
|
|
d2bd1589fe | ||
|
|
b998c3c1aa | ||
|
|
65c5a5ff7b | ||
|
|
aba0eb1749 | ||
|
|
678b5aba30 | ||
|
|
bd5f881c51 | ||
|
|
d3d57dd9b4 | ||
|
|
1bbd127a1a | ||
|
|
f803f8e269 | ||
|
|
8401fe7c54 | ||
|
|
c12e92c21d | ||
|
|
aab3242f7b | ||
|
|
f452c369ac | ||
|
|
c5c991b1a4 | ||
|
|
362539b78e | ||
|
|
66a3723cbb | ||
|
|
27e33b7ba1 | ||
|
|
761470c01d | ||
|
|
c0bbd82cd4 | ||
|
|
7fd34ff033 | ||
|
|
1e682a26eb | ||
|
|
3b3a06e06f | ||
|
|
545ed92354 | ||
|
|
49ef93940a | ||
|
|
308a6f12ae | ||
|
|
97078b96cf | ||
|
|
6507f1fd94 | ||
|
|
0964899799 | ||
|
|
550efdcdfd | ||
|
|
b96bd22372 | ||
|
|
a74f618ade | ||
|
|
2b89e2ac97 | ||
|
|
26bff7f47e | ||
|
|
582217a0da | ||
|
|
a46e331637 | ||
|
|
580cc26d63 | ||
|
|
e3d83bcf9c | ||
|
|
44f927eacf | ||
|
|
35413d5b65 | ||
|
|
e797a27d4e | ||
|
|
4b15260277 | ||
|
|
18fdc77130 | ||
|
|
2c2691b969 | ||
|
|
08d4abbb46 | ||
|
|
e4ef995f2a | ||
|
|
7fdfb8d642 | ||
|
|
d4c4142123 | ||
|
|
f7fd510224 | ||
|
|
38a5824753 | ||
|
|
e8d2c493bb | ||
|
|
b09920a537 | ||
|
|
c6184ed184 | ||
|
|
8909958055 | ||
|
|
638e4bce09 | ||
|
|
a031454586 | ||
|
|
c52a07dcfe | ||
|
|
b6ef881a9f | ||
|
|
6240d8bf89 | ||
|
|
9cb2719fab | ||
|
|
e8654c7e06 | ||
|
|
67a6bd7fc0 | ||
|
|
77e221ffaf | ||
|
|
458c11bd68 | ||
|
|
3d2bbd25ec | ||
|
|
7745720292 | ||
|
|
d46d0540d0 | ||
|
|
e449018730 | ||
|
|
e1cdb71845 | ||
|
|
a64131e22d | ||
|
|
0f0a38c1b9 | ||
|
|
535f791166 | ||
|
|
b34a09be38 | ||
|
|
6f2e328c85 | ||
|
|
55983320ed |
8
.github/actions/docker-dryrun/action.yml
vendored
8
.github/actions/docker-dryrun/action.yml
vendored
@@ -9,10 +9,10 @@ inputs:
|
|||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
- uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||||
- name: Build regular image
|
- name: Build regular image
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: ${{ inputs.platform }}
|
platforms: ${{ inputs.platform }}
|
||||||
@@ -20,7 +20,7 @@ runs:
|
|||||||
file: Dockerfile
|
file: Dockerfile
|
||||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
|
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
|
||||||
- name: Build rootless image
|
- name: Build rootless image
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: ${{ inputs.platform }}
|
platforms: ${{ inputs.platform }}
|
||||||
|
|||||||
12
.github/actions/go-cache/action.yml
vendored
12
.github/actions/go-cache/action.yml
vendored
@@ -16,34 +16,34 @@ runs:
|
|||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- if: ${{ github.workflow == 'cache-seeder' }}
|
- if: ${{ github.workflow == 'cache-seeder' }}
|
||||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
path: ~/go/pkg/mod
|
||||||
key: gomod-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
|
key: gomod-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
|
||||||
- if: ${{ github.workflow != 'cache-seeder' }}
|
- if: ${{ github.workflow != 'cache-seeder' }}
|
||||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
path: ~/go/pkg/mod
|
||||||
key: gomod-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
|
key: gomod-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
|
||||||
restore-keys: gomod-${{ runner.os }}-${{ runner.arch }}
|
restore-keys: gomod-${{ runner.os }}-${{ runner.arch }}
|
||||||
- if: ${{ github.workflow == 'cache-seeder' && inputs.lint-cache != 'true' }}
|
- if: ${{ github.workflow == 'cache-seeder' && inputs.lint-cache != 'true' }}
|
||||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/go-build
|
path: ~/.cache/go-build
|
||||||
key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
|
key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
|
||||||
- if: ${{ github.workflow != 'cache-seeder' || inputs.lint-cache == 'true' }}
|
- if: ${{ github.workflow != 'cache-seeder' || inputs.lint-cache == 'true' }}
|
||||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/go-build
|
path: ~/.cache/go-build
|
||||||
key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
|
key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
|
||||||
restore-keys: gobuild-${{ runner.os }}-${{ runner.arch }}
|
restore-keys: gobuild-${{ runner.os }}-${{ runner.arch }}
|
||||||
- if: ${{ inputs.lint-cache == 'true' && github.workflow == 'cache-seeder' }}
|
- if: ${{ inputs.lint-cache == 'true' && github.workflow == 'cache-seeder' }}
|
||||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/golangci-lint
|
path: ~/.cache/golangci-lint
|
||||||
key: golint-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum', '.golangci.yml') }}
|
key: golint-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum', '.golangci.yml') }}
|
||||||
- if: ${{ inputs.lint-cache == 'true' && github.workflow != 'cache-seeder' }}
|
- if: ${{ inputs.lint-cache == 'true' && github.workflow != 'cache-seeder' }}
|
||||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/golangci-lint
|
path: ~/.cache/golangci-lint
|
||||||
key: golint-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum', '.golangci.yml') }}
|
key: golint-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum', '.golangci.yml') }}
|
||||||
|
|||||||
4
.github/actions/node-setup/action.yml
vendored
4
.github/actions/node-setup/action.yml
vendored
@@ -13,10 +13,10 @@ runs:
|
|||||||
- if: ${{ inputs.cache == 'true' }}
|
- if: ${{ inputs.cache == 'true' }}
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 26
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
cache-dependency-path: pnpm-lock.yaml
|
cache-dependency-path: pnpm-lock.yaml
|
||||||
- if: ${{ inputs.cache != 'true' }}
|
- if: ${{ inputs.cache != 'true' }}
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 26
|
||||||
|
|||||||
4
.github/workflows/cron-renovate.yml
vendored
4
.github/workflows/cron-renovate.yml
vendored
@@ -21,12 +21,12 @@ jobs:
|
|||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
- uses: renovatebot/github-action@6d859fc95779be83a0335ca704879b47e5d79641 # v46.1.16
|
- uses: renovatebot/github-action@b50d2ba2bd928235abdcc14d06dfafc217f1c565 # v46.1.18
|
||||||
with:
|
with:
|
||||||
renovate-version: ${{ env.RENOVATE_VERSION }}
|
renovate-version: ${{ env.RENOVATE_VERSION }}
|
||||||
configurationFile: renovate.json5
|
configurationFile: renovate.json5
|
||||||
token: ${{ secrets.RENOVATE_TOKEN }}
|
token: ${{ secrets.RENOVATE_TOKEN }}
|
||||||
env:
|
env:
|
||||||
RENOVATE_BINARY_SOURCE: install # auto-install go/node toolchains needed by post-upgrade tasks.
|
RENOVATE_BINARY_SOURCE: install # auto-install go/node toolchains needed by post-upgrade tasks.
|
||||||
RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^make (tidy|svg)$"]'
|
RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^make (tidy|svg|generate-codemirror-languages)$"]'
|
||||||
RENOVATE_REPOSITORIES: '["go-gitea/gitea"]'
|
RENOVATE_REPOSITORIES: '["go-gitea/gitea"]'
|
||||||
|
|||||||
2
.github/workflows/files-changed.yml
vendored
2
.github/workflows/files-changed.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
|||||||
shell: ${{ steps.changes.outputs.shell }}
|
shell: ${{ steps.changes.outputs.shell }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||||
id: changes
|
id: changes
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
|
|||||||
2
.github/workflows/pull-compliance.yml
vendored
2
.github/workflows/pull-compliance.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
|||||||
- run: make lint-spell
|
- run: make lint-spell
|
||||||
|
|
||||||
- if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true' || needs.files-changed.outputs.actions == 'true'
|
- if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||||
with:
|
with:
|
||||||
python-version: 3.14
|
python-version: 3.14
|
||||||
- if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true'
|
- if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true'
|
||||||
|
|||||||
2
.github/workflows/pull-db-tests.yml
vendored
2
.github/workflows/pull-db-tests.yml
vendored
@@ -131,7 +131,7 @@ jobs:
|
|||||||
ports:
|
ports:
|
||||||
- "7700:7700"
|
- "7700:7700"
|
||||||
redis:
|
redis:
|
||||||
image: redis:latest@sha256:c904002d182255b6db3cbe3a1e8ce6c187d15390c39500b59fc07181aabff7bf
|
image: redis:latest@sha256:5d2c689b4b55fc3fab4b0cc8aaa950f85b508c76c1e0f35a90d8f411d55a8b2b
|
||||||
options: >- # wait until redis has started
|
options: >- # wait until redis has started
|
||||||
--health-cmd "redis-cli ping"
|
--health-cmd "redis-cli ping"
|
||||||
--health-interval 5s
|
--health-interval 5s
|
||||||
|
|||||||
4
.github/workflows/pull-labeler.yml
vendored
4
.github/workflows/pull-labeler.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
|
- uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0
|
||||||
with:
|
with:
|
||||||
sync-labels: true
|
sync-labels: true
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
ref: ${{ github.event.pull_request.base.sha }}
|
ref: ${{ github.event.pull_request.base.sha }}
|
||||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 26
|
||||||
# Labels are only synced after the title lints, so an invalid title never reaches the label diff.
|
# Labels are only synced after the title lints, so an invalid title never reaches the label diff.
|
||||||
- run: node ./tools/ci-tools.ts lint-pr-title
|
- run: node ./tools/ci-tools.ts lint-pr-title
|
||||||
env:
|
env:
|
||||||
|
|||||||
48
.github/workflows/release-nightly-snapcraft.yml
vendored
48
.github/workflows/release-nightly-snapcraft.yml
vendored
@@ -6,40 +6,30 @@ on:
|
|||||||
- main
|
- main
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-publish:
|
build-and-publish:
|
||||||
runs-on: ubuntu-latest
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
env:
|
matrix:
|
||||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
|
runner: [ubuntu-24.04, ubuntu-24.04-arm]
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
|
with:
|
||||||
- name: Install snapcraft
|
fetch-depth: 0
|
||||||
run: sudo snap install snapcraft --classic
|
- uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1.3.0
|
||||||
|
id: build
|
||||||
- name: Authenticate snapcraft
|
- uses: snapcore/action-publish@214b86e5ca036ead1668c79afb81e550e6c54d40 # v1.2.0
|
||||||
shell: bash
|
env:
|
||||||
run: snapcraft login --with <(printf '%s' "$SNAPCRAFT_STORE_CREDENTIALS")
|
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
|
||||||
|
with:
|
||||||
- name: Remote build
|
snap: ${{ steps.build.outputs.snap }}
|
||||||
run: |
|
release: latest/edge
|
||||||
snapcraft remote-build \
|
|
||||||
--launchpad-accept-public-upload \
|
|
||||||
--build-for=amd64,arm64,armhf
|
|
||||||
|
|
||||||
- name: List built snaps
|
|
||||||
run: find . -maxdepth 1 -type f -name '*.snap' -print
|
|
||||||
|
|
||||||
- name: Upload and release snapcraft nightly build
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
for snap in ./*.snap; do
|
|
||||||
echo "Uploading $snap to edge"
|
|
||||||
snapcraft upload --release="latest/edge" "$snap"
|
|
||||||
done
|
|
||||||
|
|||||||
25
.github/workflows/release-nightly.yml
vendored
25
.github/workflows/release-nightly.yml
vendored
@@ -23,12 +23,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
- uses: ./.github/actions/node-setup
|
||||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
||||||
with:
|
|
||||||
node-version: 24
|
|
||||||
cache: pnpm
|
|
||||||
cache-dependency-path: pnpm-lock.yaml
|
|
||||||
- run: make deps-frontend deps-backend
|
- run: make deps-frontend deps-backend
|
||||||
# xgo build
|
# xgo build
|
||||||
- run: make release
|
- run: make release
|
||||||
@@ -61,7 +56,7 @@ jobs:
|
|||||||
echo "Cleaned name is ${REF_NAME}"
|
echo "Cleaned name is ${REF_NAME}"
|
||||||
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
|
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
|
||||||
- name: configure aws
|
- name: configure aws
|
||||||
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0
|
uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
|
||||||
with:
|
with:
|
||||||
aws-region: ${{ secrets.AWS_REGION }}
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
@@ -83,8 +78,8 @@ jobs:
|
|||||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
# 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
|
# 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
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
- uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||||
- name: Get cleaned branch name
|
- name: Get cleaned branch name
|
||||||
id: clean_name
|
id: clean_name
|
||||||
env:
|
env:
|
||||||
@@ -92,7 +87,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
|
REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
|
||||||
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
|
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
|
||||||
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||||
id: meta
|
id: meta
|
||||||
with:
|
with:
|
||||||
images: |-
|
images: |-
|
||||||
@@ -102,7 +97,7 @@ jobs:
|
|||||||
type=raw,value=${{ steps.clean_name.outputs.branch }}
|
type=raw,value=${{ steps.clean_name.outputs.branch }}
|
||||||
annotations: |
|
annotations: |
|
||||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||||
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||||
id: meta_rootless
|
id: meta_rootless
|
||||||
with:
|
with:
|
||||||
images: |-
|
images: |-
|
||||||
@@ -116,18 +111,18 @@ jobs:
|
|||||||
annotations: |
|
annotations: |
|
||||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Login to GHCR using PAT
|
- name: Login to GHCR using PAT
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: build regular docker image
|
- name: build regular docker image
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||||
@@ -137,7 +132,7 @@ jobs:
|
|||||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
|
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
|
||||||
cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max
|
cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max
|
||||||
- name: build rootless docker image
|
- name: build rootless docker image
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||||
|
|||||||
25
.github/workflows/release-tag-rc.yml
vendored
25
.github/workflows/release-tag-rc.yml
vendored
@@ -24,12 +24,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
- uses: ./.github/actions/node-setup
|
||||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
||||||
with:
|
|
||||||
node-version: 24
|
|
||||||
cache: pnpm
|
|
||||||
cache-dependency-path: pnpm-lock.yaml
|
|
||||||
- run: make deps-frontend deps-backend
|
- run: make deps-frontend deps-backend
|
||||||
# xgo build
|
# xgo build
|
||||||
- run: make release
|
- run: make release
|
||||||
@@ -62,7 +57,7 @@ jobs:
|
|||||||
echo "Cleaned name is ${REF_NAME}"
|
echo "Cleaned name is ${REF_NAME}"
|
||||||
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
- name: configure aws
|
- name: configure aws
|
||||||
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0
|
uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
|
||||||
with:
|
with:
|
||||||
aws-region: ${{ secrets.AWS_REGION }}
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
@@ -94,9 +89,9 @@ jobs:
|
|||||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
# 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
|
# 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
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
- uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||||
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||||
id: meta
|
id: meta
|
||||||
with:
|
with:
|
||||||
images: |-
|
images: |-
|
||||||
@@ -109,7 +104,7 @@ jobs:
|
|||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
annotations: |
|
annotations: |
|
||||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||||
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||||
id: meta_rootless
|
id: meta_rootless
|
||||||
with:
|
with:
|
||||||
images: |-
|
images: |-
|
||||||
@@ -125,18 +120,18 @@ jobs:
|
|||||||
annotations: |
|
annotations: |
|
||||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Login to GHCR using PAT
|
- name: Login to GHCR using PAT
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: build regular container image
|
- name: build regular container image
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||||
@@ -144,7 +139,7 @@ jobs:
|
|||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
annotations: ${{ steps.meta.outputs.annotations }}
|
annotations: ${{ steps.meta.outputs.annotations }}
|
||||||
- name: build rootless container image
|
- name: build rootless container image
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||||
|
|||||||
25
.github/workflows/release-tag-version.yml
vendored
25
.github/workflows/release-tag-version.yml
vendored
@@ -27,12 +27,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
- uses: ./.github/actions/node-setup
|
||||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
||||||
with:
|
|
||||||
node-version: 24
|
|
||||||
cache: pnpm
|
|
||||||
cache-dependency-path: pnpm-lock.yaml
|
|
||||||
- run: make deps-frontend deps-backend
|
- run: make deps-frontend deps-backend
|
||||||
# xgo build
|
# xgo build
|
||||||
- run: make release
|
- run: make release
|
||||||
@@ -65,7 +60,7 @@ jobs:
|
|||||||
echo "Cleaned name is ${REF_NAME}"
|
echo "Cleaned name is ${REF_NAME}"
|
||||||
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
- name: configure aws
|
- name: configure aws
|
||||||
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0
|
uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
|
||||||
with:
|
with:
|
||||||
aws-region: ${{ secrets.AWS_REGION }}
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
@@ -97,9 +92,9 @@ jobs:
|
|||||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
# 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
|
# 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
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
- uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||||
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||||
id: meta
|
id: meta
|
||||||
with:
|
with:
|
||||||
images: |-
|
images: |-
|
||||||
@@ -116,7 +111,7 @@ jobs:
|
|||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
annotations: |
|
annotations: |
|
||||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||||
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||||
id: meta_rootless
|
id: meta_rootless
|
||||||
with:
|
with:
|
||||||
images: |-
|
images: |-
|
||||||
@@ -137,18 +132,18 @@ jobs:
|
|||||||
annotations: |
|
annotations: |
|
||||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Login to GHCR using PAT
|
- name: Login to GHCR using PAT
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: build regular container image
|
- name: build regular container image
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||||
@@ -156,7 +151,7 @@ jobs:
|
|||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
annotations: ${{ steps.meta.outputs.annotations }}
|
annotations: ${{ steps.meta.outputs.annotations }}
|
||||||
- name: build rootless container image
|
- name: build rootless container image
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ Jimmy Praet <jimmy.praet@telenet.be> (@jpraet)
|
|||||||
Leon Hofmeister <dev.lh@web.de> (@delvh)
|
Leon Hofmeister <dev.lh@web.de> (@delvh)
|
||||||
Wim <wim@42.be> (@42wim)
|
Wim <wim@42.be> (@42wim)
|
||||||
Jason Song <i@wolfogre.com> (@wolfogre)
|
Jason Song <i@wolfogre.com> (@wolfogre)
|
||||||
Yarden Shoham <git@yardenshoham.com> (@yardenshoham)
|
|
||||||
Yu Tian <zettat123@gmail.com> (@Zettat123)
|
Yu Tian <zettat123@gmail.com> (@Zettat123)
|
||||||
Dong Ge <gedong_1994@163.com> (@sillyguodong)
|
Dong Ge <gedong_1994@163.com> (@sillyguodong)
|
||||||
Xinyi Gong <hestergong@gmail.com> (@HesterG)
|
Xinyi Gong <hestergong@gmail.com> (@HesterG)
|
||||||
|
|||||||
18
Makefile
18
Makefile
@@ -12,13 +12,13 @@ COMMA := ,
|
|||||||
XGO_VERSION := go-1.26.x
|
XGO_VERSION := go-1.26.x
|
||||||
|
|
||||||
AIR_PACKAGE ?= github.com/air-verse/air@v1.65.3 # renovate: datasource=go
|
AIR_PACKAGE ?= github.com/air-verse/air@v1.65.3 # renovate: datasource=go
|
||||||
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.7.0 # renovate: datasource=go
|
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.8.0 # renovate: datasource=go
|
||||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 # renovate: datasource=go
|
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 # renovate: datasource=go
|
||||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 # renovate: datasource=go
|
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 # renovate: datasource=go
|
||||||
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 # 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.34.1 # renovate: datasource=go
|
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.35.0 # renovate: datasource=go
|
||||||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@v1.9.0 # renovate: datasource=go
|
XGO_PACKAGE ?= src.techknowlogick.com/xgo@v1.9.0 # renovate: datasource=go
|
||||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.4.0 # renovate: datasource=go
|
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.5.0 # renovate: datasource=go
|
||||||
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 # renovate: datasource=go
|
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 # renovate: datasource=go
|
||||||
SHELLCHECK_IMAGE ?= docker.io/koalaman/shellcheck:v0.11.0@sha256:61862eba1fcf09a484ebcc6feea46f1782532571a34ed51fedf90dd25f925a8d # renovate: datasource=docker
|
SHELLCHECK_IMAGE ?= docker.io/koalaman/shellcheck:v0.11.0@sha256:61862eba1fcf09a484ebcc6feea46f1782532571a34ed51fedf90dd25f925a8d # renovate: datasource=docker
|
||||||
|
|
||||||
@@ -231,7 +231,9 @@ endif
|
|||||||
generate-swagger: $(SWAGGER_SPEC) $(OPENAPI3_SPEC) ## generate the swagger spec from code comments
|
generate-swagger: $(SWAGGER_SPEC) $(OPENAPI3_SPEC) ## generate the swagger spec from code comments
|
||||||
|
|
||||||
$(SWAGGER_SPEC): $(GO_SOURCES) $(SWAGGER_SPEC_INPUT)
|
$(SWAGGER_SPEC): $(GO_SOURCES) $(SWAGGER_SPEC_INPUT)
|
||||||
$(GO) run $(SWAGGER_PACKAGE) generate spec --exclude "$(SWAGGER_EXCLUDE)" --input "$(SWAGGER_SPEC_INPUT)" --output './$(SWAGGER_SPEC)'
|
@output="$$($(GO) run $(SWAGGER_PACKAGE) generate spec --enable-allof-compounding --skip-enum-desc --exclude "$(SWAGGER_EXCLUDE)" --input "$(SWAGGER_SPEC_INPUT)" --output './$(SWAGGER_SPEC)' 2>&1)" || { printf '%s\n' "$$output" >&2; exit 1; }; \
|
||||||
|
warnings="$$(printf '%s\n' "$$output" | grep -v '^go: ')"; \
|
||||||
|
if [ -n "$$warnings" ]; then printf '%s\n' "$$warnings" >&2; exit 1; fi
|
||||||
|
|
||||||
.PHONY: swagger-check
|
.PHONY: swagger-check
|
||||||
swagger-check: generate-swagger
|
swagger-check: generate-swagger
|
||||||
@@ -246,9 +248,11 @@ swagger-check: generate-swagger
|
|||||||
swagger-validate: ## check if the swagger spec is valid
|
swagger-validate: ## check if the swagger spec is valid
|
||||||
@# swagger "validate" requires that the "basePath" must start with a slash, but we are using Golang template "{{...}}"
|
@# swagger "validate" requires that the "basePath" must start with a slash, but we are using Golang template "{{...}}"
|
||||||
@$(SED_INPLACE) -E -e 's|"basePath":( *)"(.*)"|"basePath":\1"/\2"|g' './$(SWAGGER_SPEC)' # add a prefix slash to basePath
|
@$(SED_INPLACE) -E -e 's|"basePath":( *)"(.*)"|"basePath":\1"/\2"|g' './$(SWAGGER_SPEC)' # add a prefix slash to basePath
|
||||||
@# FIXME: there are some warnings
|
@output="$$($(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)' 2>&1)"; status=$$?; \
|
||||||
$(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)'
|
$(SED_INPLACE) -E -e 's|"basePath":( *)"/(.*)"|"basePath":\1"\2"|g' './$(SWAGGER_SPEC)'; \
|
||||||
@$(SED_INPLACE) -E -e 's|"basePath":( *)"/(.*)"|"basePath":\1"\2"|g' './$(SWAGGER_SPEC)' # remove the prefix slash from basePath
|
printf '%s\n' "$$output" | grep -v '^go: '; \
|
||||||
|
[ $$status -eq 0 ] || exit $$status; \
|
||||||
|
case "$$output" in *WARNING:*) exit 1;; esac
|
||||||
|
|
||||||
.PHONY: generate-openapi3
|
.PHONY: generate-openapi3
|
||||||
generate-openapi3: $(OPENAPI3_SPEC) ## generate the OpenAPI 3.0 spec from the Swagger 2.0 spec
|
generate-openapi3: $(OPENAPI3_SPEC) ## generate the OpenAPI 3.0 spec from the Swagger 2.0 spec
|
||||||
|
|||||||
8
assets/codemirror-languages.json
generated
8
assets/codemirror-languages.json
generated
@@ -436,6 +436,7 @@
|
|||||||
"jsonl",
|
"jsonl",
|
||||||
"mcmeta",
|
"mcmeta",
|
||||||
"sarif",
|
"sarif",
|
||||||
|
"slnlaunch",
|
||||||
"tact",
|
"tact",
|
||||||
"tfstate",
|
"tfstate",
|
||||||
"topojson",
|
"topojson",
|
||||||
@@ -691,10 +692,17 @@
|
|||||||
"extensions": [
|
"extensions": [
|
||||||
"ini",
|
"ini",
|
||||||
"cnf",
|
"cnf",
|
||||||
|
"container",
|
||||||
"dof",
|
"dof",
|
||||||
"lektorproject",
|
"lektorproject",
|
||||||
|
"mount",
|
||||||
|
"network",
|
||||||
"prefs",
|
"prefs",
|
||||||
"properties",
|
"properties",
|
||||||
|
"service",
|
||||||
|
"socket",
|
||||||
|
"target",
|
||||||
|
"timer",
|
||||||
"url",
|
"url",
|
||||||
"conf"
|
"conf"
|
||||||
],
|
],
|
||||||
|
|||||||
14
assets/go-licenses.json
generated
14
assets/go-licenses.json
generated
File diff suppressed because one or more lines are too long
@@ -18,6 +18,7 @@ func newUserCommand() *cli.Command {
|
|||||||
microcmdUserDelete(),
|
microcmdUserDelete(),
|
||||||
newUserGenerateAccessTokenCommand(),
|
newUserGenerateAccessTokenCommand(),
|
||||||
microcmdUserMustChangePassword(),
|
microcmdUserMustChangePassword(),
|
||||||
|
microcmdUserDisableTwoFactor(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
72
cmd/admin_user_disable_2fa.go
Normal file
72
cmd/admin_user_disable_2fa.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
auth_model "gitea.dev/models/auth"
|
||||||
|
user_model "gitea.dev/models/user"
|
||||||
|
"gitea.dev/modules/setting"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func microcmdUserDisableTwoFactor() *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "disable-2fa",
|
||||||
|
Usage: "Disable two-factor authentication for a user",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "username",
|
||||||
|
Aliases: []string{"u"},
|
||||||
|
Usage: "Username of the user to disable 2FA for",
|
||||||
|
},
|
||||||
|
&cli.Int64Flag{
|
||||||
|
Name: "id",
|
||||||
|
Usage: "ID of the user to disable 2FA for",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: runDisableTwoFactor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDisableTwoFactor(ctx context.Context, c *cli.Command) error {
|
||||||
|
if !c.IsSet("id") && !c.IsSet("username") {
|
||||||
|
return errors.New("either --id or --username must be provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !setting.IsInTesting {
|
||||||
|
if err := initDB(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var user *user_model.User
|
||||||
|
var err error
|
||||||
|
if c.IsSet("id") {
|
||||||
|
user, err = user_model.GetUserByID(ctx, c.Int64("id"))
|
||||||
|
} else {
|
||||||
|
user, err = user_model.GetUserByName(ctx, c.String("username"))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// When both selectors are given, make sure they refer to the same user.
|
||||||
|
if c.IsSet("id") && c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) {
|
||||||
|
return fmt.Errorf("the user with id %d is %q, which does not match the provided username %q", user.ID, user.Name, c.String("username"))
|
||||||
|
}
|
||||||
|
|
||||||
|
totp, webAuthn, err := auth_model.DisableTwoFactor(ctx, user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Disabled 2FA for user %q (removed %d TOTP and %d WebAuthn credential(s))\n", user.Name, totp, webAuthn)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
119
cmd/admin_user_disable_2fa_test.go
Normal file
119
cmd/admin_user_disable_2fa_test.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
auth_model "gitea.dev/models/auth"
|
||||||
|
"gitea.dev/models/db"
|
||||||
|
"gitea.dev/models/unittest"
|
||||||
|
user_model "gitea.dev/models/user"
|
||||||
|
|
||||||
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDisableTwoFactorCommand(t *testing.T) {
|
||||||
|
ctx := t.Context()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}, &auth_model.TwoFactor{}, &auth_model.WebAuthnCredential{}))
|
||||||
|
}()
|
||||||
|
|
||||||
|
t.Run("disable TOTP and WebAuthn", func(t *testing.T) {
|
||||||
|
require.NoError(t, microcmdUserCreate().Run(ctx, []string{"create", "--username", "tfuser", "--email", "tfuser@gitea.local", "--random-password"}))
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "tfuser"})
|
||||||
|
|
||||||
|
// Enroll TOTP.
|
||||||
|
tf := &auth_model.TwoFactor{UID: user.ID}
|
||||||
|
require.NoError(t, tf.SetSecret("test-secret"))
|
||||||
|
_, err := tf.GenerateScratchToken()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, auth_model.NewTwoFactor(ctx, tf))
|
||||||
|
|
||||||
|
// Register a WebAuthn credential.
|
||||||
|
_, err = auth_model.CreateCredential(ctx, user.ID, "test-key", &webauthn.Credential{ID: []byte("test-cred-id")})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
has, err := auth_model.HasTwoFactorOrWebAuthn(ctx, user.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, has)
|
||||||
|
|
||||||
|
require.NoError(t, microcmdUserDisableTwoFactor().Run(ctx, []string{"disable-2fa", "--username", "tfuser"}))
|
||||||
|
|
||||||
|
// Both factors must be gone afterwards.
|
||||||
|
has, err = auth_model.HasTwoFactorOrWebAuthn(ctx, user.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, has)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("disable by id", func(t *testing.T) {
|
||||||
|
require.NoError(t, microcmdUserCreate().Run(ctx, []string{"create", "--username", "iduser", "--email", "iduser@gitea.local", "--random-password"}))
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "iduser"})
|
||||||
|
|
||||||
|
tf := &auth_model.TwoFactor{UID: user.ID}
|
||||||
|
require.NoError(t, tf.SetSecret("test-secret"))
|
||||||
|
require.NoError(t, auth_model.NewTwoFactor(ctx, tf))
|
||||||
|
|
||||||
|
require.NoError(t, microcmdUserDisableTwoFactor().Run(ctx, []string{"disable-2fa", "--id", strconv.FormatInt(user.ID, 10)}))
|
||||||
|
|
||||||
|
has, err := auth_model.HasTwoFactorOrWebAuthn(ctx, user.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, has)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no enrollment is a no-op", func(t *testing.T) {
|
||||||
|
require.NoError(t, microcmdUserCreate().Run(ctx, []string{"create", "--username", "plainuser", "--email", "plainuser@gitea.local", "--random-password"}))
|
||||||
|
require.NoError(t, microcmdUserDisableTwoFactor().Run(ctx, []string{"disable-2fa", "--username", "plainuser"}))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("id and username must match when both given", func(t *testing.T) {
|
||||||
|
require.NoError(t, microcmdUserCreate().Run(ctx, []string{"create", "--username", "matchuser", "--email", "matchuser@gitea.local", "--random-password"}))
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "matchuser"})
|
||||||
|
id := strconv.FormatInt(user.ID, 10)
|
||||||
|
|
||||||
|
// Matching id + username is accepted.
|
||||||
|
require.NoError(t, microcmdUserDisableTwoFactor().Run(ctx, []string{"disable-2fa", "--id", id, "--username", "matchuser"}))
|
||||||
|
|
||||||
|
// Mismatched id + username is rejected.
|
||||||
|
cmd := microcmdUserDisableTwoFactor()
|
||||||
|
cmd.Writer, cmd.ErrWriter = io.Discard, io.Discard
|
||||||
|
err := cmd.Run(ctx, []string{"disable-2fa", "--id", id, "--username", "someotheruser"})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "does not match the provided username")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("failure cases", func(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
expectedErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "user does not exist",
|
||||||
|
args: []string{"disable-2fa", "--username", "nonexistentuser"},
|
||||||
|
expectedErr: "user does not exist",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "neither id nor username",
|
||||||
|
args: []string{"disable-2fa"},
|
||||||
|
expectedErr: "either --id or --username must be provided",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
cmd := microcmdUserDisableTwoFactor()
|
||||||
|
cmd.Writer, cmd.ErrWriter = io.Discard, io.Discard
|
||||||
|
err := cmd.Run(ctx, tc.args)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), tc.expectedErr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -536,6 +536,13 @@ INTERNAL_TOKEN =
|
|||||||
;; Leave it empty to apply the default policy, or set it to "unset" to disable Content-Security-Policy.
|
;; Leave it empty to apply the default policy, or set it to "unset" to disable Content-Security-Policy.
|
||||||
;CONTENT_SECURITY_POLICY_GENERAL =
|
;CONTENT_SECURITY_POLICY_GENERAL =
|
||||||
|
|
||||||
|
;; Webhook and oauth2 clients can only call allowed hosts for security reasons. Comma separated list, eg: external, 192.168.1.0/24, *.mydomain.com
|
||||||
|
;; Built-in: loopback (for localhost), private (for LAN/intranet), external (for public hosts on internet), * (for all hosts)
|
||||||
|
;; CIDR list: 1.2.3.0/8, 2001:db8::/32
|
||||||
|
;; Wildcard hosts: *.mydomain.com, 192.168.100.*
|
||||||
|
;; 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 = external
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
[camo]
|
[camo]
|
||||||
@@ -1191,6 +1198,7 @@ LEVEL = Info
|
|||||||
;; Default source for the pull request title when opening a new PR.
|
;; Default source for the pull request title when opening a new PR.
|
||||||
;; "first-commit" uses the oldest commit's summary.
|
;; "first-commit" uses the oldest commit's summary.
|
||||||
;; "auto" uses commit's summary if the PR only has one commit, normalizes the branch name if multiple commits.
|
;; "auto" uses commit's summary if the PR only has one commit, normalizes the branch name if multiple commits.
|
||||||
|
;; "branch-name" always uses the PR's branch name.
|
||||||
;DEFAULT_TITLE_SOURCE = auto
|
;DEFAULT_TITLE_SOURCE = auto
|
||||||
;;
|
;;
|
||||||
;; Delay mergeable check until page view or API access, for pull requests that have not been updated in the specified days when their base branches get updated.
|
;; Delay mergeable check until page view or API access, for pull requests that have not been updated in the specified days when their base branches get updated.
|
||||||
@@ -1754,13 +1762,6 @@ LEVEL = Info
|
|||||||
;; Deliver timeout in seconds
|
;; Deliver timeout in seconds
|
||||||
;DELIVER_TIMEOUT = 5
|
;DELIVER_TIMEOUT = 5
|
||||||
;;
|
;;
|
||||||
;; Webhook can only call allowed hosts for security reasons. Comma separated list, eg: external, 192.168.1.0/24, *.mydomain.com
|
|
||||||
;; Built-in: loopback (for localhost), private (for LAN/intranet), external (for public hosts on internet), * (for all hosts)
|
|
||||||
;; CIDR list: 1.2.3.0/8, 2001:db8::/32
|
|
||||||
;; Wildcard hosts: *.mydomain.com, 192.168.100.*
|
|
||||||
;; Since 1.15.7. Default to * for 1.15.x, external for 1.16 and later
|
|
||||||
;ALLOWED_HOST_LIST = external
|
|
||||||
;;
|
|
||||||
;; Allow insecure certification
|
;; Allow insecure certification
|
||||||
;SKIP_TLS_VERIFY = false
|
;SKIP_TLS_VERIFY = false
|
||||||
;;
|
;;
|
||||||
@@ -3008,6 +3009,10 @@ LEVEL = Info
|
|||||||
;SCOPED_WORKFLOW_DIRS = .gitea/scoped_workflows
|
;SCOPED_WORKFLOW_DIRS = .gitea/scoped_workflows
|
||||||
;; Maximum number of attempts a single workflow run can have. Default value is 50.
|
;; Maximum number of attempts a single workflow run can have. Default value is 50.
|
||||||
;MAX_RERUN_ATTEMPTS = 50
|
;MAX_RERUN_ATTEMPTS = 50
|
||||||
|
;; Maximum number of runners that may run the task-assignment query concurrently, per Gitea instance.
|
||||||
|
;; Caps this instance's DB load when many runners poll at once; excess runners retry on their next poll.
|
||||||
|
;; In a multi-instance deployment the cluster-wide limit is this value times the number of instances. Default value is 16.
|
||||||
|
;MAX_CONCURRENT_TASK_PICKS = 16
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|||||||
@@ -95,8 +95,15 @@ However, if there are no objections from maintainers, the PR can be merged with
|
|||||||
### Commit messages
|
### Commit messages
|
||||||
|
|
||||||
Mergers are required to rewrite the PR title and the first comment (the summary) when necessary so the squash commit message is clear.
|
Mergers are required to rewrite the PR title and the first comment (the summary) when necessary so the squash commit message is clear.
|
||||||
|
Usually the Pull Request description and commit message body should not be empty, unless the title is already clear enough or the description would be a copy of the comments in code.
|
||||||
|
|
||||||
The final commit message should not hedge: replace phrases like `hopefully, <x> won't happen anymore` with definite wording.
|
The final commit message:
|
||||||
|
|
||||||
|
- should match the code changes.
|
||||||
|
- should only keep true co-authors, false-positive co-authors should be removed.
|
||||||
|
- should not hedge: replace phrases like `hopefully, <x> won't happen anymore` with definite wording.
|
||||||
|
- should not contain hidden information like `<!-- -->` or extra information after the description's divider `----`.
|
||||||
|
- should not contain unrelated contents (e.g.: Release Notes, Configuration, etc.) from a Renovate update PR.
|
||||||
|
|
||||||
#### PR Co-authors
|
#### PR Co-authors
|
||||||
|
|
||||||
@@ -158,9 +165,16 @@ Any account with write access (including bots and TOC members) **must** use [2FA
|
|||||||
Mergers are the maintainers who carry out the final merge of approved PRs. Their responsibilities, described throughout this guide, are:
|
Mergers are the maintainers who carry out the final merge of approved PRs. Their responsibilities, described throughout this guide, are:
|
||||||
|
|
||||||
- Merging PRs from the [merge queue](#getting-prs-merged) in order, once a PR has `lgtm/done`, no open discussions, and no merge conflicts.
|
- Merging PRs from the [merge queue](#getting-prs-merged) in order, once a PR has `lgtm/done`, no open discussions, and no merge conflicts.
|
||||||
- Rewriting the PR title and summary so the squash [commit message](#commit-messages) is clear, removing false-positive co-authors while keeping every true co-author.
|
- Rewriting the PR title and description prior to the merge, making the [commit message](#commit-messages) clear.\
|
||||||
|
In particular, mergers should edit the PR description.\
|
||||||
|
Mergers should **not** edit the actual commit message except to remove unnecessary information. Because of that, even if users are looking at the PR, they can understand what changed.
|
||||||
- Assigning the correct labels (including `type/…`) needed for changelog and backport decisions.
|
- Assigning the correct labels (including `type/…`) needed for changelog and backport decisions.
|
||||||
- Agreeing, together with the owners, on when a release is ready (see [release management](release-management.md)).
|
- Agreeing, together with the owners, on when a release is ready (see [release management](release-management.md)).
|
||||||
|
- Merging a PR also means the PR looks good to the merger and is approved by the merger.
|
||||||
|
|
||||||
|
If a merger violates these merge guides more than 3 times in the past 365 days
|
||||||
|
(e.g.: merge with unresolved reviews without TOC decision to ignore the review, merge with garbage commit messages),
|
||||||
|
they may lose their merging privileges for at least three months.
|
||||||
|
|
||||||
#### Becoming a merger
|
#### Becoming a merger
|
||||||
|
|
||||||
@@ -206,7 +220,7 @@ The result of this script needs then to be published in the TOC election issue t
|
|||||||
|
|
||||||
- 2026-06-14 ~ 2026-12-31
|
- 2026-06-14 ~ 2026-12-31
|
||||||
- Company
|
- Company
|
||||||
- [Jason Song](https://gitea.com/wolfogre) <i@wolfogre.com>
|
- [Yu Tian](https://gitea.com/Zettat123) <zettat123@gmail.com>
|
||||||
- [Lunny Xiao](https://gitea.com/lunny) <xiaolunwen@gmail.com>
|
- [Lunny Xiao](https://gitea.com/lunny) <xiaolunwen@gmail.com>
|
||||||
- [Matti Ranta](https://gitea.com/techknowlogick) <techknowlogick@gitea.com>
|
- [Matti Ranta](https://gitea.com/techknowlogick) <techknowlogick@gitea.com>
|
||||||
- Community
|
- Community
|
||||||
@@ -227,7 +241,7 @@ Here's the history of the owners and the time they served:
|
|||||||
- [Andrew Thornton](https://gitea.com/zeripath) - [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
|
- [Andrew Thornton](https://gitea.com/zeripath) - [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
|
||||||
- [6543](https://gitea.com/6543) - 2023, 2025
|
- [6543](https://gitea.com/6543) - 2023, 2025
|
||||||
- [John Olheiser](https://gitea.com/jolheiser) - 2023, 2024
|
- [John Olheiser](https://gitea.com/jolheiser) - 2023, 2024
|
||||||
- [Jason Song](https://gitea.com/wolfogre) - 2023
|
- [Jason Song](https://gitea.com/wolfogre) - 2023, 2025
|
||||||
|
|
||||||
## Governance Compensation
|
## Governance Compensation
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import arrayFunc from 'eslint-plugin-array-func';
|
|
||||||
import comments from '@eslint-community/eslint-plugin-eslint-comments';
|
import comments from '@eslint-community/eslint-plugin-eslint-comments';
|
||||||
import deMorgan from 'eslint-plugin-de-morgan';
|
|
||||||
import globals from 'globals';
|
import globals from 'globals';
|
||||||
import importPlugin from 'eslint-plugin-import-x';
|
import importPlugin from 'eslint-plugin-import-x';
|
||||||
import playwright from 'eslint-plugin-playwright';
|
import playwright from 'eslint-plugin-playwright';
|
||||||
@@ -15,7 +13,6 @@ import vue from 'eslint-plugin-vue';
|
|||||||
import vueScopedCss from 'eslint-plugin-vue-scoped-css';
|
import vueScopedCss from 'eslint-plugin-vue-scoped-css';
|
||||||
import wc from 'eslint-plugin-wc';
|
import wc from 'eslint-plugin-wc';
|
||||||
import {defineConfig, globalIgnores} from 'eslint/config';
|
import {defineConfig, globalIgnores} from 'eslint/config';
|
||||||
import type {ESLint} from 'eslint';
|
|
||||||
|
|
||||||
import unescapedHtmlLiteral from './tools/eslint-rules/unescaped-html-literal.ts';
|
import unescapedHtmlLiteral from './tools/eslint-rules/unescaped-html-literal.ts';
|
||||||
|
|
||||||
@@ -64,10 +61,8 @@ export default defineConfig([
|
|||||||
'@eslint-community/eslint-comments': comments,
|
'@eslint-community/eslint-comments': comments,
|
||||||
'@stylistic': stylistic,
|
'@stylistic': stylistic,
|
||||||
'@typescript-eslint': typescriptPlugin.plugin,
|
'@typescript-eslint': typescriptPlugin.plugin,
|
||||||
'array-func': arrayFunc,
|
|
||||||
'de-morgan': deMorgan,
|
|
||||||
'gitea': {rules: {'unescaped-html-literal': unescapedHtmlLiteral}},
|
'gitea': {rules: {'unescaped-html-literal': unescapedHtmlLiteral}},
|
||||||
'import-x': importPlugin as unknown as ESLint.Plugin, // https://github.com/un-ts/eslint-plugin-import-x/issues/203
|
'import-x': importPlugin,
|
||||||
regexp,
|
regexp,
|
||||||
sonarjs,
|
sonarjs,
|
||||||
unicorn,
|
unicorn,
|
||||||
@@ -278,12 +273,6 @@ export default defineConfig([
|
|||||||
'@typescript-eslint/unified-signatures': [2],
|
'@typescript-eslint/unified-signatures': [2],
|
||||||
'accessor-pairs': [2],
|
'accessor-pairs': [2],
|
||||||
'array-callback-return': [2, {checkForEach: true}],
|
'array-callback-return': [2, {checkForEach: true}],
|
||||||
'array-func/avoid-reverse': [2],
|
|
||||||
'array-func/from-map': [2],
|
|
||||||
'array-func/no-unnecessary-this-arg': [2],
|
|
||||||
'array-func/prefer-array-from': [2],
|
|
||||||
'array-func/prefer-flat-map': [0], // handled by unicorn/prefer-array-flat-map
|
|
||||||
'array-func/prefer-flat': [0], // handled by unicorn/prefer-array-flat
|
|
||||||
'arrow-body-style': [0],
|
'arrow-body-style': [0],
|
||||||
'block-scoped-var': [2],
|
'block-scoped-var': [2],
|
||||||
'camelcase': [0],
|
'camelcase': [0],
|
||||||
@@ -294,8 +283,6 @@ export default defineConfig([
|
|||||||
'consistent-this': [0],
|
'consistent-this': [0],
|
||||||
'constructor-super': [2],
|
'constructor-super': [2],
|
||||||
'curly': [0],
|
'curly': [0],
|
||||||
'de-morgan/no-negated-conjunction': [2],
|
|
||||||
'de-morgan/no-negated-disjunction': [2],
|
|
||||||
'default-case-last': [2],
|
'default-case-last': [2],
|
||||||
'default-case': [0],
|
'default-case': [0],
|
||||||
'default-param-last': [0],
|
'default-param-last': [0],
|
||||||
@@ -745,6 +732,7 @@ export default defineConfig([
|
|||||||
'unicorn/consistent-json-file-read': [2],
|
'unicorn/consistent-json-file-read': [2],
|
||||||
'unicorn/consistent-optional-chaining': [2],
|
'unicorn/consistent-optional-chaining': [2],
|
||||||
'unicorn/consistent-template-literal-escape': [2],
|
'unicorn/consistent-template-literal-escape': [2],
|
||||||
|
'unicorn/consistent-tuple-labels': [2],
|
||||||
'unicorn/custom-error-definition': [0],
|
'unicorn/custom-error-definition': [0],
|
||||||
'unicorn/default-export-style': [2],
|
'unicorn/default-export-style': [2],
|
||||||
'unicorn/dom-node-dataset': [2, {preferAttributes: true}],
|
'unicorn/dom-node-dataset': [2, {preferAttributes: true}],
|
||||||
@@ -778,6 +766,7 @@ export default defineConfig([
|
|||||||
'unicorn/no-array-sort-for-min-max': [2],
|
'unicorn/no-array-sort-for-min-max': [2],
|
||||||
'unicorn/no-array-splice': [0],
|
'unicorn/no-array-splice': [0],
|
||||||
'unicorn/no-asterisk-prefix-in-documentation-comments': [0],
|
'unicorn/no-asterisk-prefix-in-documentation-comments': [0],
|
||||||
|
'unicorn/no-async-promise-finally': [2],
|
||||||
'unicorn/no-await-expression-member': [0],
|
'unicorn/no-await-expression-member': [0],
|
||||||
'unicorn/no-await-in-promise-methods': [2],
|
'unicorn/no-await-in-promise-methods': [2],
|
||||||
'unicorn/no-blob-to-file': [2],
|
'unicorn/no-blob-to-file': [2],
|
||||||
@@ -814,8 +803,10 @@ export default defineConfig([
|
|||||||
'unicorn/no-invalid-fetch-options': [2],
|
'unicorn/no-invalid-fetch-options': [2],
|
||||||
'unicorn/no-invalid-file-input-accept': [2],
|
'unicorn/no-invalid-file-input-accept': [2],
|
||||||
'unicorn/no-invalid-remove-event-listener': [2],
|
'unicorn/no-invalid-remove-event-listener': [2],
|
||||||
|
'unicorn/no-invalid-well-known-symbol-methods': [2],
|
||||||
'unicorn/no-keyword-prefix': [0],
|
'unicorn/no-keyword-prefix': [0],
|
||||||
'unicorn/no-late-current-target-access': [2],
|
'unicorn/no-late-current-target-access': [2],
|
||||||
|
'unicorn/no-late-event-control': [2],
|
||||||
'unicorn/no-lonely-if': [2],
|
'unicorn/no-lonely-if': [2],
|
||||||
'unicorn/no-loop-iterable-mutation': [2],
|
'unicorn/no-loop-iterable-mutation': [2],
|
||||||
'unicorn/no-magic-array-flat-depth': [0],
|
'unicorn/no-magic-array-flat-depth': [0],
|
||||||
@@ -852,9 +843,11 @@ export default defineConfig([
|
|||||||
'unicorn/no-uncalled-method': [2],
|
'unicorn/no-uncalled-method': [2],
|
||||||
'unicorn/no-undeclared-class-members': [2],
|
'unicorn/no-undeclared-class-members': [2],
|
||||||
'unicorn/no-unnecessary-array-flat-depth': [2],
|
'unicorn/no-unnecessary-array-flat-depth': [2],
|
||||||
|
'unicorn/no-unnecessary-array-flat-map': [2],
|
||||||
'unicorn/no-unnecessary-array-splice-count': [2],
|
'unicorn/no-unnecessary-array-splice-count': [2],
|
||||||
'unicorn/no-unnecessary-await': [2],
|
'unicorn/no-unnecessary-await': [2],
|
||||||
'unicorn/no-unnecessary-boolean-comparison': [2],
|
'unicorn/no-unnecessary-boolean-comparison': [2],
|
||||||
|
'unicorn/no-unnecessary-fetch-options': [0],
|
||||||
'unicorn/no-unnecessary-global-this': [0],
|
'unicorn/no-unnecessary-global-this': [0],
|
||||||
'unicorn/no-unnecessary-nested-ternary': [2],
|
'unicorn/no-unnecessary-nested-ternary': [2],
|
||||||
'unicorn/no-unnecessary-polyfills': [2],
|
'unicorn/no-unnecessary-polyfills': [2],
|
||||||
@@ -867,6 +860,7 @@ export default defineConfig([
|
|||||||
'unicorn/no-unreadable-object-destructuring': [0],
|
'unicorn/no-unreadable-object-destructuring': [0],
|
||||||
'unicorn/no-unsafe-buffer-conversion': [2],
|
'unicorn/no-unsafe-buffer-conversion': [2],
|
||||||
'unicorn/no-unsafe-dom-html': [0],
|
'unicorn/no-unsafe-dom-html': [0],
|
||||||
|
'unicorn/no-unsafe-promise-all-settled-values': [2],
|
||||||
'unicorn/no-unsafe-property-key': [0],
|
'unicorn/no-unsafe-property-key': [0],
|
||||||
'unicorn/no-unsafe-string-replacement': [0],
|
'unicorn/no-unsafe-string-replacement': [0],
|
||||||
'unicorn/no-unused-array-method-return': [2],
|
'unicorn/no-unused-array-method-return': [2],
|
||||||
@@ -896,13 +890,17 @@ export default defineConfig([
|
|||||||
'unicorn/number-literal-case': [0],
|
'unicorn/number-literal-case': [0],
|
||||||
'unicorn/numeric-separators-style': [0],
|
'unicorn/numeric-separators-style': [0],
|
||||||
'unicorn/operator-assignment': [2],
|
'unicorn/operator-assignment': [2],
|
||||||
|
'unicorn/prefer-abort-signal-any': [2],
|
||||||
|
'unicorn/prefer-abort-signal-timeout': [2],
|
||||||
'unicorn/prefer-add-event-listener': [2],
|
'unicorn/prefer-add-event-listener': [2],
|
||||||
'unicorn/prefer-add-event-listener-options': [2],
|
'unicorn/prefer-add-event-listener-options': [2],
|
||||||
|
'unicorn/prefer-aggregate-error': [2],
|
||||||
'unicorn/prefer-array-find': [0], // handled by @typescript-eslint/prefer-find
|
'unicorn/prefer-array-find': [0], // handled by @typescript-eslint/prefer-find
|
||||||
'unicorn/prefer-array-flat': [2],
|
'unicorn/prefer-array-flat': [2],
|
||||||
'unicorn/prefer-array-flat-map': [2],
|
'unicorn/prefer-array-flat-map': [2],
|
||||||
'unicorn/prefer-array-from-async': [2],
|
'unicorn/prefer-array-from-async': [2],
|
||||||
'unicorn/prefer-array-from-map': [2],
|
'unicorn/prefer-array-from-map': [2],
|
||||||
|
'unicorn/prefer-array-from-range': [2],
|
||||||
'unicorn/prefer-array-index-of': [2],
|
'unicorn/prefer-array-index-of': [2],
|
||||||
'unicorn/prefer-array-iterable-methods': [2],
|
'unicorn/prefer-array-iterable-methods': [2],
|
||||||
'unicorn/prefer-array-last-methods': [2],
|
'unicorn/prefer-array-last-methods': [2],
|
||||||
@@ -912,6 +910,7 @@ export default defineConfig([
|
|||||||
'unicorn/prefer-await': [2],
|
'unicorn/prefer-await': [2],
|
||||||
'unicorn/prefer-bigint-literals': [2],
|
'unicorn/prefer-bigint-literals': [2],
|
||||||
'unicorn/prefer-blob-reading-methods': [2],
|
'unicorn/prefer-blob-reading-methods': [2],
|
||||||
|
'unicorn/prefer-block-statement-over-iife': [2],
|
||||||
'unicorn/prefer-boolean-return': [2],
|
'unicorn/prefer-boolean-return': [2],
|
||||||
'unicorn/prefer-class-fields': [2],
|
'unicorn/prefer-class-fields': [2],
|
||||||
'unicorn/prefer-classlist-toggle': [2],
|
'unicorn/prefer-classlist-toggle': [2],
|
||||||
@@ -924,15 +923,18 @@ export default defineConfig([
|
|||||||
'unicorn/prefer-dom-node-append': [2],
|
'unicorn/prefer-dom-node-append': [2],
|
||||||
'unicorn/prefer-dom-node-html-methods': [0],
|
'unicorn/prefer-dom-node-html-methods': [0],
|
||||||
'unicorn/prefer-dom-node-remove': [2],
|
'unicorn/prefer-dom-node-remove': [2],
|
||||||
|
'unicorn/prefer-dom-node-replace-children': [2],
|
||||||
'unicorn/prefer-dom-node-text-content': [2],
|
'unicorn/prefer-dom-node-text-content': [2],
|
||||||
'unicorn/prefer-early-return': [0],
|
'unicorn/prefer-early-return': [0],
|
||||||
'unicorn/prefer-else-if': [2],
|
'unicorn/prefer-else-if': [2],
|
||||||
|
'unicorn/prefer-error-is-error': [0],
|
||||||
'unicorn/prefer-event-target': [2],
|
'unicorn/prefer-event-target': [2],
|
||||||
'unicorn/prefer-export-from': [0],
|
'unicorn/prefer-export-from': [0],
|
||||||
'unicorn/prefer-flat-math-min-max': [2],
|
'unicorn/prefer-flat-math-min-max': [2],
|
||||||
'unicorn/prefer-get-or-insert-computed': [2],
|
'unicorn/prefer-get-or-insert-computed': [2],
|
||||||
'unicorn/prefer-global-number-constants': [2],
|
'unicorn/prefer-global-number-constants': [2],
|
||||||
'unicorn/prefer-global-this': [0],
|
'unicorn/prefer-global-this': [0],
|
||||||
|
'unicorn/prefer-group-by': [2],
|
||||||
'unicorn/prefer-has-check': [2],
|
'unicorn/prefer-has-check': [2],
|
||||||
'unicorn/prefer-hoisting-branch-code': [2],
|
'unicorn/prefer-hoisting-branch-code': [2],
|
||||||
'unicorn/prefer-https': [0], // false-positives on namespace and schema URIs
|
'unicorn/prefer-https': [0], // false-positives on namespace and schema URIs
|
||||||
@@ -942,6 +944,7 @@ export default defineConfig([
|
|||||||
'unicorn/prefer-includes-over-repeated-comparisons': [0], // too opinionated
|
'unicorn/prefer-includes-over-repeated-comparisons': [0], // too opinionated
|
||||||
'unicorn/prefer-iterable-in-constructor': [2],
|
'unicorn/prefer-iterable-in-constructor': [2],
|
||||||
'unicorn/prefer-iterator-concat': [0], // too opinionated
|
'unicorn/prefer-iterator-concat': [0], // too opinionated
|
||||||
|
'unicorn/prefer-iterator-helpers': [2],
|
||||||
'unicorn/prefer-iterator-to-array': [2],
|
'unicorn/prefer-iterator-to-array': [2],
|
||||||
'unicorn/prefer-iterator-to-array-at-end': [2],
|
'unicorn/prefer-iterator-to-array-at-end': [2],
|
||||||
'unicorn/prefer-keyboard-event-key': [2],
|
'unicorn/prefer-keyboard-event-key': [2],
|
||||||
@@ -966,9 +969,11 @@ export default defineConfig([
|
|||||||
'unicorn/prefer-object-destructuring-defaults': [2],
|
'unicorn/prefer-object-destructuring-defaults': [2],
|
||||||
'unicorn/prefer-object-from-entries': [2],
|
'unicorn/prefer-object-from-entries': [2],
|
||||||
'unicorn/prefer-object-iterable-methods': [2],
|
'unicorn/prefer-object-iterable-methods': [2],
|
||||||
|
'unicorn/prefer-observer-apis': [2],
|
||||||
'unicorn/prefer-optional-catch-binding': [2],
|
'unicorn/prefer-optional-catch-binding': [2],
|
||||||
'unicorn/prefer-path2d': [2],
|
'unicorn/prefer-path2d': [2],
|
||||||
'unicorn/prefer-private-class-fields': [0],
|
'unicorn/prefer-private-class-fields': [0],
|
||||||
|
'unicorn/prefer-promise-try': [2],
|
||||||
'unicorn/prefer-promise-with-resolvers': [2],
|
'unicorn/prefer-promise-with-resolvers': [2],
|
||||||
'unicorn/prefer-prototype-methods': [0],
|
'unicorn/prefer-prototype-methods': [0],
|
||||||
'unicorn/prefer-query-selector': [2],
|
'unicorn/prefer-query-selector': [2],
|
||||||
@@ -979,10 +984,12 @@ export default defineConfig([
|
|||||||
'unicorn/prefer-response-static-json': [2],
|
'unicorn/prefer-response-static-json': [2],
|
||||||
'unicorn/prefer-scoped-selector': [0],
|
'unicorn/prefer-scoped-selector': [0],
|
||||||
'unicorn/prefer-set-has': [0],
|
'unicorn/prefer-set-has': [0],
|
||||||
|
'unicorn/prefer-set-methods': [0],
|
||||||
'unicorn/prefer-set-size': [2],
|
'unicorn/prefer-set-size': [2],
|
||||||
'unicorn/prefer-short-arrow-method': [2],
|
'unicorn/prefer-short-arrow-method': [2],
|
||||||
'unicorn/prefer-simple-condition-first': [0],
|
'unicorn/prefer-simple-condition-first': [0],
|
||||||
'unicorn/prefer-simple-sort-comparator': [2],
|
'unicorn/prefer-simple-sort-comparator': [2],
|
||||||
|
'unicorn/prefer-simplified-conditions': [2],
|
||||||
'unicorn/prefer-single-array-predicate': [2],
|
'unicorn/prefer-single-array-predicate': [2],
|
||||||
'unicorn/prefer-single-call': [2],
|
'unicorn/prefer-single-call': [2],
|
||||||
'unicorn/prefer-single-object-destructuring': [2],
|
'unicorn/prefer-single-object-destructuring': [2],
|
||||||
@@ -1002,6 +1009,7 @@ export default defineConfig([
|
|||||||
'unicorn/prefer-switch': [0],
|
'unicorn/prefer-switch': [0],
|
||||||
'unicorn/prefer-temporal': [0],
|
'unicorn/prefer-temporal': [0],
|
||||||
'unicorn/prefer-ternary': [0],
|
'unicorn/prefer-ternary': [0],
|
||||||
|
'unicorn/prefer-toggle-attribute': [2],
|
||||||
'unicorn/prefer-top-level-await': [0],
|
'unicorn/prefer-top-level-await': [0],
|
||||||
'unicorn/prefer-type-error': [0],
|
'unicorn/prefer-type-error': [0],
|
||||||
'unicorn/prefer-type-literal-last': [0],
|
'unicorn/prefer-type-literal-last': [0],
|
||||||
@@ -1010,6 +1018,7 @@ export default defineConfig([
|
|||||||
'unicorn/prefer-unicode-code-point-escapes': [0],
|
'unicorn/prefer-unicode-code-point-escapes': [0],
|
||||||
'unicorn/prefer-url-can-parse': [2],
|
'unicorn/prefer-url-can-parse': [2],
|
||||||
'unicorn/prefer-url-href': [2],
|
'unicorn/prefer-url-href': [2],
|
||||||
|
'unicorn/prefer-url-search-parameters': [2],
|
||||||
'unicorn/prefer-while-loop-condition': [2],
|
'unicorn/prefer-while-loop-condition': [2],
|
||||||
'unicorn/prevent-abbreviations': [0],
|
'unicorn/prevent-abbreviations': [0],
|
||||||
'unicorn/relative-url-style': [2],
|
'unicorn/relative-url-style': [2],
|
||||||
|
|||||||
6
flake.lock
generated
6
flake.lock
generated
@@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1776877367,
|
"lastModified": 1782723713,
|
||||||
"narHash": "sha256-EHq1/OX139R1RvBzOJ0aMRT3xnWyqtHBRUBuO1gFzjI=",
|
"narHash": "sha256-oPXCU/SSUokcGaJREHibG1CBX3+s/W7orDWQOZDsEeQ=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "0726a0ecb6d4e08f6adced58726b95db924cef57",
|
"rev": "b5aa0fbd538984f6e3d201be0005b4463d8b09f8",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
# only bump toolchain versions here
|
# only bump toolchain versions here
|
||||||
go = pkgs.go_1_26;
|
go = pkgs.go_1_26;
|
||||||
nodejs = pkgs.nodejs_24;
|
nodejs = pkgs.nodejs_26;
|
||||||
python3 = pkgs.python314;
|
python3 = pkgs.python314;
|
||||||
pnpm = pkgs.pnpm_10;
|
pnpm = pkgs.pnpm_10;
|
||||||
|
|
||||||
|
|||||||
60
go.mod
60
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module gitea.dev
|
module gitea.dev
|
||||||
|
|
||||||
go 1.26.4
|
go 1.26.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
|
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
|
||||||
@@ -9,11 +9,11 @@ require (
|
|||||||
gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a
|
gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a
|
||||||
gitea.com/go-chi/cache v0.2.1
|
gitea.com/go-chi/cache v0.2.1
|
||||||
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
|
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
|
||||||
gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e
|
gitea.com/go-chi/session v0.0.0-20260708011333-ebced8a7a2d6
|
||||||
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
|
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
|
||||||
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
|
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
|
||||||
gitea.dev/actions-proto-go v0.6.0
|
gitea.dev/actions-proto-go v0.6.0
|
||||||
gitea.dev/sdk v1.1.0
|
gitea.dev/sdk v1.2.0
|
||||||
github.com/42wim/httpsig v1.2.4
|
github.com/42wim/httpsig v1.2.4
|
||||||
github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432
|
github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0
|
||||||
@@ -24,12 +24,12 @@ require (
|
|||||||
github.com/PuerkitoBio/goquery v1.12.0
|
github.com/PuerkitoBio/goquery v1.12.0
|
||||||
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0
|
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0
|
||||||
github.com/alecthomas/chroma/v2 v2.27.0
|
github.com/alecthomas/chroma/v2 v2.27.0
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.24
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.28
|
||||||
github.com/aws/aws-sdk-go-v2/service/codecommit v1.34.4
|
github.com/aws/aws-sdk-go-v2/service/codecommit v1.35.0
|
||||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
||||||
github.com/blevesearch/bleve/v2 v2.6.0
|
github.com/blevesearch/bleve/v2 v2.6.0
|
||||||
github.com/bohde/codel v0.2.0
|
github.com/bohde/codel v0.2.0
|
||||||
github.com/buildkite/terminal-to-html/v3 v3.16.8
|
github.com/buildkite/terminal-to-html/v3 v3.17.1
|
||||||
github.com/caddyserver/certmagic v0.25.4
|
github.com/caddyserver/certmagic v0.25.4
|
||||||
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20260309112543-12416315a635
|
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20260309112543-12416315a635
|
||||||
github.com/chi-middleware/proxy v1.1.1
|
github.com/chi-middleware/proxy v1.1.1
|
||||||
@@ -44,20 +44,20 @@ require (
|
|||||||
github.com/fsnotify/fsnotify v1.10.1
|
github.com/fsnotify/fsnotify v1.10.1
|
||||||
github.com/getkin/kin-openapi v0.140.0
|
github.com/getkin/kin-openapi v0.140.0
|
||||||
github.com/gliderlabs/ssh v0.3.8
|
github.com/gliderlabs/ssh v0.3.8
|
||||||
github.com/go-chi/chi/v5 v5.3.0
|
github.com/go-chi/chi/v5 v5.3.1
|
||||||
github.com/go-chi/cors v1.2.2
|
github.com/go-chi/cors v1.2.2
|
||||||
github.com/go-co-op/gocron/v2 v2.21.2
|
github.com/go-co-op/gocron/v2 v2.21.2
|
||||||
github.com/go-enry/go-enry/v2 v2.9.6
|
github.com/go-enry/go-enry/v2 v2.9.6
|
||||||
github.com/go-git/go-billy/v5 v5.9.0
|
github.com/go-git/go-billy/v5 v5.9.0
|
||||||
github.com/go-git/go-git/v5 v5.19.1
|
github.com/go-git/go-git/v5 v5.19.1
|
||||||
github.com/go-ldap/ldap/v3 v3.4.13
|
github.com/go-ldap/ldap/v3 v3.4.13
|
||||||
github.com/go-redsync/redsync/v4 v4.16.0
|
github.com/go-redsync/redsync/v4 v4.17.0
|
||||||
github.com/go-sql-driver/mysql v1.10.0
|
github.com/go-sql-driver/mysql v1.10.0
|
||||||
github.com/go-webauthn/webauthn v0.17.4
|
github.com/go-webauthn/webauthn v0.17.4
|
||||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
|
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
|
||||||
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
|
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
github.com/google/go-github/v88 v88.0.0
|
github.com/google/go-github/v89 v89.0.0
|
||||||
github.com/google/licenseclassifier/v2 v2.0.0
|
github.com/google/licenseclassifier/v2 v2.0.0
|
||||||
github.com/google/pprof v0.0.0-20260604005048-7023385849c0
|
github.com/google/pprof v0.0.0-20260604005048-7023385849c0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
@@ -68,8 +68,8 @@ require (
|
|||||||
github.com/huandu/xstrings v1.5.0
|
github.com/huandu/xstrings v1.5.0
|
||||||
github.com/jhillyerd/enmime/v2 v2.4.1
|
github.com/jhillyerd/enmime/v2 v2.4.1
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||||
github.com/klauspost/compress v1.18.6
|
github.com/klauspost/compress v1.19.0
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0
|
github.com/klauspost/cpuid/v2 v2.4.0
|
||||||
github.com/lib/pq v1.12.3
|
github.com/lib/pq v1.12.3
|
||||||
github.com/markbates/goth v1.82.0
|
github.com/markbates/goth v1.82.0
|
||||||
github.com/mattn/go-isatty v0.0.22
|
github.com/mattn/go-isatty v0.0.22
|
||||||
@@ -78,7 +78,7 @@ require (
|
|||||||
github.com/mholt/archives v0.1.5
|
github.com/mholt/archives v0.1.5
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27
|
github.com/microcosm-cc/bluemonday v1.0.27
|
||||||
github.com/microsoft/go-mssqldb v1.10.0
|
github.com/microsoft/go-mssqldb v1.10.0
|
||||||
github.com/minio/minio-go/v7 v7.2.0
|
github.com/minio/minio-go/v7 v7.2.1
|
||||||
github.com/msteinert/pam/v2 v2.1.0
|
github.com/msteinert/pam/v2 v2.1.0
|
||||||
github.com/niklasfasching/go-org v1.9.1
|
github.com/niklasfasching/go-org v1.9.1
|
||||||
github.com/opencontainers/go-digest v1.0.0
|
github.com/opencontainers/go-digest v1.0.0
|
||||||
@@ -96,29 +96,29 @@ require (
|
|||||||
github.com/tstranex/u2f v1.0.0
|
github.com/tstranex/u2f v1.0.0
|
||||||
github.com/ulikunitz/xz v0.5.15
|
github.com/ulikunitz/xz v0.5.15
|
||||||
github.com/urfave/cli-docs/v3 v3.1.0
|
github.com/urfave/cli-docs/v3 v3.1.0
|
||||||
github.com/urfave/cli/v3 v3.10.0
|
github.com/urfave/cli/v3 v3.10.1
|
||||||
github.com/wneessen/go-mail v0.7.3
|
github.com/wneessen/go-mail v0.8.1
|
||||||
github.com/yohcop/openid-go v1.0.1
|
github.com/yohcop/openid-go v1.0.1
|
||||||
github.com/yuin/goldmark v1.8.2
|
github.com/yuin/goldmark v1.8.2
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||||
gitlab.com/gitlab-org/api/client-go/v2 v2.42.0
|
gitlab.com/gitlab-org/api/client-go/v2 v2.46.0
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.5
|
go.yaml.in/yaml/v4 v4.0.0-rc.5
|
||||||
golang.org/x/crypto v0.53.0
|
golang.org/x/crypto v0.54.0
|
||||||
golang.org/x/image v0.43.0
|
golang.org/x/image v0.44.0
|
||||||
golang.org/x/mod v0.37.0
|
golang.org/x/mod v0.38.0
|
||||||
golang.org/x/net v0.56.0
|
golang.org/x/net v0.57.0
|
||||||
golang.org/x/oauth2 v0.36.0
|
golang.org/x/oauth2 v0.36.0
|
||||||
golang.org/x/sync v0.21.0
|
golang.org/x/sync v0.22.0
|
||||||
golang.org/x/sys v0.46.0
|
golang.org/x/sys v0.47.0
|
||||||
golang.org/x/text v0.38.0
|
golang.org/x/text v0.40.0
|
||||||
google.golang.org/grpc v1.81.1
|
google.golang.org/grpc v1.82.0
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.36.11
|
||||||
gopkg.in/ini.v1 v1.67.3
|
gopkg.in/ini.v1 v1.67.3
|
||||||
modernc.org/sqlite v1.53.0
|
modernc.org/sqlite v1.53.0
|
||||||
mvdan.cc/xurls/v2 v2.6.0
|
mvdan.cc/xurls/v2 v2.6.0
|
||||||
strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab
|
strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab
|
||||||
xorm.io/builder v0.3.13
|
xorm.io/builder v0.3.13
|
||||||
xorm.io/xorm v1.3.11
|
xorm.io/xorm v1.4.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -133,10 +133,10 @@ require (
|
|||||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||||
github.com/andybalholm/cascadia v1.3.4 // indirect
|
github.com/andybalholm/cascadia v1.3.4 // indirect
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||||
github.com/aws/aws-sdk-go-v2 v1.42.0 // indirect
|
github.com/aws/aws-sdk-go-v2 v1.42.1 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 // indirect
|
||||||
github.com/aws/smithy-go v1.27.2 // indirect
|
github.com/aws/smithy-go v1.27.3 // indirect
|
||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bits-and-blooms/bitset v1.24.5 // indirect
|
github.com/bits-and-blooms/bitset v1.24.5 // indirect
|
||||||
@@ -199,9 +199,7 @@ require (
|
|||||||
github.com/gorilla/css v1.0.1 // indirect
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/gorilla/mux v1.8.1 // indirect
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||||
github.com/inbucket/html2text v1.0.0 // indirect
|
github.com/inbucket/html2text v1.0.0 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
@@ -269,7 +267,7 @@ require (
|
|||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
||||||
golang.org/x/time v0.15.0 // indirect
|
golang.org/x/time v0.15.0 // indirect
|
||||||
golang.org/x/tools v0.45.0 // indirect
|
golang.org/x/tools v0.47.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
131
go.sum
131
go.sum
@@ -18,8 +18,8 @@ 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/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 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo=
|
||||||
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098/go.mod h1:LjzIOHlRemuUyO7WR12fmm18VZIlCAaOt9L3yKw40pk=
|
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098/go.mod h1:LjzIOHlRemuUyO7WR12fmm18VZIlCAaOt9L3yKw40pk=
|
||||||
gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e h1:4bugwPyGMLvblEm3pZ8fZProSPVxE4l0UXF2Kv6IJoY=
|
gitea.com/go-chi/session v0.0.0-20260708011333-ebced8a7a2d6 h1:YWzVGeC/8SZThrJS48ZmQYLkzssdeABxHPhbdnxPDIU=
|
||||||
gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e/go.mod h1:KDvcfMUoXfATPHs2mbMoXFTXT45/FAFAS39waz9tPk0=
|
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 h1:+wWBi6Qfruqu7xJgjOIrKVQGiLUZdpKYCZewJ4clqhw=
|
||||||
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY=
|
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 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o=
|
||||||
@@ -28,8 +28,8 @@ gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGq
|
|||||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
|
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
|
||||||
gitea.dev/actions-proto-go v0.6.0 h1:gjllYQ5vmwlkqOeofTQu5qKTZpmf7kWsafoHvoPCSzY=
|
gitea.dev/actions-proto-go v0.6.0 h1:gjllYQ5vmwlkqOeofTQu5qKTZpmf7kWsafoHvoPCSzY=
|
||||||
gitea.dev/actions-proto-go v0.6.0/go.mod h1:p4RX+D9oqiEEzzkPMXscw2CmaGuYFPWFc6xIOmDNDqs=
|
gitea.dev/actions-proto-go v0.6.0/go.mod h1:p4RX+D9oqiEEzzkPMXscw2CmaGuYFPWFc6xIOmDNDqs=
|
||||||
gitea.dev/sdk v1.1.0 h1:wLlz03WkLEiXa2bQpO1JQBTlYf7tQI2neYtZK1kU+TE=
|
gitea.dev/sdk v1.2.0 h1:avRtJl/nKCGispgSalo9czoZM9Rto1awnE0caNAoXGo=
|
||||||
gitea.dev/sdk v1.1.0/go.mod h1:Zfl+EZXdsGGCLkryDfsmvYrQo6GKMl4U3BJA8Beu+cs=
|
gitea.dev/sdk v1.2.0/go.mod h1:rfh5oNdIK24cbCREwIn1tqWKQW+IICXFGWJyebuOAOE=
|
||||||
github.com/42wim/httpsig v1.2.4 h1:mI5bH0nm4xn7K18fo1K3okNDRq8CCJ0KbBYWyA6r8lU=
|
github.com/42wim/httpsig v1.2.4 h1:mI5bH0nm4xn7K18fo1K3okNDRq8CCJ0KbBYWyA6r8lU=
|
||||||
github.com/42wim/httpsig v1.2.4/go.mod h1:yKsYfSyTBEohkPik224QPFylmzEBtda/kjyIAJjh3ps=
|
github.com/42wim/httpsig v1.2.4/go.mod h1:yKsYfSyTBEohkPik224QPFylmzEBtda/kjyIAJjh3ps=
|
||||||
github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432 h1:3Fcz1QzlS7Jv4FT2KI3cHNSZL+KPN3dXxurn9f3YL/Y=
|
github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432 h1:3Fcz1QzlS7Jv4FT2KI3cHNSZL+KPN3dXxurn9f3YL/Y=
|
||||||
@@ -92,18 +92,18 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuW
|
|||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
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/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA=
|
github.com/aws/aws-sdk-go-v2 v1.42.1 h1:9eOTgu1z/dVtYpNZ3/8/XbbaX0x/BqE3HUzAzs6K0ek=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg=
|
github.com/aws/aws-sdk-go-v2 v1.42.1/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.24 h1:2hQqYCV9yqyePQ9o6dCrZc/zO8U3TwPr9mIKlZnPu/I=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.28 h1:zTXJSsNcoO91/mTXsZoYf0AK8dvNPiA58/VtyGXR+wM=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.24/go.mod h1:IDwpACtwqHLISdzfwUUNq4P9DsB/h5BLg4FwJPNfqFY=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.28/go.mod h1:Kd9E0JzDBW/q1xbsHFrev/GnbAf5J0Ng8xoyc7HZ91Q=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 h1:xM/Is9cKMHa8Jj8zkvWhvrFkZsXJV9E+BB4g0HW0duQ=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30/go.mod h1:WueJeNDZvK1fMYEWJIkcivBfEzUkTpBhzlrUKKY8EuA=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 h1:jn46zC9LdsVR/ZpMIJqMqb8hHv31BlLx3ulVqNspUOk=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30/go.mod h1:1hTMsAgbdS/AtUi4bw8+gUuh1pceo+eXRLfpSuSQj3M=
|
||||||
github.com/aws/aws-sdk-go-v2/service/codecommit v1.34.4 h1:Uu+wqrOXozYYvaxcNIqjFsMTjoIJIZDN3R0f70ZIjyQ=
|
github.com/aws/aws-sdk-go-v2/service/codecommit v1.35.0 h1:w/iPWj+VObIbyo1WYN5010Bi61OkkqOiFZpNDAP1Rfc=
|
||||||
github.com/aws/aws-sdk-go-v2/service/codecommit v1.34.4/go.mod h1:pYrBdL1tMTZO7PaKRsa1cTUB8HtQh3fFM3zJHGhTQcE=
|
github.com/aws/aws-sdk-go-v2/service/codecommit v1.35.0/go.mod h1:uH156Pb0jnMXuCSD9irU0Qz6UNhYUF2qbowFqIwyLQ8=
|
||||||
github.com/aws/smithy-go v1.27.2 h1:y9NPmSE6am6LjEFPfqHqG/jJk7AauQvhCJONKh7kpzk=
|
github.com/aws/smithy-go v1.27.3 h1:F3Zb497UhhskkfpJmfkXswyo+t0sh9OTBnIHjogWbVY=
|
||||||
github.com/aws/smithy-go v1.27.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
github.com/aws/smithy-go v1.27.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
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/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
@@ -185,8 +185,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
|||||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
github.com/buildkite/terminal-to-html/v3 v3.16.8 h1:QN/daUob6cmK8GcdKnwn9+YTlPr1vNj+oeAIiJK6fPc=
|
github.com/buildkite/terminal-to-html/v3 v3.17.1 h1:gs9WhKUwxK27YHf3hijj4tG4wuDLfCANYbRPd5ltMdI=
|
||||||
github.com/buildkite/terminal-to-html/v3 v3.16.8/go.mod h1:+k1KVKROZocrTLsEQ9PEf9A+8+X8uaVV5iO1ZIOwKYM=
|
github.com/buildkite/terminal-to-html/v3 v3.17.1/go.mod h1:0pv+Z75FC+NvIju2o+Cw+YHIDkueeudIoeyfVm8i8GQ=
|
||||||
github.com/caddyserver/certmagic v0.25.4 h1:8eIXh0HC3MsGnNo8One+BCxMGTbe5zb/oz+2KsxBFQg=
|
github.com/caddyserver/certmagic v0.25.4 h1:8eIXh0HC3MsGnNo8One+BCxMGTbe5zb/oz+2KsxBFQg=
|
||||||
github.com/caddyserver/certmagic v0.25.4/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA=
|
github.com/caddyserver/certmagic v0.25.4/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA=
|
||||||
github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE=
|
github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE=
|
||||||
@@ -285,8 +285,8 @@ github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1T
|
|||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
|
github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8=
|
||||||
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
||||||
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
||||||
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||||
github.com/go-co-op/gocron/v2 v2.21.2 h1:bD8/YwkojYHgXFr3iEulL148KBdTbKVxUZzFKpXcdbY=
|
github.com/go-co-op/gocron/v2 v2.21.2 h1:bD8/YwkojYHgXFr3iEulL148KBdTbKVxUZzFKpXcdbY=
|
||||||
@@ -319,8 +319,8 @@ github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOr
|
|||||||
github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
|
github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
|
||||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||||
github.com/go-redsync/redsync/v4 v4.16.0 h1:bNcOzeHH9d3s6pghU9NJFMPrQa41f5Nx3L4YKr3BdEU=
|
github.com/go-redsync/redsync/v4 v4.17.0 h1:FFJ+uxZs44y4Sq10//IFKic9T94AYl+u3Sog98AHzBo=
|
||||||
github.com/go-redsync/redsync/v4 v4.16.0/go.mod h1:V4gagqgyASWBZuwx4xGzu72aZNb/6Mo05byUa3mVmKQ=
|
github.com/go-redsync/redsync/v4 v4.17.0/go.mod h1:CKVA6qwT07S/916i+Yd9h1/8YFQhCCpPYTQhvvYytJo=
|
||||||
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
|
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
|
||||||
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||||
@@ -376,8 +376,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/go-github/v88 v88.0.0 h1:dZA9IKkPK1eXZj4ypngnpRj5FwdpTv4whix2PrQMP7M=
|
github.com/google/go-github/v89 v89.0.0 h1:35bEK5XoEcF3PZrlVbl9XN63f5BcJRA/UGkxeC9xPg0=
|
||||||
github.com/google/go-github/v88 v88.0.0/go.mod h1:rufTDgn2N45wjhukLTyxmvc9nilSp3mr3Rgtt6b1MPw=
|
github.com/google/go-github/v89 v89.0.0/go.mod h1:QLcbU0ipeAqQuR5KSg8c2lql4Qk1EwJ2dWz/0rP4Nho=
|
||||||
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
||||||
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
||||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
@@ -413,17 +413,10 @@ github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pw
|
|||||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||||
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
||||||
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
||||||
github.com/graph-gophers/graphql-go v1.10.2 h1:HXu6Wu5klCH4ALn1fQHVI20cjEIa4wftavHIgbLA4Fo=
|
|
||||||
github.com/graph-gophers/graphql-go v1.10.2/go.mod h1:AsADheC4CCFwd8n1/QbkduTlHgYYMsRgtPihYVAlEsk=
|
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
|
||||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
||||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||||
@@ -471,12 +464,12 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C
|
|||||||
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
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/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.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
|
||||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
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.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||||
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
|
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
|
||||||
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
|
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
|
||||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||||
@@ -534,8 +527,8 @@ github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI
|
|||||||
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||||
github.com/minio/minio-go/v7 v7.2.0 h1:RCJM0R1XOsRs+A3x3UCaf3ZYbByDaLjFeAi+YCQEPhs=
|
github.com/minio/minio-go/v7 v7.2.1 h1:PfBfwvKB/MmqyN8Vb1G9voWisaM9OrLv+WwOvMwS9Dw=
|
||||||
github.com/minio/minio-go/v7 v7.2.0/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0=
|
github.com/minio/minio-go/v7 v7.2.1/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0=
|
||||||
github.com/minio/minlz v1.1.1 h1:OGmft1V6AnI/Wme332U6bhG54nxEan+VFgkD7lat4KM=
|
github.com/minio/minlz v1.1.1 h1:OGmft1V6AnI/Wme332U6bhG54nxEan+VFgkD7lat4KM=
|
||||||
github.com/minio/minlz v1.1.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
|
github.com/minio/minlz v1.1.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
@@ -620,10 +613,10 @@ github.com/quasoft/websspi v1.1.2/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKc
|
|||||||
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E=
|
github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E=
|
||||||
github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
|
github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
|
||||||
github.com/redis/rueidis v1.0.71 h1:pODtnAR5GAB7j4ekhldZ29HKOxe4Hph0GTDGk1ayEQY=
|
github.com/redis/rueidis v1.0.76 h1:RdDWuvlYBSp+bTrBvaXqJnNEL3VVzsnjo+0psPFgLc4=
|
||||||
github.com/redis/rueidis v1.0.71/go.mod h1:lfdcZzJ1oKGKL37vh9fO3ymwt+0TdjkkUCJxbgpmcgQ=
|
github.com/redis/rueidis v1.0.76/go.mod h1:UsfHPSbomB6QAVMk4iiFkzRy0nh9o7scDGa+SitvBY4=
|
||||||
github.com/redis/rueidis/rueidiscompat v1.0.71 h1:wNZ//kEjMZgBM0KCk7ncOX8KmAgROU2kDdDNpwheG4w=
|
github.com/redis/rueidis/rueidiscompat v1.0.76 h1:7LikbiqCQqCsZXeZ+akgZMnjIV/J0VHih9PIX4gGZC4=
|
||||||
github.com/redis/rueidis/rueidiscompat v1.0.71/go.mod h1:esmCLJvaRzZoKlgB82G1bY7Iky5TnO9Rz+NlhbEccFI=
|
github.com/redis/rueidis/rueidiscompat v1.0.76/go.mod h1:UatQQLVj4QMIsZtpvRWY28qm6r2d72idhcS+C/RM+Zg=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rhysd/actionlint v1.7.12 h1:vQ4GeJN86C0QH+gTUQcs8McmK62OLT3kmakPMtEWYnY=
|
github.com/rhysd/actionlint v1.7.12 h1:vQ4GeJN86C0QH+gTUQcs8McmK62OLT3kmakPMtEWYnY=
|
||||||
@@ -709,11 +702,11 @@ 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/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 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw=
|
||||||
github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to=
|
github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to=
|
||||||
github.com/urfave/cli/v3 v3.10.0 h1:0aU8yOObVDMkM13Cj4G+zb4P0PdeJMec65f81Ak1ioM=
|
github.com/urfave/cli/v3 v3.10.1 h1:7Kx9H50hrHbRbyxgO1KP6/BcbiGRz0uYh5YyQ30JEEY=
|
||||||
github.com/urfave/cli/v3 v3.10.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
github.com/urfave/cli/v3 v3.10.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||||
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||||
github.com/wneessen/go-mail v0.7.3 h1:g3DravXC5SMlVdboFrQA8Jx95A8sOzoBeS5F+vzNRK0=
|
github.com/wneessen/go-mail v0.8.1 h1:tVcncj02/QySVFw3zr/kXOzZcuFQqBNT6K+Rbgm/pcM=
|
||||||
github.com/wneessen/go-mail v0.7.3/go.mod h1:QGhBX0yNbc1J+Mkjcu7z2rpj4B4l+BmDY8gYznPC9sk=
|
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 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
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 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||||
@@ -740,8 +733,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/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 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||||
gitlab.com/gitlab-org/api/client-go/v2 v2.42.0 h1:Bq5YIYgUJVbt4Hbh7ibBwNR4SNEafsyDVhIXl7dXDdg=
|
gitlab.com/gitlab-org/api/client-go/v2 v2.46.0 h1:t9QSeMck3kuHu9R6C7h8cYk4qBI4ZD4SQ0jspxKYs2E=
|
||||||
gitlab.com/gitlab-org/api/client-go/v2 v2.42.0/go.mod h1:SKUbKSS59KPt6WeGNJoYF8HDaf/rFMUSITlftj/HkLg=
|
gitlab.com/gitlab-org/api/client-go/v2 v2.46.0/go.mod h1:1LK5UOowqo9apkL+zn7sM8t/hCzBV6F1b2RUIHL9+Oo=
|
||||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||||
@@ -776,12 +769,12 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
|||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||||
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
|
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
|
||||||
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
|
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
@@ -790,8 +783,8 @@ 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.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.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.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
@@ -807,8 +800,8 @@ 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.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
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.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -822,8 +815,8 @@ 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.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.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.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -854,8 +847,8 @@ 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.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.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.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -865,8 +858,8 @@ 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.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
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.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||||
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||||
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
@@ -877,8 +870,8 @@ 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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.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.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -890,16 +883,16 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
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.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.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad h1:45WmJvIV6C2+O/jjLkPUH+F3aOj/1miDoU2DD0+NWbg=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad h1:45WmJvIV6C2+O/jjLkPUH+F3aOj/1miDoU2DD0+NWbg=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU=
|
||||||
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
@@ -964,5 +957,5 @@ strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab h1:3IZDVyI
|
|||||||
strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab/go.mod h1:FJGmPh3vz9jSos1L/F91iAgnC/aejc0wIIrF2ZwJxdY=
|
strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab/go.mod h1:FJGmPh3vz9jSos1L/F91iAgnC/aejc0wIIrF2ZwJxdY=
|
||||||
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
|
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
|
||||||
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
||||||
xorm.io/xorm v1.3.11 h1:i4tlVUASogb0ZZFJHA7dZqoRU2pUpUsutnNdaOlFyMI=
|
xorm.io/xorm v1.4.1 h1:m7QlNd0eBGb31IV4Q/ow0Du83rtdC1CiwlvJZGvYde8=
|
||||||
xorm.io/xorm v1.3.11/go.mod h1:cs0ePc8O4a0jD78cNvD+0VFwhqotTvLQZv372QsDw7Q=
|
xorm.io/xorm v1.4.1/go.mod h1:cs0ePc8O4a0jD78cNvD+0VFwhqotTvLQZv372QsDw7Q=
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ func (run *ActionRun) PrettyRef() string {
|
|||||||
return refName.ShortName()
|
return refName.ShortName()
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefTooltip return a tooltop of run's ref. For pull request, it's the title of the PR, otherwise it's the ShortName.
|
// RefTooltip return a tooltip of run's ref. For pull request, it's the title of the PR, otherwise it's the ShortName.
|
||||||
func (run *ActionRun) RefTooltip() string {
|
func (run *ActionRun) RefTooltip() string {
|
||||||
payload, err := run.GetPullRequestEventPayload()
|
payload, err := run.GetPullRequestEventPayload()
|
||||||
if err == nil && payload != nil && payload.PullRequest != nil {
|
if err == nil && payload != nil && payload.PullRequest != nil {
|
||||||
@@ -277,6 +277,12 @@ func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, er
|
|||||||
return &run, nil
|
return &run, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetRunsByRepoAndID(ctx context.Context, repoID int64, runIDs []int64) ([]*ActionRun, error) {
|
||||||
|
var runs []*ActionRun
|
||||||
|
err := db.GetEngine(ctx).In("id", runIDs).Where("repo_id=?", repoID).Find(&runs)
|
||||||
|
return runs, err
|
||||||
|
}
|
||||||
|
|
||||||
func GetRunByRepoAndIndex(ctx context.Context, repoID, runIndex int64) (*ActionRun, error) {
|
func GetRunByRepoAndIndex(ctx context.Context, repoID, runIndex int64) (*ActionRun, error) {
|
||||||
run, has, err := db.Get[ActionRun](ctx, builder.Eq{"repo_id": repoID, "`index`": runIndex})
|
run, has, err := db.Get[ActionRun](ctx, builder.Eq{"repo_id": repoID, "`index`": runIndex})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -99,6 +99,10 @@ type FindRunJobOptions struct {
|
|||||||
UpdatedBefore timeutil.TimeStamp
|
UpdatedBefore timeutil.TimeStamp
|
||||||
ConcurrencyGroup string
|
ConcurrencyGroup string
|
||||||
OrderBy db.SearchOrderBy
|
OrderBy db.SearchOrderBy
|
||||||
|
// AccessibleRepoIDsSubQuery, when non-nil, restricts results to the repo IDs selected by the
|
||||||
|
// subquery (the caller's accessible repos). A nil value means no restriction. Using a subquery
|
||||||
|
// instead of a materialized ID slice avoids exceeding DB parameter limits for large owners.
|
||||||
|
AccessibleRepoIDsSubQuery *builder.Builder
|
||||||
}
|
}
|
||||||
|
|
||||||
var JobOrderByMap = map[string]map[string]db.SearchOrderBy{
|
var JobOrderByMap = map[string]map[string]db.SearchOrderBy{
|
||||||
@@ -132,6 +136,9 @@ func (opts FindRunJobOptions) ToConds() builder.Cond {
|
|||||||
}
|
}
|
||||||
cond = cond.And(builder.Eq{"`action_run_job`.concurrency_group": opts.ConcurrencyGroup})
|
cond = cond.And(builder.Eq{"`action_run_job`.concurrency_group": opts.ConcurrencyGroup})
|
||||||
}
|
}
|
||||||
|
if opts.AccessibleRepoIDsSubQuery != nil {
|
||||||
|
cond = cond.And(builder.In("`action_run_job`.repo_id", opts.AccessibleRepoIDsSubQuery))
|
||||||
|
}
|
||||||
return cond
|
return cond
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ type FindRunOptions struct {
|
|||||||
Status []Status
|
Status []Status
|
||||||
ConcurrencyGroup string
|
ConcurrencyGroup string
|
||||||
CommitSHA string
|
CommitSHA string
|
||||||
|
// AccessibleRepoIDsSubQuery, when non-nil, restricts results to the repo IDs selected by the
|
||||||
|
// subquery (the caller's accessible repos). A nil value means no restriction. Using a subquery
|
||||||
|
// instead of a materialized ID slice avoids exceeding DB parameter limits for large owners.
|
||||||
|
AccessibleRepoIDsSubQuery *builder.Builder
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts FindRunOptions) ToConds() builder.Cond {
|
func (opts FindRunOptions) ToConds() builder.Cond {
|
||||||
@@ -101,6 +105,9 @@ func (opts FindRunOptions) ToConds() builder.Cond {
|
|||||||
if opts.CommitSHA != "" {
|
if opts.CommitSHA != "" {
|
||||||
cond = cond.And(builder.Eq{"`action_run`.commit_sha": opts.CommitSHA})
|
cond = cond.And(builder.Eq{"`action_run`.commit_sha": opts.CommitSHA})
|
||||||
}
|
}
|
||||||
|
if opts.AccessibleRepoIDsSubQuery != nil {
|
||||||
|
cond = cond.And(builder.In("`action_run`.repo_id", opts.AccessibleRepoIDsSubQuery))
|
||||||
|
}
|
||||||
return cond
|
return cond
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,8 +142,8 @@ type StatusInfo struct {
|
|||||||
|
|
||||||
// GetStatusInfoList returns a slice of StatusInfo
|
// GetStatusInfoList returns a slice of StatusInfo
|
||||||
func GetStatusInfoList(ctx context.Context, lang translation.Locale) []StatusInfo {
|
func GetStatusInfoList(ctx context.Context, lang translation.Locale) []StatusInfo {
|
||||||
// same as those in aggregateJobStatus
|
// same as those in aggregateJobStatus (StatusUnknown excluded; it's the "shouldn't happen" fallback)
|
||||||
allStatus := []Status{StatusSuccess, StatusFailure, StatusWaiting, StatusRunning, StatusCancelling}
|
allStatus := []Status{StatusSuccess, StatusFailure, StatusCancelled, StatusSkipped, StatusWaiting, StatusRunning, StatusBlocked, StatusCancelling}
|
||||||
statusInfoList := make([]StatusInfo, 0, len(allStatus))
|
statusInfoList := make([]StatusInfo, 0, len(allStatus))
|
||||||
for _, s := range allStatus {
|
for _, s := range allStatus {
|
||||||
statusInfoList = append(statusInfoList, StatusInfo{
|
statusInfoList = append(statusInfoList, StatusInfo{
|
||||||
|
|||||||
@@ -73,8 +73,11 @@ func TestGetStatusInfoList(t *testing.T) {
|
|||||||
assert.Equal(t, []StatusInfo{
|
assert.Equal(t, []StatusInfo{
|
||||||
{Status: int(StatusSuccess), StatusName: StatusSuccess.String(), DisplayedStatus: "actions.status.success"},
|
{Status: int(StatusSuccess), StatusName: StatusSuccess.String(), DisplayedStatus: "actions.status.success"},
|
||||||
{Status: int(StatusFailure), StatusName: StatusFailure.String(), DisplayedStatus: "actions.status.failure"},
|
{Status: int(StatusFailure), StatusName: StatusFailure.String(), DisplayedStatus: "actions.status.failure"},
|
||||||
|
{Status: int(StatusCancelled), StatusName: StatusCancelled.String(), DisplayedStatus: "actions.status.cancelled"},
|
||||||
|
{Status: int(StatusSkipped), StatusName: StatusSkipped.String(), DisplayedStatus: "actions.status.skipped"},
|
||||||
{Status: int(StatusWaiting), StatusName: StatusWaiting.String(), DisplayedStatus: "actions.status.waiting"},
|
{Status: int(StatusWaiting), StatusName: StatusWaiting.String(), DisplayedStatus: "actions.status.waiting"},
|
||||||
{Status: int(StatusRunning), StatusName: StatusRunning.String(), DisplayedStatus: "actions.status.running"},
|
{Status: int(StatusRunning), StatusName: StatusRunning.String(), DisplayedStatus: "actions.status.running"},
|
||||||
|
{Status: int(StatusBlocked), StatusName: StatusBlocked.String(), DisplayedStatus: "actions.status.blocked"},
|
||||||
{Status: int(StatusCancelling), StatusName: StatusCancelling.String(), DisplayedStatus: "actions.status.cancelling"},
|
{Status: int(StatusCancelling), StatusName: StatusCancelling.String(), DisplayedStatus: "actions.status.cancelling"},
|
||||||
}, statusInfoList)
|
}, statusInfoList)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,8 +75,28 @@ type ActionRunner struct {
|
|||||||
const (
|
const (
|
||||||
RunnerOfflineTime = time.Minute
|
RunnerOfflineTime = time.Minute
|
||||||
RunnerIdleTime = 10 * time.Second
|
RunnerIdleTime = 10 * time.Second
|
||||||
|
// RunnerHeartbeatInterval is how often last_online is persisted on poll.
|
||||||
|
// Must stay well below RunnerOfflineTime so runners don't flap to offline.
|
||||||
|
RunnerHeartbeatInterval = 30 * time.Second
|
||||||
|
// RunnerActiveInterval is how often last_active is persisted while a runner
|
||||||
|
// streams task updates and logs. Must stay well below RunnerIdleTime so a
|
||||||
|
// busy runner keeps showing ACTIVE without a DB write on every RPC.
|
||||||
|
RunnerActiveInterval = 5 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ShouldPersistLastOnline reports whether last_online is stale enough to be
|
||||||
|
// worth writing back. Avoids a DB write on every runner poll.
|
||||||
|
func ShouldPersistLastOnline(last timeutil.TimeStamp, now time.Time) bool {
|
||||||
|
return now.Sub(last.AsTime()) >= RunnerHeartbeatInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldPersistLastActive reports whether last_active is stale enough to be
|
||||||
|
// worth writing back. Avoids a DB write on every UpdateTask/UpdateLog RPC while
|
||||||
|
// a runner is actively streaming logs.
|
||||||
|
func ShouldPersistLastActive(last timeutil.TimeStamp, now time.Time) bool {
|
||||||
|
return now.Sub(last.AsTime()) >= RunnerActiveInterval
|
||||||
|
}
|
||||||
|
|
||||||
// BelongsToOwnerName before calling, should guarantee that all attributes are loaded
|
// BelongsToOwnerName before calling, should guarantee that all attributes are loaded
|
||||||
func (r *ActionRunner) BelongsToOwnerName() string {
|
func (r *ActionRunner) BelongsToOwnerName() string {
|
||||||
if r.RepoID != 0 {
|
if r.RepoID != 0 {
|
||||||
@@ -251,21 +271,24 @@ func (opts FindRunnerOptions) ToConds() builder.Cond {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (opts FindRunnerOptions) ToOrders() string {
|
func (opts FindRunnerOptions) ToOrders() string {
|
||||||
|
// A unique tiebreaker (id) is appended so that runners sharing the same
|
||||||
|
// last_online or name keep a deterministic order across paginated queries,
|
||||||
|
// otherwise the same runner may appear on more than one page.
|
||||||
switch opts.Sort {
|
switch opts.Sort {
|
||||||
case "online":
|
case "online":
|
||||||
return "last_online DESC"
|
return "last_online DESC, id ASC"
|
||||||
case "offline":
|
case "offline":
|
||||||
return "last_online ASC"
|
return "last_online ASC, id ASC"
|
||||||
case "alphabetically":
|
case "alphabetically":
|
||||||
return "name ASC"
|
return "name ASC, id ASC"
|
||||||
case "reversealphabetically":
|
case "reversealphabetically":
|
||||||
return "name DESC"
|
return "name DESC, id ASC"
|
||||||
case "newest":
|
case "newest":
|
||||||
return "id DESC"
|
return "id DESC"
|
||||||
case "oldest":
|
case "oldest":
|
||||||
return "id ASC"
|
return "id ASC"
|
||||||
}
|
}
|
||||||
return "last_online DESC"
|
return "last_online DESC, id ASC"
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRunnerByUUID returns a runner via uuid
|
// GetRunnerByUUID returns a runner via uuid
|
||||||
|
|||||||
149
models/actions/runner_test.go
Normal file
149
models/actions/runner_test.go
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.dev/models/db"
|
||||||
|
"gitea.dev/models/unittest"
|
||||||
|
"gitea.dev/modules/timeutil"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestShouldPersistLastOnline(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
last timeutil.TimeStamp
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "fresh, skip write",
|
||||||
|
last: timeutil.TimeStamp(now.Add(-5 * time.Second).Unix()),
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exactly at interval, write",
|
||||||
|
last: timeutil.TimeStamp(now.Add(-RunnerHeartbeatInterval).Unix()),
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stale, write",
|
||||||
|
last: timeutil.TimeStamp(now.Add(-2 * RunnerHeartbeatInterval).Unix()),
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero (never seen), write",
|
||||||
|
last: 0,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.want, ShouldPersistLastOnline(tt.last, now))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldPersistLastActive(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
last timeutil.TimeStamp
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "fresh, skip write",
|
||||||
|
last: timeutil.TimeStamp(now.Add(-1 * time.Second).Unix()),
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exactly at interval, write",
|
||||||
|
last: timeutil.TimeStamp(now.Add(-RunnerActiveInterval).Unix()),
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stale, write",
|
||||||
|
last: timeutil.TimeStamp(now.Add(-2 * RunnerActiveInterval).Unix()),
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero (never seen), write",
|
||||||
|
last: 0,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.want, ShouldPersistLastActive(tt.last, now))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindRunnerOptions_ToOrders_StableTiebreaker(t *testing.T) {
|
||||||
|
// Sorts on a non-unique column must end with the unique id tiebreaker so
|
||||||
|
// pagination is deterministic; without it, runners sharing the same
|
||||||
|
// last_online or name can appear on more than one page. Sorts already on
|
||||||
|
// the unique id need no tiebreaker.
|
||||||
|
expected := map[string]string{
|
||||||
|
"": "last_online DESC, id ASC",
|
||||||
|
"online": "last_online DESC, id ASC",
|
||||||
|
"offline": "last_online ASC, id ASC",
|
||||||
|
"alphabetically": "name ASC, id ASC",
|
||||||
|
"reversealphabetically": "name DESC, id ASC",
|
||||||
|
"newest": "id DESC",
|
||||||
|
"oldest": "id ASC",
|
||||||
|
}
|
||||||
|
for sort, want := range expected {
|
||||||
|
assert.Equal(t, want, FindRunnerOptions{Sort: sort}.ToOrders(), "sort %q", sort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindRunners_PaginationNoDuplicates(t *testing.T) {
|
||||||
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
ctx := t.Context()
|
||||||
|
|
||||||
|
// Create several runners that all share the same last_online value so the
|
||||||
|
// primary sort key (last_online) is tied for all of them.
|
||||||
|
const ownerID = 1000
|
||||||
|
const count = 6
|
||||||
|
for i := range count {
|
||||||
|
runner := &ActionRunner{
|
||||||
|
Name: "paginated-runner",
|
||||||
|
UUID: fmt.Sprintf("PAGINATE-TEST-0000-0000-00000000000%d", i),
|
||||||
|
TokenHash: fmt.Sprintf("paginate-test-token-hash-%d", i),
|
||||||
|
OwnerID: ownerID,
|
||||||
|
RepoID: 0,
|
||||||
|
LastOnline: 42,
|
||||||
|
}
|
||||||
|
require.NoError(t, db.Insert(ctx, runner))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page through the runners and ensure every id is returned exactly once.
|
||||||
|
seen := make(map[int64]int)
|
||||||
|
const pageSize = 2
|
||||||
|
for page := 1; ; page++ {
|
||||||
|
runners, err := db.Find[ActionRunner](ctx, FindRunnerOptions{
|
||||||
|
ListOptions: db.ListOptions{Page: page, PageSize: pageSize},
|
||||||
|
OwnerID: ownerID,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
if len(runners) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for _, r := range runners {
|
||||||
|
seen[r.ID]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Len(t, seen, count, "each runner should be returned exactly once across all pages")
|
||||||
|
for id, n := range seen {
|
||||||
|
assert.Equal(t, 1, n, "runner %d appeared on %d pages", id, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"gitea.dev/models/db"
|
"gitea.dev/models/db"
|
||||||
"gitea.dev/models/unit"
|
"gitea.dev/models/unit"
|
||||||
"gitea.dev/modules/actions/jobparser"
|
"gitea.dev/modules/actions/jobparser"
|
||||||
|
"gitea.dev/modules/globallock"
|
||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
"gitea.dev/modules/timeutil"
|
"gitea.dev/modules/timeutil"
|
||||||
@@ -231,6 +232,11 @@ func makeTaskStepDisplayName(step *jobparser.Step, limit int) (name string) {
|
|||||||
// another runner won the optimistic-lock race; it is never returned to callers.
|
// another runner won the optimistic-lock race; it is never returned to callers.
|
||||||
var errJobAlreadyClaimed = errors.New("job already claimed by another runner")
|
var errJobAlreadyClaimed = errors.New("job already claimed by another runner")
|
||||||
|
|
||||||
|
// pickTaskBatchSize bounds how many waiting jobs each CreateTaskForRunner query loads,
|
||||||
|
// so a large backlog is not fetched into memory on every runner poll.
|
||||||
|
// It is a var only so tests can shrink it to exercise pagination cheaply.
|
||||||
|
var pickTaskBatchSize = 100
|
||||||
|
|
||||||
// CreateTaskForRunner finds a waiting job that matches the runner's labels and
|
// CreateTaskForRunner finds a waiting job that matches the runner's labels and
|
||||||
// atomically claims it. It iterates through all matching jobs so that a
|
// atomically claims it. It iterates through all matching jobs so that a
|
||||||
// concurrent claim by another runner (which would lose the optimistic lock on
|
// concurrent claim by another runner (which would lose the optimistic lock on
|
||||||
@@ -249,17 +255,30 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
|
|||||||
Join("INNER", "repo_unit", "`repository`.id = `repo_unit`.repo_id").
|
Join("INNER", "repo_unit", "`repository`.id = `repo_unit`.repo_id").
|
||||||
Where(builder.Eq{"`repository`.owner_id": runner.OwnerID, "`repo_unit`.type": unit.TypeActions}))
|
Where(builder.Eq{"`repository`.owner_id": runner.OwnerID, "`repo_unit`.type": unit.TypeActions}))
|
||||||
}
|
}
|
||||||
if jobCond.IsValid() {
|
baseCond := builder.Eq{"task_id": 0, "status": StatusWaiting, "is_reusable_caller": false}.And(jobCond)
|
||||||
jobCond = builder.In("run_id", builder.Select("id").From("action_run").Where(jobCond))
|
|
||||||
}
|
|
||||||
|
|
||||||
var jobs []*ActionRunJob
|
|
||||||
if err := e.Where("task_id=? AND status=? AND is_reusable_caller=?", 0, StatusWaiting, false).And(jobCond).Asc("updated", "id").Find(&jobs); err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: a more efficient way to filter labels
|
// TODO: a more efficient way to filter labels
|
||||||
log.Trace("runner labels: %v", runner.AgentLabels)
|
log.Trace("runner labels: %v", runner.AgentLabels)
|
||||||
|
|
||||||
|
// Page through the waiting jobs oldest-first instead of loading the whole backlog into memory on every poll.
|
||||||
|
// Keyset pagination on (updated, id) is safe under concurrent claims:
|
||||||
|
// updated only moves forward, so the advancing cursor never skips a still-waiting job even as claimed jobs drop out.
|
||||||
|
var cursorUpdated timeutil.TimeStamp
|
||||||
|
var cursorID int64
|
||||||
|
for {
|
||||||
|
cond := baseCond
|
||||||
|
if cursorID > 0 {
|
||||||
|
cond = cond.And(builder.Or(
|
||||||
|
builder.Gt{"updated": cursorUpdated},
|
||||||
|
builder.And(builder.Eq{"updated": cursorUpdated}, builder.Gt{"id": cursorID}),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
var jobs []*ActionRunJob
|
||||||
|
if err := e.Where(cond).Asc("updated", "id").Limit(pickTaskBatchSize).Find(&jobs); err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
for _, v := range jobs {
|
for _, v := range jobs {
|
||||||
if !runner.CanMatchLabels(v.RunsOn) {
|
if !runner.CanMatchLabels(v.RunsOn) {
|
||||||
continue
|
continue
|
||||||
@@ -273,7 +292,14 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
|
|||||||
}
|
}
|
||||||
// Another runner claimed this job concurrently; try the next one.
|
// Another runner claimed this job concurrently; try the next one.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A short page means no waiting jobs remain beyond it.
|
||||||
|
if len(jobs) < pickTaskBatchSize {
|
||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
last := jobs[len(jobs)-1]
|
||||||
|
cursorUpdated, cursorID = last.Updated, last.ID
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// claimJobForRunner attempts to atomically claim job for runner inside its own
|
// claimJobForRunner attempts to atomically claim job for runner inside its own
|
||||||
@@ -413,6 +439,18 @@ func UpdateTask(ctx context.Context, task *ActionTask, cols ...string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getRunIDByTaskID(ctx context.Context, taskID int64) (runID int64, _ error) {
|
||||||
|
if has, err := db.GetEngine(ctx).Cols("action_run_job.run_id").
|
||||||
|
Table("action_task").
|
||||||
|
Join("INNER", "action_run_job", "action_run_job.id = action_task.job_id").
|
||||||
|
Where(builder.Eq{"action_task.id": taskID}).Get(&runID); err != nil {
|
||||||
|
return runID, err
|
||||||
|
} else if !has {
|
||||||
|
return runID, util.ErrNotExist
|
||||||
|
}
|
||||||
|
return runID, nil
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateTaskByState updates the task by the state.
|
// UpdateTaskByState updates the task by the state.
|
||||||
// It will always update the task if the state is not final, even there is no change.
|
// It will always update the task if the state is not final, even there is no change.
|
||||||
// So it will update ActionTask.Updated to avoid the task being judged as a zombie task.
|
// So it will update ActionTask.Updated to avoid the task being judged as a zombie task.
|
||||||
@@ -422,21 +460,26 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task
|
|||||||
stepStates[v.Id] = v
|
stepStates[v.Id] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
return db.WithTx2(ctx, func(ctx context.Context) (*ActionTask, error) {
|
// Only one request can update the task because the final state needs to be calculated with all job states.
|
||||||
e := db.GetEngine(ctx)
|
// Otherwise, concurrent requests with transaction will make the SQL read stale job state and result in wrong final state.
|
||||||
|
taskID := state.Id
|
||||||
task := &ActionTask{}
|
runID, err := getRunIDByTaskID(ctx, taskID)
|
||||||
if has, err := e.ID(state.Id).Get(task); err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
}
|
||||||
|
task := &ActionTask{}
|
||||||
|
err = globallock.LockAndDo(ctx, fmt.Sprintf("UpdateTaskByState-run-%d", runID), func(ctx context.Context) error {
|
||||||
|
if has, err := db.GetEngine(ctx).ID(taskID).Get(task); err != nil {
|
||||||
|
return err
|
||||||
} else if !has {
|
} else if !has {
|
||||||
return nil, util.ErrNotExist
|
return util.ErrNotExist
|
||||||
} else if runnerID != task.RunnerID {
|
} else if runnerID != task.RunnerID {
|
||||||
return nil, errors.New("invalid runner for task")
|
return errors.New("invalid runner for task")
|
||||||
}
|
}
|
||||||
|
|
||||||
if task.Status.IsDone() {
|
if task.Status.IsDone() {
|
||||||
// the state is final, do nothing
|
// the state is final, do nothing
|
||||||
return task, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// state.Result is not unspecified means the task is finished
|
// state.Result is not unspecified means the task is finished
|
||||||
@@ -449,7 +492,7 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task
|
|||||||
}
|
}
|
||||||
task.Stopped = timeutil.TimeStamp(state.StoppedAt.AsTime().Unix())
|
task.Stopped = timeutil.TimeStamp(state.StoppedAt.AsTime().Unix())
|
||||||
if err := UpdateTask(ctx, task, "status", "stopped"); err != nil {
|
if err := UpdateTask(ctx, task, "status", "stopped"); err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := UpdateRunJob(ctx, &ActionRunJob{
|
if _, err := UpdateRunJob(ctx, &ActionRunJob{
|
||||||
ID: task.JobID,
|
ID: task.JobID,
|
||||||
@@ -457,18 +500,18 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task
|
|||||||
Status: task.Status,
|
Status: task.Status,
|
||||||
Stopped: task.Stopped,
|
Stopped: task.Stopped,
|
||||||
}, nil, "status", "stopped"); err != nil {
|
}, nil, "status", "stopped"); err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Force update ActionTask.Updated to avoid the task being judged as a zombie task
|
// Force update ActionTask.Updated to avoid the task being judged as a zombie task
|
||||||
task.Updated = timeutil.TimeStampNow()
|
task.Updated = timeutil.TimeStampNow()
|
||||||
if err := UpdateTask(ctx, task, "updated"); err != nil {
|
if err := UpdateTask(ctx, task, "updated"); err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := task.LoadAttributes(ctx); err != nil {
|
if err := task.LoadAttributes(ctx); err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, step := range task.Steps {
|
for _, step := range task.Steps {
|
||||||
@@ -485,13 +528,13 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task
|
|||||||
} else if step.Started != 0 {
|
} else if step.Started != 0 {
|
||||||
step.Status = StatusRunning
|
step.Status = StatusRunning
|
||||||
}
|
}
|
||||||
if _, err := e.ID(step.ID).Update(step); err != nil {
|
if _, err := db.GetEngine(ctx).ID(step.ID).Update(step); err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
return task, nil
|
|
||||||
})
|
})
|
||||||
|
return task, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func StopTask(ctx context.Context, taskID int64, status Status) error {
|
func StopTask(ctx context.Context, taskID int64, status Status) error {
|
||||||
|
|||||||
@@ -371,3 +371,74 @@ func TestReleaseTaskForRunner(t *testing.T) {
|
|||||||
unittest.AssertNotExistsBean(t, &ActionTask{ID: task.ID})
|
unittest.AssertNotExistsBean(t, &ActionTask{ID: task.ID})
|
||||||
unittest.AssertNotExistsBean(t, &ActionTaskStep{TaskID: task.ID})
|
unittest.AssertNotExistsBean(t, &ActionTaskStep{TaskID: task.ID})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestCreateTaskForRunnerPagination verifies that a job sitting beyond the first page is still claimed
|
||||||
|
func TestCreateTaskForRunnerPagination(t *testing.T) {
|
||||||
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
defer func(orig int) { pickTaskBatchSize = orig }(pickTaskBatchSize)
|
||||||
|
pickTaskBatchSize = 2
|
||||||
|
|
||||||
|
run := &ActionRun{
|
||||||
|
Title: "pagination-test-run",
|
||||||
|
RepoID: 1,
|
||||||
|
OwnerID: 2,
|
||||||
|
WorkflowID: "test.yaml",
|
||||||
|
Index: 9903,
|
||||||
|
TriggerUserID: 2,
|
||||||
|
Ref: "refs/heads/main",
|
||||||
|
CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0",
|
||||||
|
Event: "push",
|
||||||
|
TriggerEvent: "push",
|
||||||
|
Status: StatusWaiting,
|
||||||
|
}
|
||||||
|
require.NoError(t, db.Insert(t.Context(), run))
|
||||||
|
|
||||||
|
// Five waiting jobs the runner cannot run, then one it can.
|
||||||
|
// With a page size of 2 the matching job only appears on the third page.
|
||||||
|
for i := range 5 {
|
||||||
|
mismatch := &ActionRunJob{
|
||||||
|
RunID: run.ID,
|
||||||
|
RepoID: run.RepoID,
|
||||||
|
OwnerID: run.OwnerID,
|
||||||
|
CommitSHA: run.CommitSHA,
|
||||||
|
Name: "mismatch-" + string(rune('a'+i)),
|
||||||
|
Attempt: 1,
|
||||||
|
JobID: "mismatch-" + string(rune('a'+i)),
|
||||||
|
Status: StatusWaiting,
|
||||||
|
RunsOn: []string{"windows-latest"},
|
||||||
|
}
|
||||||
|
require.NoError(t, db.Insert(t.Context(), mismatch))
|
||||||
|
}
|
||||||
|
|
||||||
|
target := &ActionRunJob{
|
||||||
|
RunID: run.ID,
|
||||||
|
RepoID: run.RepoID,
|
||||||
|
OwnerID: run.OwnerID,
|
||||||
|
CommitSHA: run.CommitSHA,
|
||||||
|
Name: "target-job",
|
||||||
|
Attempt: 1,
|
||||||
|
JobID: "target-job",
|
||||||
|
Status: StatusWaiting,
|
||||||
|
RunsOn: []string{"ubuntu-latest"},
|
||||||
|
WorkflowPayload: []byte("on: push\njobs:\n target-job:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n"),
|
||||||
|
}
|
||||||
|
require.NoError(t, db.Insert(t.Context(), target))
|
||||||
|
|
||||||
|
runner := &ActionRunner{
|
||||||
|
UUID: "pagination-runner-uuid",
|
||||||
|
Name: "pagination-runner",
|
||||||
|
AgentLabels: []string{"ubuntu-latest"},
|
||||||
|
}
|
||||||
|
runner.GenerateAndFillToken()
|
||||||
|
require.NoError(t, db.Insert(t.Context(), runner))
|
||||||
|
|
||||||
|
task, ok, err := CreateTaskForRunner(t.Context(), runner)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.NotNil(t, task)
|
||||||
|
|
||||||
|
claimed := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: target.ID})
|
||||||
|
assert.Equal(t, StatusRunning, claimed.Status)
|
||||||
|
assert.Equal(t, task.ID, claimed.TaskID)
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,7 +36,18 @@ import (
|
|||||||
|
|
||||||
const ssh2keyStart = "---- BEGIN SSH2 PUBLIC KEY ----"
|
const ssh2keyStart = "---- BEGIN SSH2 PUBLIC KEY ----"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// the longest RSA key ssh-keygen allows to generate is 16384 bits (2048 bytes), we still relax the limit a little here
|
||||||
|
maxKeyBinaryBytes = 4096
|
||||||
|
maxKeyContentBase64Bytes = maxKeyBinaryBytes * 4 / 3
|
||||||
|
maxKeyContentExtraBytes = 4 * 1024 // header, footer, comment
|
||||||
|
maxKeyContentBytes = maxKeyContentBase64Bytes + maxKeyContentExtraBytes
|
||||||
|
)
|
||||||
|
|
||||||
func extractTypeFromBase64Key(key string) (string, error) {
|
func extractTypeFromBase64Key(key string) (string, error) {
|
||||||
|
if len(key) > maxKeyContentBase64Bytes {
|
||||||
|
return "", util.NewInvalidArgumentErrorf("SSH public key base64 is too long")
|
||||||
|
}
|
||||||
b, err := base64.StdEncoding.DecodeString(key)
|
b, err := base64.StdEncoding.DecodeString(key)
|
||||||
if err != nil || len(b) < 4 {
|
if err != nil || len(b) < 4 {
|
||||||
return "", fmt.Errorf("invalid key format: %w", err)
|
return "", fmt.Errorf("invalid key format: %w", err)
|
||||||
@@ -52,6 +63,10 @@ func extractTypeFromBase64Key(key string) (string, error) {
|
|||||||
|
|
||||||
// parseKeyString parses any key string in OpenSSH or SSH2 format to clean OpenSSH string (RFC4253).
|
// parseKeyString parses any key string in OpenSSH or SSH2 format to clean OpenSSH string (RFC4253).
|
||||||
func parseKeyString(content string) (string, error) {
|
func parseKeyString(content string) (string, error) {
|
||||||
|
if len(content) > maxKeyContentBytes {
|
||||||
|
return "", util.NewInvalidArgumentErrorf("SSH public key content is too long")
|
||||||
|
}
|
||||||
|
|
||||||
// remove whitespace at start and end
|
// remove whitespace at start and end
|
||||||
content = strings.TrimSpace(content)
|
content = strings.TrimSpace(content)
|
||||||
|
|
||||||
@@ -63,6 +78,8 @@ func parseKeyString(content string) (string, error) {
|
|||||||
// Transform all legal line endings to a single "\n".
|
// Transform all legal line endings to a single "\n".
|
||||||
content = strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(content)
|
content = strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(content)
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
b.Grow(len(content))
|
||||||
lines := strings.Split(content, "\n")
|
lines := strings.Split(content, "\n")
|
||||||
continuationLine := false
|
continuationLine := false
|
||||||
|
|
||||||
@@ -74,9 +91,10 @@ func parseKeyString(content string) (string, error) {
|
|||||||
if continuationLine || strings.ContainsAny(line, ":-") {
|
if continuationLine || strings.ContainsAny(line, ":-") {
|
||||||
continuationLine = strings.HasSuffix(line, "\\")
|
continuationLine = strings.HasSuffix(line, "\\")
|
||||||
} else {
|
} else {
|
||||||
keyContent += line
|
b.WriteString(line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
keyContent = b.String()
|
||||||
|
|
||||||
t, err := extractTypeFromBase64Key(keyContent)
|
t, err := extractTypeFromBase64Key(keyContent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -473,10 +473,20 @@ func runErr(t *testing.T, stdin []byte, args ...string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_PublicKeysAreExternallyManaged(t *testing.T) {
|
func TestPublicKeysAreExternallyManaged(t *testing.T) {
|
||||||
key1 := unittest.AssertExistsAndLoadBean(t, &PublicKey{ID: 1})
|
key1 := unittest.AssertExistsAndLoadBean(t, &PublicKey{ID: 1})
|
||||||
externals, err := PublicKeysAreExternallyManaged(t.Context(), []*PublicKey{key1})
|
externals, err := PublicKeysAreExternallyManaged(t.Context(), []*PublicKey{key1})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, externals, 1)
|
assert.Len(t, externals, 1)
|
||||||
assert.False(t, externals[0])
|
assert.False(t, externals[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestCheckPublicKeyStringOversized tests if oversized SSH2 public key strings are rejected before triggering costly operations.
|
||||||
|
func TestCheckPublicKeyStringOversized(t *testing.T) {
|
||||||
|
_, err := parseKeyString(strings.Repeat("a", maxKeyContentBytes+1))
|
||||||
|
assert.ErrorContains(t, err, "SSH public key content is too long")
|
||||||
|
|
||||||
|
content := "---- BEGIN SSH2 PUBLIC KEY ----\n" + strings.Repeat("a", maxKeyContentBase64Bytes+1) + "\n--- END SSH2 PUBLIC KEY ----"
|
||||||
|
_, err = parseKeyString(content)
|
||||||
|
assert.ErrorContains(t, err, "SSH public key base64 is too long")
|
||||||
|
}
|
||||||
|
|||||||
@@ -304,6 +304,36 @@ func (s AccessTokenScope) PublicOnly() (bool, error) {
|
|||||||
return bitmap.hasScope(AccessTokenScopePublicOnly)
|
return bitmap.hasScope(AccessTokenScopePublicOnly)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CanCreateChildScope reports whether a request authenticated by this (parent) scope may mint a token
|
||||||
|
// carrying the child scope. It rejects any grantable scope the parent does not hold, closing the
|
||||||
|
// scope-escalation path. public-only is a restriction rather than a grantable permission, so it is
|
||||||
|
// ignored here (a child may always be public-only); EnforcePublicOnlyFrom handles carrying it down.
|
||||||
|
func (s AccessTokenScope) CanCreateChildScope(child AccessTokenScope) (bool, error) {
|
||||||
|
requested := child.StringSlice()
|
||||||
|
scopes := make([]AccessTokenScope, 0, len(requested))
|
||||||
|
for _, sc := range requested {
|
||||||
|
childScope := AccessTokenScope(sc)
|
||||||
|
if childScope == AccessTokenScopePublicOnly {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
scopes = append(scopes, childScope)
|
||||||
|
}
|
||||||
|
return s.HasScope(scopes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnforcePublicOnlyFrom adds the public-only restriction to s when the authorizing parent scope is
|
||||||
|
// public-only, so a public-only token cannot mint a child token that drops the restriction.
|
||||||
|
func (s AccessTokenScope) EnforcePublicOnlyFrom(parent AccessTokenScope) (AccessTokenScope, error) {
|
||||||
|
publicOnly, err := parent.PublicOnly()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if !publicOnly {
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
return AccessTokenScope(string(s) + "," + string(AccessTokenScopePublicOnly)).Normalize()
|
||||||
|
}
|
||||||
|
|
||||||
// HasScope returns true if the string has the given scope
|
// HasScope returns true if the string has the given scope
|
||||||
func (s AccessTokenScope) HasScope(scopes ...AccessTokenScope) (bool, error) {
|
func (s AccessTokenScope) HasScope(scopes ...AccessTokenScope) (bool, error) {
|
||||||
bitmap, err := s.parse()
|
bitmap, err := s.parse()
|
||||||
|
|||||||
@@ -89,3 +89,26 @@ func TestAccessTokenScope_HasScope(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAccessTokenScope_EnforcePublicOnlyFrom(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
in AccessTokenScope
|
||||||
|
parent AccessTokenScope
|
||||||
|
out AccessTokenScope
|
||||||
|
}{
|
||||||
|
// public-only parent forces the restriction onto the minted scope
|
||||||
|
{"write:user", "write:user,public-only", "public-only,write:user"},
|
||||||
|
// already public-only stays public-only
|
||||||
|
{"public-only,read:user", "public-only", "public-only,read:user"},
|
||||||
|
// non-public-only parent leaves the scope untouched
|
||||||
|
{"write:user", "write:user", "write:user"},
|
||||||
|
{"all", "all", "all"},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(string(test.parent)+"->"+string(test.in), func(t *testing.T) {
|
||||||
|
got, err := test.in.EnforcePublicOnlyFrom(test.parent)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, test.out, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -195,3 +195,18 @@ func HasTwoFactorOrWebAuthn(ctx context.Context, id int64) (bool, error) {
|
|||||||
}
|
}
|
||||||
return HasWebAuthnRegistrationsByUID(ctx, id)
|
return HasWebAuthnRegistrationsByUID(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DisableTwoFactor removes every two-factor method of the given user atomically,
|
||||||
|
// returning the number of TOTP records and WebAuthn credentials removed.
|
||||||
|
// It is a no-op for a user that has no 2FA enrolled.
|
||||||
|
func DisableTwoFactor(ctx context.Context, uid int64) (totp, webAuthn int64, err error) {
|
||||||
|
err = db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
|
var e error
|
||||||
|
if totp, e = db.GetEngine(ctx).Where("uid = ?", uid).Delete(&TwoFactor{}); e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
webAuthn, e = db.GetEngine(ctx).Where("user_id = ?", uid).Delete(&WebAuthnCredential{})
|
||||||
|
return e
|
||||||
|
})
|
||||||
|
return totp, webAuthn, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
auth_model "gitea.dev/models/auth"
|
auth_model "gitea.dev/models/auth"
|
||||||
"gitea.dev/models/unittest"
|
"gitea.dev/models/unittest"
|
||||||
|
|
||||||
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -45,3 +46,37 @@ func TestTwoFactorValidateAndConsumeTOTP(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, ok)
|
assert.False(t, ok)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDisableTwoFactor(t *testing.T) {
|
||||||
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
ctx := t.Context()
|
||||||
|
|
||||||
|
const uid = 1000 // a uid with no user/2FA fixtures
|
||||||
|
|
||||||
|
// Enroll TOTP and register a WebAuthn credential.
|
||||||
|
tfa := &auth_model.TwoFactor{UID: uid}
|
||||||
|
require.NoError(t, tfa.SetSecret("test-secret"))
|
||||||
|
require.NoError(t, auth_model.NewTwoFactor(ctx, tfa))
|
||||||
|
_, err := auth_model.CreateCredential(ctx, uid, "test-key", &webauthn.Credential{ID: []byte("test-cred-id")})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
has, err := auth_model.HasTwoFactorOrWebAuthn(ctx, uid)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, has)
|
||||||
|
|
||||||
|
// Both records are removed and counted separately.
|
||||||
|
totp, webAuthn, err := auth_model.DisableTwoFactor(ctx, uid)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.EqualValues(t, 1, totp)
|
||||||
|
assert.EqualValues(t, 1, webAuthn)
|
||||||
|
|
||||||
|
has, err = auth_model.HasTwoFactorOrWebAuthn(ctx, uid)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, has)
|
||||||
|
|
||||||
|
// A second call on a user without 2FA is a no-op.
|
||||||
|
totp, webAuthn, err = auth_model.DisableTwoFactor(ctx, uid)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.EqualValues(t, 0, totp)
|
||||||
|
assert.EqualValues(t, 0, webAuthn)
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import (
|
|||||||
"gitea.dev/modules/markup"
|
"gitea.dev/modules/markup"
|
||||||
"gitea.dev/modules/optional"
|
"gitea.dev/modules/optional"
|
||||||
"gitea.dev/modules/references"
|
"gitea.dev/modules/references"
|
||||||
|
"gitea.dev/modules/setting"
|
||||||
"gitea.dev/modules/structs"
|
"gitea.dev/modules/structs"
|
||||||
"gitea.dev/modules/timeutil"
|
"gitea.dev/modules/timeutil"
|
||||||
"gitea.dev/modules/translation"
|
"gitea.dev/modules/translation"
|
||||||
@@ -626,11 +627,18 @@ func UpdateCommentAttachments(ctx context.Context, c *Comment, uuids []string) e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
|
issue, err := GetIssueByID(ctx, c.IssueID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids)
|
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err)
|
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err)
|
||||||
}
|
}
|
||||||
for i := range attachments {
|
for i := range attachments {
|
||||||
|
if err := validateAttachmentForIssue(ctx, issue, attachments[i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
attachments[i].IssueID = c.IssueID
|
attachments[i].IssueID = c.IssueID
|
||||||
attachments[i].CommentID = c.ID
|
attachments[i].CommentID = c.ID
|
||||||
if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil {
|
if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil {
|
||||||
@@ -643,36 +651,18 @@ func UpdateCommentAttachments(ctx context.Context, c *Comment, uuids []string) e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoadAssigneeUserAndTeam if comment.Type is CommentTypeAssignees, then load assignees
|
// LoadAssigneeUserAndTeam if comment.Type is CommentTypeAssignees, then load assignees
|
||||||
func (c *Comment) LoadAssigneeUserAndTeam(ctx context.Context) error {
|
func (c *Comment) LoadAssigneeUserAndTeam(ctx context.Context) (err error) {
|
||||||
var err error
|
|
||||||
|
|
||||||
if c.AssigneeID > 0 && c.Assignee == nil {
|
if c.AssigneeID > 0 && c.Assignee == nil {
|
||||||
c.Assignee, err = user_model.GetUserByID(ctx, c.AssigneeID)
|
_, c.Assignee, err = user_model.GetPossibleUserByID(ctx, c.AssigneeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !user_model.IsErrUserNotExist(err) {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.Assignee = user_model.NewGhostUser()
|
|
||||||
}
|
}
|
||||||
} else if c.AssigneeTeamID > 0 && c.AssigneeTeam == nil {
|
if c.AssigneeTeamID > 0 && c.AssigneeTeam == nil {
|
||||||
if err = c.LoadIssue(ctx); err != nil {
|
_, c.AssigneeTeam, err = organization.GetPossibleTeamByID(ctx, c.AssigneeTeamID)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = c.Issue.LoadRepo(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = c.Issue.Repo.LoadOwner(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Issue.Repo.Owner.IsOrganization() {
|
|
||||||
c.AssigneeTeam, err = organization.GetTeamByID(ctx, c.AssigneeTeamID)
|
|
||||||
if err != nil && !organization.IsErrTeamNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -795,8 +785,7 @@ func (c *Comment) MetaSpecialDoerTr(locale translation.Locale) template.HTML {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Comment) TimelineRequestedReviewTr(locale translation.Locale, createdStr template.HTML) template.HTML {
|
func (c *Comment) TimelineRequestedReviewTr(locale translation.Locale, createdStr template.HTML) template.HTML {
|
||||||
if c.AssigneeID > 0 {
|
if c.Assignee != nil {
|
||||||
// it guarantees LoadAssigneeUserAndTeam has been called, and c.Assignee is Ghost user but not nil if the user doesn't exist
|
|
||||||
if c.RemovedAssignee {
|
if c.RemovedAssignee {
|
||||||
if c.PosterID == c.AssigneeID {
|
if c.PosterID == c.AssigneeID {
|
||||||
return locale.Tr("repo.issues.review.remove_review_request_self", createdStr)
|
return locale.Tr("repo.issues.review.remove_review_request_self", createdStr)
|
||||||
@@ -805,14 +794,20 @@ func (c *Comment) TimelineRequestedReviewTr(locale translation.Locale, createdSt
|
|||||||
}
|
}
|
||||||
return locale.Tr("repo.issues.review.add_review_request", c.Assignee.GetDisplayName(), createdStr)
|
return locale.Tr("repo.issues.review.add_review_request", c.Assignee.GetDisplayName(), createdStr)
|
||||||
}
|
}
|
||||||
teamName := "Ghost Team"
|
|
||||||
if c.AssigneeTeam != nil {
|
if c.AssigneeTeam != nil {
|
||||||
teamName = c.AssigneeTeam.Name
|
|
||||||
}
|
|
||||||
if c.RemovedAssignee {
|
if c.RemovedAssignee {
|
||||||
return locale.Tr("repo.issues.review.remove_review_request", teamName, createdStr)
|
return locale.Tr("repo.issues.review.remove_review_request", c.AssigneeTeam.Name, createdStr)
|
||||||
}
|
}
|
||||||
return locale.Tr("repo.issues.review.add_review_request", teamName, createdStr)
|
return locale.Tr("repo.issues.review.add_review_request", c.AssigneeTeam.Name, createdStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// impossible fallback
|
||||||
|
assigneePrompt := fmt.Sprintf("(AssigneeID=%d, AssigneeTeamID=%d)", c.AssigneeID, c.AssigneeTeam.ID)
|
||||||
|
setting.PanicInDevOrTesting("unknown timeline pull request review event comment: id=%d, %s", c.ID, assigneePrompt)
|
||||||
|
if c.RemovedAssignee {
|
||||||
|
return locale.Tr("repo.issues.review.remove_review_request", assigneePrompt, createdStr)
|
||||||
|
}
|
||||||
|
return locale.Tr("repo.issues.review.add_review_request", assigneePrompt, createdStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateComment creates comment with context
|
// CreateComment creates comment with context
|
||||||
|
|||||||
@@ -45,11 +45,26 @@ func TestCreateComment(t *testing.T) {
|
|||||||
unittest.AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix))
|
unittest.AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadAssigneeUserAndTeam_DeletedTeamBecomesGhostTeam(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 15})
|
||||||
|
comment := &issues_model.Comment{
|
||||||
|
Type: issues_model.CommentTypeAssignees,
|
||||||
|
IssueID: issue.ID,
|
||||||
|
AssigneeTeamID: 999999, // non-existing team ID
|
||||||
|
}
|
||||||
|
assert.NoError(t, comment.LoadAssigneeUserAndTeam(t.Context()))
|
||||||
|
assert.NotNil(t, comment.AssigneeTeam)
|
||||||
|
assert.EqualValues(t, -1, comment.AssigneeTeam.ID)
|
||||||
|
}
|
||||||
|
|
||||||
func Test_UpdateCommentAttachment(t *testing.T) {
|
func Test_UpdateCommentAttachment(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
|
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
|
||||||
|
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
|
||||||
attachment := repo_model.Attachment{
|
attachment := repo_model.Attachment{
|
||||||
|
RepoID: issue.RepoID, // must match the comment's repo, else the cross-repo guard rejects it
|
||||||
Name: "test.txt",
|
Name: "test.txt",
|
||||||
}
|
}
|
||||||
assert.NoError(t, db.Insert(t.Context(), &attachment))
|
assert.NoError(t, db.Insert(t.Context(), &attachment))
|
||||||
|
|||||||
@@ -263,14 +263,46 @@ func AddDeletePRBranchComment(ctx context.Context, doer *user_model.User, repo *
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateAttachmentForIssue rejects a foreign or already-linked attachment before it is linked to
|
||||||
|
// issue: a known UUID could otherwise re-link (and expose) another repo's private attachment. A
|
||||||
|
// legacy attachment predating repo_id-on-upload is adopted into the issue's repo.
|
||||||
|
func validateAttachmentForIssue(ctx context.Context, issue *Issue, attachment *repo_model.Attachment) error {
|
||||||
|
if attachment.RepoID == 0 && attachment.CreatedUnix < repo_model.LegacyAttachmentMissingRepoIDCutoff {
|
||||||
|
attachment.RepoID = issue.RepoID
|
||||||
|
if err := repo_model.UpdateAttachmentByUUID(ctx, attachment, "repo_id"); err != nil {
|
||||||
|
return fmt.Errorf("update attachment repo_id [id: %d]: %w", attachment.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if attachment.RepoID != issue.RepoID {
|
||||||
|
return util.NewPermissionDeniedErrorf("attachment belongs to a different repository")
|
||||||
|
}
|
||||||
|
if attachment.IssueID != 0 && attachment.IssueID != issue.ID {
|
||||||
|
return util.NewPermissionDeniedErrorf("attachment is already linked to another issue")
|
||||||
|
}
|
||||||
|
if attachment.ReleaseID != 0 {
|
||||||
|
return util.NewPermissionDeniedErrorf("attachment is already linked to a release")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateIssueAttachments update attachments by UUIDs for the issue
|
// UpdateIssueAttachments update attachments by UUIDs for the issue
|
||||||
func UpdateIssueAttachments(ctx context.Context, issueID int64, uuids []string) (err error) {
|
func UpdateIssueAttachments(ctx context.Context, issueID int64, uuids []string) (err error) {
|
||||||
|
if len(uuids) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
|
issue, err := GetIssueByID(ctx, issueID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids)
|
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err)
|
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err)
|
||||||
}
|
}
|
||||||
for i := range attachments {
|
for i := range attachments {
|
||||||
|
if err := validateAttachmentForIssue(ctx, issue, attachments[i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
attachments[i].IssueID = issueID
|
attachments[i].IssueID = issueID
|
||||||
if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil {
|
if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil {
|
||||||
return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err)
|
return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err)
|
||||||
|
|||||||
33
models/issues/issue_update_test.go
Normal file
33
models/issues/issue_update_test.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package issues_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
issues_model "gitea.dev/models/issues"
|
||||||
|
repo_model "gitea.dev/models/repo"
|
||||||
|
"gitea.dev/models/unittest"
|
||||||
|
"gitea.dev/modules/util"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUpdateIssueAttachmentsCrossRepo(t *testing.T) {
|
||||||
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
// attachment id 2 belongs to repo 2 / issue 4; issue 1 lives in repo 1
|
||||||
|
issue1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
|
||||||
|
foreign := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 2})
|
||||||
|
require.NotEqual(t, issue1.RepoID, foreign.RepoID)
|
||||||
|
|
||||||
|
// re-linking a foreign repo's attachment by UUID must be rejected
|
||||||
|
err := issues_model.UpdateIssueAttachments(t.Context(), issue1.ID, []string{foreign.UUID})
|
||||||
|
assert.ErrorIs(t, err, util.ErrPermissionDenied)
|
||||||
|
|
||||||
|
// the foreign attachment must be left untouched
|
||||||
|
reloaded := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 2})
|
||||||
|
assert.Equal(t, foreign.IssueID, reloaded.IssueID)
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ package issues
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -27,12 +28,6 @@ type ErrRepoLabelNotExist struct {
|
|||||||
RepoID int64
|
RepoID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsErrRepoLabelNotExist checks if an error is a RepoErrLabelNotExist.
|
|
||||||
func IsErrRepoLabelNotExist(err error) bool {
|
|
||||||
_, ok := err.(ErrRepoLabelNotExist)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err ErrRepoLabelNotExist) Error() string {
|
func (err ErrRepoLabelNotExist) Error() string {
|
||||||
return fmt.Sprintf("label does not exist [label_id: %d, repo_id: %d]", err.LabelID, err.RepoID)
|
return fmt.Sprintf("label does not exist [label_id: %d, repo_id: %d]", err.LabelID, err.RepoID)
|
||||||
}
|
}
|
||||||
@@ -312,6 +307,18 @@ func GetLabelInRepoByName(ctx context.Context, repoID int64, labelName string) (
|
|||||||
return l, nil
|
return l, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetLabelInRepoOrOrgByID returns the label with labelID scoped to the repo, falling back to the
|
||||||
|
// repo's owning organization when ownerIsOrg is set. It returns ErrRepoLabelNotExist /
|
||||||
|
// ErrOrgLabelNotExist when the label is in neither scope, so a foreign-but-existing label ID is
|
||||||
|
// indistinguishable from a nonexistent one (no cross-repo enumeration oracle).
|
||||||
|
func GetLabelInRepoOrOrgByID(ctx context.Context, repoID, ownerID int64, ownerIsOrg bool, labelID int64) (*Label, error) {
|
||||||
|
label, err := GetLabelInRepoByID(ctx, repoID, labelID)
|
||||||
|
if err != nil && errors.Is(err, util.ErrNotExist) && ownerIsOrg {
|
||||||
|
return GetLabelInOrgByID(ctx, ownerID, labelID)
|
||||||
|
}
|
||||||
|
return label, err
|
||||||
|
}
|
||||||
|
|
||||||
// GetLabelInRepoByID returns a label by ID in given repository.
|
// GetLabelInRepoByID returns a label by ID in given repository.
|
||||||
func GetLabelInRepoByID(ctx context.Context, repoID, labelID int64) (*Label, error) {
|
func GetLabelInRepoByID(ctx context.Context, repoID, labelID int64) (*Label, error) {
|
||||||
if labelID <= 0 || repoID <= 0 {
|
if labelID <= 0 || repoID <= 0 {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"gitea.dev/models/unittest"
|
"gitea.dev/models/unittest"
|
||||||
user_model "gitea.dev/models/user"
|
user_model "gitea.dev/models/user"
|
||||||
"gitea.dev/modules/timeutil"
|
"gitea.dev/modules/timeutil"
|
||||||
|
"gitea.dev/modules/util"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@@ -94,10 +95,10 @@ func TestGetLabelInRepoByName(t *testing.T) {
|
|||||||
assert.Equal(t, "label1", label.Name)
|
assert.Equal(t, "label1", label.Name)
|
||||||
|
|
||||||
_, err = issues_model.GetLabelInRepoByName(t.Context(), 1, "")
|
_, err = issues_model.GetLabelInRepoByName(t.Context(), 1, "")
|
||||||
assert.True(t, issues_model.IsErrRepoLabelNotExist(err))
|
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||||
|
|
||||||
_, err = issues_model.GetLabelInRepoByName(t.Context(), unittest.NonexistentID, "nonexistent")
|
_, err = issues_model.GetLabelInRepoByName(t.Context(), unittest.NonexistentID, "nonexistent")
|
||||||
assert.True(t, issues_model.IsErrRepoLabelNotExist(err))
|
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetLabelInRepoByNames(t *testing.T) {
|
func TestGetLabelInRepoByNames(t *testing.T) {
|
||||||
@@ -131,10 +132,10 @@ func TestGetLabelInRepoByID(t *testing.T) {
|
|||||||
assert.EqualValues(t, 1, label.ID)
|
assert.EqualValues(t, 1, label.ID)
|
||||||
|
|
||||||
_, err = issues_model.GetLabelInRepoByID(t.Context(), 1, -1)
|
_, err = issues_model.GetLabelInRepoByID(t.Context(), 1, -1)
|
||||||
assert.True(t, issues_model.IsErrRepoLabelNotExist(err))
|
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||||
|
|
||||||
_, err = issues_model.GetLabelInRepoByID(t.Context(), unittest.NonexistentID, unittest.NonexistentID)
|
_, err = issues_model.GetLabelInRepoByID(t.Context(), unittest.NonexistentID, unittest.NonexistentID)
|
||||||
assert.True(t, issues_model.IsErrRepoLabelNotExist(err))
|
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetLabelsInRepoByIDs(t *testing.T) {
|
func TestGetLabelsInRepoByIDs(t *testing.T) {
|
||||||
|
|||||||
@@ -324,6 +324,59 @@ func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organizatio
|
|||||||
return slices.Contains(pb.ApprovalsWhitelistTeamIDs, team.ID), nil
|
return slices.Contains(pb.ApprovalsWhitelistTeamIDs, team.ID), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RecalculateReviewsOfficial re-evaluates the "official" flag of the latest approve
|
||||||
|
// and reject reviews of an issue against its pull request's current base branch.
|
||||||
|
// It must be called whenever the target branch changes, otherwise an approval that
|
||||||
|
// was official on the previous (possibly unprotected) branch would keep satisfying
|
||||||
|
// the new branch's protection rules.
|
||||||
|
func RecalculateReviewsOfficial(ctx context.Context, issue *Issue) error {
|
||||||
|
if err := issue.LoadPullRequest(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clearing and restoring the official flags must happen atomically, otherwise a
|
||||||
|
// failure in between would leave the reviews without any official flag set.
|
||||||
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
|
// Only the latest approve/reject review of each reviewer counts as official, so
|
||||||
|
// clear the flag on all of them first and restore it only where it still applies.
|
||||||
|
if _, err := db.GetEngine(ctx).
|
||||||
|
Where("issue_id = ?", issue.ID).
|
||||||
|
In("type", ReviewTypeApprove, ReviewTypeReject).
|
||||||
|
Cols("official").
|
||||||
|
Update(&Review{Official: false}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reviews, err := FindLatestReviews(ctx, FindReviewOptions{
|
||||||
|
Types: []ReviewType{ReviewTypeApprove, ReviewTypeReject},
|
||||||
|
IssueID: issue.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, review := range reviews {
|
||||||
|
if err := review.LoadReviewer(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if review.Reviewer == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
official, err := IsOfficialReviewer(ctx, issue, review.Reviewer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if official {
|
||||||
|
if _, err := db.GetEngine(ctx).ID(review.ID).Cols("official").Update(&Review{Official: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// CreateReview creates a new review based on opts
|
// CreateReview creates a new review based on opts
|
||||||
func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error) {
|
func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error) {
|
||||||
return db.WithTx2(ctx, func(ctx context.Context) (*Review, error) {
|
return db.WithTx2(ctx, func(ctx context.Context) (*Review, error) {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ package issues_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dev/models/db"
|
||||||
|
git_model "gitea.dev/models/git"
|
||||||
issues_model "gitea.dev/models/issues"
|
issues_model "gitea.dev/models/issues"
|
||||||
repo_model "gitea.dev/models/repo"
|
repo_model "gitea.dev/models/repo"
|
||||||
"gitea.dev/models/unittest"
|
"gitea.dev/models/unittest"
|
||||||
@@ -386,3 +388,45 @@ func TestAddReviewRequest(t *testing.T) {
|
|||||||
assert.NotNil(t, comment.CommentMetaData)
|
assert.NotNil(t, comment.CommentMetaData)
|
||||||
assert.Equal(t, issues_model.SpecialDoerNameCodeOwners, comment.CommentMetaData.SpecialDoerName)
|
assert.Equal(t, issues_model.SpecialDoerNameCodeOwners, comment.CommentMetaData.SpecialDoerName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRecalculateReviewsOfficial(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
// PR #2 targets repo1's "master" branch. Simulate an approval that became
|
||||||
|
// official while the PR targeted an unprotected branch.
|
||||||
|
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
|
||||||
|
reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||||
|
review, err := issues_model.CreateReview(t.Context(), issues_model.CreateReviewOptions{
|
||||||
|
Type: issues_model.ReviewTypeApprove,
|
||||||
|
Issue: issue,
|
||||||
|
Reviewer: reviewer,
|
||||||
|
Official: true,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Protect the (now current) target branch with an approvals whitelist that
|
||||||
|
// does not include the reviewer, mirroring a retarget onto a protected branch.
|
||||||
|
rule := &git_model.ProtectedBranch{
|
||||||
|
RepoID: issue.RepoID,
|
||||||
|
RuleName: "master",
|
||||||
|
EnableApprovalsWhitelist: true,
|
||||||
|
ApprovalsWhitelistUserIDs: []int64{2},
|
||||||
|
RequiredApprovals: 1,
|
||||||
|
}
|
||||||
|
assert.NoError(t, db.Insert(t.Context(), rule))
|
||||||
|
|
||||||
|
// Re-evaluating must strip the stale official flag, otherwise the approval
|
||||||
|
// would still satisfy the protected branch's required approvals.
|
||||||
|
assert.NoError(t, issues_model.RecalculateReviewsOfficial(t.Context(), issue))
|
||||||
|
review = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: review.ID})
|
||||||
|
assert.False(t, review.Official)
|
||||||
|
|
||||||
|
// Once the reviewer is whitelisted, re-evaluating restores the official flag.
|
||||||
|
rule.ApprovalsWhitelistUserIDs = []int64{2, reviewer.ID}
|
||||||
|
_, err = db.GetEngine(t.Context()).ID(rule.ID).Cols("approvals_whitelist_user_i_ds").Update(rule)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.NoError(t, issues_model.RecalculateReviewsOfficial(t.Context(), issue))
|
||||||
|
review = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: review.ID})
|
||||||
|
assert.True(t, review.Official)
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package organization
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -92,6 +93,15 @@ func (t *Team) IsPublic() bool { return t.Visibility.IsPublic() }
|
|||||||
func (t *Team) IsLimited() bool { return t.Visibility.IsLimited() }
|
func (t *Team) IsLimited() bool { return t.Visibility.IsLimited() }
|
||||||
func (t *Team) IsPrivate() bool { return t.Visibility.IsPrivate() }
|
func (t *Team) IsPrivate() bool { return t.Visibility.IsPrivate() }
|
||||||
|
|
||||||
|
const (
|
||||||
|
ghostTeamID = -1
|
||||||
|
ghostTeamName = "(deleted team)"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newGhostTeam() *Team {
|
||||||
|
return &Team{ID: ghostTeamID, Name: ghostTeamName, LowerName: ghostTeamName}
|
||||||
|
}
|
||||||
|
|
||||||
// CanNonMemberReadMeta reports whether a non-member, non-owner doer may read
|
// CanNonMemberReadMeta reports whether a non-member, non-owner doer may read
|
||||||
// the team's metadata, based on the team's visibility tier and the parent org's
|
// the team's metadata, based on the team's visibility tier and the parent org's
|
||||||
// visibility. Privileged callers (site admins, org owners, team members) are
|
// visibility. Privileged callers (site admins, org owners, team members) are
|
||||||
@@ -270,6 +280,17 @@ func GetTeamByID(ctx context.Context, teamID int64) (*Team, error) {
|
|||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetPossibleTeamByID(ctx context.Context, teamID int64) (int64, *Team, error) {
|
||||||
|
t, err := GetTeamByID(ctx, teamID)
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
t = newGhostTeam()
|
||||||
|
return t.ID, t, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
return t.ID, t, nil
|
||||||
|
}
|
||||||
|
|
||||||
// IncrTeamRepoNum increases the number of repos for the given team by 1
|
// IncrTeamRepoNum increases the number of repos for the given team by 1
|
||||||
func IncrTeamRepoNum(ctx context.Context, teamID int64) error {
|
func IncrTeamRepoNum(ctx context.Context, teamID int64) error {
|
||||||
_, err := db.GetEngine(ctx).Incr("num_repos").ID(teamID).Update(new(Team))
|
_, err := db.GetEngine(ctx).Incr("num_repos").ID(teamID).Update(new(Team))
|
||||||
|
|||||||
@@ -31,18 +31,28 @@ func GetOrgRepositoryIDs(ctx context.Context, orgID int64) (repoIDs []int64, _ e
|
|||||||
type SearchTeamRepoOptions struct {
|
type SearchTeamRepoOptions struct {
|
||||||
db.ListOptions
|
db.ListOptions
|
||||||
TeamID int64
|
TeamID int64
|
||||||
|
// PublicOnly restricts the result (and count) to non-private repositories.
|
||||||
|
PublicOnly bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opts *SearchTeamRepoOptions) toCond() builder.Cond {
|
||||||
|
cond := builder.NewCond()
|
||||||
|
if opts.TeamID > 0 {
|
||||||
|
cond = cond.And(builder.In("id",
|
||||||
|
builder.Select("repo_id").
|
||||||
|
From("team_repo").
|
||||||
|
Where(builder.Eq{"team_id": opts.TeamID}),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
if opts.PublicOnly {
|
||||||
|
cond = cond.And(builder.Eq{"is_private": false})
|
||||||
|
}
|
||||||
|
return cond
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTeamRepositories returns paginated repositories in team of organization.
|
// GetTeamRepositories returns paginated repositories in team of organization.
|
||||||
func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (RepositoryList, error) {
|
func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (RepositoryList, error) {
|
||||||
sess := db.GetEngine(ctx)
|
sess := db.GetEngine(ctx).Where(opts.toCond())
|
||||||
if opts.TeamID > 0 {
|
|
||||||
sess = sess.In("id",
|
|
||||||
builder.Select("repo_id").
|
|
||||||
From("team_repo").
|
|
||||||
Where(builder.Eq{"team_id": opts.TeamID}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if opts.PageSize > 0 {
|
if opts.PageSize > 0 {
|
||||||
sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
|
sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
|
||||||
}
|
}
|
||||||
@@ -51,6 +61,11 @@ func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (Repo
|
|||||||
Find(&repos)
|
Find(&repos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CountTeamRepositories returns the number of repositories in team of organization matching opts.
|
||||||
|
func CountTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (int64, error) {
|
||||||
|
return db.GetEngine(ctx).Where(opts.toCond()).Count(new(Repository))
|
||||||
|
}
|
||||||
|
|
||||||
// AccessibleReposEnvironment operations involving the repositories that are
|
// AccessibleReposEnvironment operations involving the repositories that are
|
||||||
// accessible to a particular user
|
// accessible to a particular user
|
||||||
type AccessibleReposEnvironment interface {
|
type AccessibleReposEnvironment interface {
|
||||||
|
|||||||
@@ -310,11 +310,17 @@ func userOrgTeamRepoBuilder(userID int64) *builder.Builder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// userOrgTeamUnitRepoBuilder returns repo ids where user's teams can access the special unit.
|
// userOrgTeamUnitRepoBuilder returns repo ids where user's teams can access the special unit.
|
||||||
|
// A team grants the unit either through an explicit team_unit row (access_mode > none) or by being an
|
||||||
|
// admin/owner team (team.authorize >= admin), which grants every unit regardless of team_unit rows —
|
||||||
|
// mirroring the HasAdminAccess() short-circuit in access.GetIndividualUserRepoPermission.
|
||||||
func userOrgTeamUnitRepoBuilder(userID int64, unitType unit.Type) *builder.Builder {
|
func userOrgTeamUnitRepoBuilder(userID int64, unitType unit.Type) *builder.Builder {
|
||||||
return userOrgTeamRepoBuilder(userID).
|
return userOrgTeamRepoBuilder(userID).
|
||||||
Join("INNER", "team_unit", "`team_unit`.team_id = `team_repo`.team_id").
|
Join("INNER", "team", "`team`.id = `team_repo`.team_id").
|
||||||
Where(builder.Eq{"`team_unit`.`type`": unitType}).
|
Join("LEFT", "team_unit", builder.Expr("`team_unit`.team_id = `team_repo`.team_id AND `team_unit`.`type` = ?", unitType)).
|
||||||
And(builder.Gt{"`team_unit`.`access_mode`": int(perm.AccessModeNone)})
|
Where(builder.Or(
|
||||||
|
builder.Gte{"`team`.authorize": int(perm.AccessModeAdmin)},
|
||||||
|
builder.Gt{"`team_unit`.`access_mode`": int(perm.AccessModeNone)},
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// userOrgTeamUnitRepoCond returns a condition to select repo ids where user's teams can access the special unit.
|
// userOrgTeamUnitRepoCond returns a condition to select repo ids where user's teams can access the special unit.
|
||||||
@@ -326,7 +332,7 @@ func userOrgTeamUnitRepoCond(idStr string, userID int64, unitType unit.Type) bui
|
|||||||
func UserOrgUnitRepoCond(idStr string, userID, orgID int64, unitType unit.Type) builder.Cond {
|
func UserOrgUnitRepoCond(idStr string, userID, orgID int64, unitType unit.Type) builder.Cond {
|
||||||
return builder.In(idStr,
|
return builder.In(idStr,
|
||||||
userOrgTeamUnitRepoBuilder(userID, unitType).
|
userOrgTeamUnitRepoBuilder(userID, unitType).
|
||||||
And(builder.Eq{"`team_unit`.org_id": orgID}),
|
And(builder.Eq{"`team`.org_id": orgID}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -755,6 +761,40 @@ func FindUserCodeAccessibleOwnerRepoIDs(ctx context.Context, ownerID int64, user
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PublicRepoUnderPublicOwnerCond restricts to public repos whose owner is publicly visible: the
|
||||||
|
// "genuinely public" set a public-only token or an anonymous caller may see (a public repo under a
|
||||||
|
// limited/private owner is not publicly reachable and must be excluded).
|
||||||
|
func PublicRepoUnderPublicOwnerCond() builder.Cond {
|
||||||
|
return builder.And(
|
||||||
|
builder.Eq{"`repository`.is_private": false},
|
||||||
|
builder.In("`repository`.owner_id", builder.Select("id").From("`user`").Where(builder.Eq{"visibility": structs.VisibleTypePublic})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserActionsAccessibleOwnerRepoCond selects the repos owned by ownerID whose Actions `user` may read.
|
||||||
|
// It is used to list an org/user's Actions runs and jobs (see the callers in routers/api/v1/shared).
|
||||||
|
// - owner_id = ownerID: only that owner's repos.
|
||||||
|
// - AccessibleRepositoryCondition(user, TypeActions): only repos whose Actions the user can read
|
||||||
|
// (admin/owner teams are handled inside it; a site admin is not, callers must skip the filter for one).
|
||||||
|
// - publicOnly (a public-only token): additionally limit to public repos under a public owner.
|
||||||
|
func UserActionsAccessibleOwnerRepoCond(ownerID int64, user *user_model.User, publicOnly bool) builder.Cond {
|
||||||
|
cond := builder.NewCond().And(
|
||||||
|
builder.Eq{"`repository`.owner_id": ownerID},
|
||||||
|
AccessibleRepositoryCondition(user, unit.TypeActions),
|
||||||
|
)
|
||||||
|
if publicOnly {
|
||||||
|
cond = cond.And(PublicRepoUnderPublicOwnerCond())
|
||||||
|
}
|
||||||
|
return cond
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindUserActionsAccessibleOwnerRepoIDsSubQuery returns a subquery selecting the repository IDs the user
|
||||||
|
// can see for the given owner. Callers embed it in an `IN (...)` condition so that a large owner does not
|
||||||
|
// materialize every repo ID into the SQL statement, which could exceed database parameter limits.
|
||||||
|
func FindUserActionsAccessibleOwnerRepoIDsSubQuery(ownerID int64, user *user_model.User, publicOnly bool) *builder.Builder {
|
||||||
|
return builder.Select("id").From("repository").Where(UserActionsAccessibleOwnerRepoCond(ownerID, user, publicOnly))
|
||||||
|
}
|
||||||
|
|
||||||
// GetUserRepositories returns a list of repositories of given user.
|
// GetUserRepositories returns a list of repositories of given user.
|
||||||
func GetUserRepositories(ctx context.Context, opts SearchRepoOptions) (RepositoryList, int64, error) {
|
func GetUserRepositories(ctx context.Context, opts SearchRepoOptions) (RepositoryList, int64, error) {
|
||||||
if len(opts.OrderBy) == 0 {
|
if len(opts.OrderBy) == 0 {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"gitea.dev/models/db"
|
"gitea.dev/models/db"
|
||||||
repo_model "gitea.dev/models/repo"
|
repo_model "gitea.dev/models/repo"
|
||||||
|
"gitea.dev/models/unit"
|
||||||
"gitea.dev/models/unittest"
|
"gitea.dev/models/unittest"
|
||||||
user_model "gitea.dev/models/user"
|
user_model "gitea.dev/models/user"
|
||||||
"gitea.dev/modules/optional"
|
"gitea.dev/modules/optional"
|
||||||
@@ -466,3 +467,50 @@ func TestSearchRepositoryByTopicName(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFindUserActionsAccessibleOwnerRepoIDs(t *testing.T) {
|
||||||
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
// user2 is on org3's owner team, so it can access org3's private repo3 (which has the actions unit)
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
|
||||||
|
// org3 is a public org owning repo3 (private) and repo32 (public), both with the actions unit
|
||||||
|
const orgID = 3
|
||||||
|
|
||||||
|
all, err := repo_model.SearchRepositoryIDsByCondition(t.Context(), repo_model.UserActionsAccessibleOwnerRepoCond(orgID, user, false))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, all, int64(3), "without public-only the private repo's actions are listed")
|
||||||
|
|
||||||
|
publicOnly, err := repo_model.SearchRepositoryIDsByCondition(t.Context(), repo_model.UserActionsAccessibleOwnerRepoCond(orgID, user, true))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotContains(t, publicOnly, int64(3), "a public-only token must not list a private repo's actions")
|
||||||
|
assert.Contains(t, publicOnly, int64(32), "a public repo under a public owner stays listed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUserOrgUnitRepoCondTeamAuthorize pins the team.authorize behavior of userOrgTeamUnitRepoBuilder
|
||||||
|
// (exercised through UserOrgUnitRepoCond): an admin/owner team grants every unit even without an explicit
|
||||||
|
// team_unit row, while a non-admin team only grants a unit it has an explicit row for. This guards both
|
||||||
|
// directions — hiding repos from admin-team members, and over-broadening a plain team's access.
|
||||||
|
func TestUserOrgUnitRepoCondTeamAuthorize(t *testing.T) {
|
||||||
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
accessibleRepoIDs := func(userID, orgID int64, unitType unit.Type) []int64 {
|
||||||
|
ids, err := repo_model.SearchRepositoryIDsByCondition(t.Context(),
|
||||||
|
repo_model.UserOrgUnitRepoCond("`repository`.id", userID, orgID, unitType))
|
||||||
|
require.NoError(t, err)
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case A: user18 is only on org17's owner team (team5, authorize=owner), linked to the private repo24
|
||||||
|
// but with no Actions team_unit row. The owner authorize must still grant it, mirroring the runtime
|
||||||
|
// HasAdminAccess() short-circuit in access.GetIndividualUserRepoPermission.
|
||||||
|
assert.Contains(t, accessibleRepoIDs(18, 17, unit.TypeActions), int64(24),
|
||||||
|
"an owner team grants a unit it has no explicit team_unit row for")
|
||||||
|
|
||||||
|
// Cases B and C share one subject so the team_unit row is the only difference: user4 is only on org3's
|
||||||
|
// write team (team2, authorize=write, non-admin), linked to the private repo3. team2 has an explicit
|
||||||
|
// Projects row but none for Actions.
|
||||||
|
assert.Contains(t, accessibleRepoIDs(4, 3, unit.TypeProjects), int64(3),
|
||||||
|
"a non-admin team grants a unit it has an explicit team_unit row for")
|
||||||
|
assert.NotContains(t, accessibleRepoIDs(4, 3, unit.TypeActions), int64(3),
|
||||||
|
"a non-admin team must NOT grant a unit it has no team_unit row for")
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ type StarredReposOptions struct {
|
|||||||
StarrerID int64
|
StarrerID int64
|
||||||
RepoOwnerID int64
|
RepoOwnerID int64
|
||||||
IncludePrivate bool
|
IncludePrivate bool
|
||||||
|
// Actor is the user the private repositories are gated on: a private repo is only
|
||||||
|
// returned when Actor still has access to it, even if it was starred while access was granted.
|
||||||
|
Actor *user_model.User
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts *StarredReposOptions) ApplyPublicOnly(publicOnly bool) {
|
func (opts *StarredReposOptions) ApplyPublicOnly(publicOnly bool) {
|
||||||
@@ -39,10 +42,12 @@ func (opts *StarredReposOptions) ToConds() builder.Cond {
|
|||||||
"repository.owner_id": opts.RepoOwnerID,
|
"repository.owner_id": opts.RepoOwnerID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if !opts.IncludePrivate {
|
if opts.IncludePrivate {
|
||||||
cond = cond.And(builder.Eq{
|
// only include private repos the actor can still access, so metadata does not leak after access revocation
|
||||||
"repository.is_private": false,
|
cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid))
|
||||||
})
|
} else {
|
||||||
|
// a public repo under a limited/private owner is not publicly reachable, so exclude it too
|
||||||
|
cond = cond.And(PublicRepoUnderPublicOwnerCond())
|
||||||
}
|
}
|
||||||
return cond
|
return cond
|
||||||
}
|
}
|
||||||
@@ -66,6 +71,9 @@ type WatchedReposOptions struct {
|
|||||||
WatcherID int64
|
WatcherID int64
|
||||||
RepoOwnerID int64
|
RepoOwnerID int64
|
||||||
IncludePrivate bool
|
IncludePrivate bool
|
||||||
|
// Actor is the user the private repositories are gated on: a private repo is only
|
||||||
|
// returned when Actor still has access to it, even if it was watched while access was granted.
|
||||||
|
Actor *user_model.User
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts *WatchedReposOptions) ApplyPublicOnly(publicOnly bool) {
|
func (opts *WatchedReposOptions) ApplyPublicOnly(publicOnly bool) {
|
||||||
@@ -83,10 +91,12 @@ func (opts *WatchedReposOptions) ToConds() builder.Cond {
|
|||||||
"repository.owner_id": opts.RepoOwnerID,
|
"repository.owner_id": opts.RepoOwnerID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if !opts.IncludePrivate {
|
if opts.IncludePrivate {
|
||||||
cond = cond.And(builder.Eq{
|
// only include private repos the actor can still access, so metadata does not leak after access revocation
|
||||||
"repository.is_private": false,
|
cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid))
|
||||||
})
|
} else {
|
||||||
|
// a public repo under a limited/private owner is not publicly reachable, so exclude it too
|
||||||
|
cond = cond.And(PublicRepoUnderPublicOwnerCond())
|
||||||
}
|
}
|
||||||
return cond.And(builder.Neq{
|
return cond.And(builder.Neq{
|
||||||
"watch.mode": WatchModeDont,
|
"watch.mode": WatchModeDont,
|
||||||
|
|||||||
@@ -84,3 +84,40 @@ func testUserRepoGetIssuePostersWithSearch(t *testing.T) {
|
|||||||
require.Len(t, users, 1)
|
require.Len(t, users, 1)
|
||||||
assert.Equal(t, "user2", users[0].Name)
|
assert.Equal(t, "user2", users[0].Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStarredWatchedReposExcludeNonPublicOwners(t *testing.T) {
|
||||||
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
const viewerID = 2
|
||||||
|
// repo1: public repo under a public owner; repo38: public repo under a limited org (not publicly reachable)
|
||||||
|
const publicOwnerRepo, limitedOwnerRepo = 1, 38
|
||||||
|
|
||||||
|
require.NoError(t, db.Insert(t.Context(), &repo_model.Star{UID: viewerID, RepoID: publicOwnerRepo}))
|
||||||
|
require.NoError(t, db.Insert(t.Context(), &repo_model.Star{UID: viewerID, RepoID: limitedOwnerRepo}))
|
||||||
|
require.NoError(t, db.Insert(t.Context(), &repo_model.Watch{UserID: viewerID, RepoID: publicOwnerRepo, Mode: repo_model.WatchModeNormal}))
|
||||||
|
require.NoError(t, db.Insert(t.Context(), &repo_model.Watch{UserID: viewerID, RepoID: limitedOwnerRepo, Mode: repo_model.WatchModeNormal}))
|
||||||
|
|
||||||
|
listOpts := db.ListOptions{Page: 1, PageSize: 50}
|
||||||
|
|
||||||
|
starred, err := repo_model.GetStarredRepos(t.Context(), &repo_model.StarredReposOptions{
|
||||||
|
ListOptions: listOpts, StarrerID: viewerID, IncludePrivate: false,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotContains(t, repoIDs(starred), int64(limitedOwnerRepo), "a public repo under a limited owner must be hidden from a public star listing")
|
||||||
|
assert.Contains(t, repoIDs(starred), int64(publicOwnerRepo), "a public repo under a public owner stays visible")
|
||||||
|
|
||||||
|
watched, _, err := repo_model.GetWatchedRepos(t.Context(), &repo_model.WatchedReposOptions{
|
||||||
|
ListOptions: listOpts, WatcherID: viewerID, IncludePrivate: false,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotContains(t, repoIDs(watched), int64(limitedOwnerRepo), "a public repo under a limited owner must be hidden from a public watch listing")
|
||||||
|
assert.Contains(t, repoIDs(watched), int64(publicOwnerRepo), "a public repo under a public owner stays visible")
|
||||||
|
}
|
||||||
|
|
||||||
|
func repoIDs(repos []*repo_model.Repository) []int64 {
|
||||||
|
ids := make([]int64, len(repos))
|
||||||
|
for i, r := range repos {
|
||||||
|
ids[i] = r.ID
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
package actions
|
package actions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"gitea.dev/modules/actions/jobparser"
|
"gitea.dev/modules/actions/jobparser"
|
||||||
"gitea.dev/modules/git"
|
"gitea.dev/modules/git"
|
||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
@@ -13,8 +15,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ListScopedWorkflows lists scoped workflow files (under SCOPED_WORKFLOW_DIRS) at the given commit.
|
// ListScopedWorkflows lists scoped workflow files (under SCOPED_WORKFLOW_DIRS) at the given commit.
|
||||||
func ListScopedWorkflows(commit *git.Commit) (string, git.Entries, error) {
|
func ListScopedWorkflows(ctx context.Context, gitRepo *git.Repository, commit *git.Commit) (string, git.Entries, error) {
|
||||||
return listWorkflowsInDirs(commit, setting.Actions.ScopedWorkflowDirs)
|
return listWorkflowsInDirs(ctx, gitRepo, commit, setting.Actions.ScopedWorkflowDirs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParsedScopedWorkflow is one scoped workflow's source-side parse result
|
// ParsedScopedWorkflow is one scoped workflow's source-side parse result
|
||||||
@@ -26,15 +28,15 @@ type ParsedScopedWorkflow struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ParseScopedWorkflows lists and parses the scoped workflow files at sourceCommit (under SCOPED_WORKFLOW_DIRS).
|
// ParseScopedWorkflows lists and parses the scoped workflow files at sourceCommit (under SCOPED_WORKFLOW_DIRS).
|
||||||
func ParseScopedWorkflows(sourceCommit *git.Commit) ([]*ParsedScopedWorkflow, error) {
|
func ParseScopedWorkflows(ctx context.Context, gitRepo *git.Repository, sourceCommit *git.Commit) ([]*ParsedScopedWorkflow, error) {
|
||||||
_, entries, err := ListScopedWorkflows(sourceCommit)
|
_, entries, err := ListScopedWorkflows(ctx, gitRepo, sourceCommit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
parsed := make([]*ParsedScopedWorkflow, 0, len(entries))
|
parsed := make([]*ParsedScopedWorkflow, 0, len(entries))
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
content, err := GetContentFromEntry(entry)
|
content, err := GetContentFromEntry(gitRepo, entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -55,29 +57,34 @@ func ParseScopedWorkflows(sourceCommit *git.Commit) ([]*ParsedScopedWorkflow, er
|
|||||||
return parsed, nil
|
return parsed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MatchScopedWorkflows evaluates already-parsed scoped workflows against one consuming event, returning those whose `on:` matches.
|
// MatchScopedWorkflows evaluates already-parsed scoped workflows against one consuming event.
|
||||||
|
// It returns the workflows whose `on:` matches, and those that matched the event but were excluded by a branch/paths filter (filtered).
|
||||||
func MatchScopedWorkflows(
|
func MatchScopedWorkflows(
|
||||||
parsed []*ParsedScopedWorkflow,
|
parsed []*ParsedScopedWorkflow,
|
||||||
consumerGitRepo *git.Repository,
|
consumerGitRepo *git.Repository,
|
||||||
consumerCommit *git.Commit,
|
consumerCommit *git.Commit,
|
||||||
triggedEvent webhook_module.HookEventType,
|
triggedEvent webhook_module.HookEventType,
|
||||||
payload api.Payloader,
|
payload api.Payloader,
|
||||||
) []*DetectedWorkflow {
|
) (matched, filtered []*DetectedWorkflow) {
|
||||||
workflows := make([]*DetectedWorkflow, 0, len(parsed))
|
|
||||||
for _, p := range parsed {
|
for _, p := range parsed {
|
||||||
for _, evt := range p.Events {
|
for _, evt := range p.Events {
|
||||||
if evt.IsSchedule() {
|
if evt.IsSchedule() {
|
||||||
// schedule is a non-target for scoped workflows
|
// schedule is a non-target for scoped workflows
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if detectMatched(consumerGitRepo, consumerCommit, triggedEvent, payload, evt) {
|
dwf := &DetectedWorkflow{
|
||||||
workflows = append(workflows, &DetectedWorkflow{
|
|
||||||
EntryName: p.EntryName,
|
EntryName: p.EntryName,
|
||||||
TriggerEvent: evt,
|
TriggerEvent: evt,
|
||||||
Content: p.Content,
|
Content: p.Content,
|
||||||
})
|
}
|
||||||
|
switch detectWorkflowMatch(consumerGitRepo, consumerCommit, triggedEvent, payload, evt) {
|
||||||
|
case detectMatched:
|
||||||
|
matched = append(matched, dwf)
|
||||||
|
case detectFilteredOut:
|
||||||
|
filtered = append(filtered, dwf)
|
||||||
|
case detectNotApplicable:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return workflows
|
return matched, filtered
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package actions
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -30,6 +31,14 @@ type DetectedWorkflow struct {
|
|||||||
Content []byte
|
Content []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type detectResult int
|
||||||
|
|
||||||
|
const (
|
||||||
|
detectMatched detectResult = iota // event matched; run normally
|
||||||
|
detectNotApplicable // event/type doesn't apply; create nothing
|
||||||
|
detectFilteredOut // matched but excluded by a branch/paths filter; posts a skipped commit status when the context is a required check
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
model.OnDecodeNodeError = func(node yaml.Node, out any, err error) {
|
model.OnDecodeNodeError = func(node yaml.Node, out any, err error) {
|
||||||
// Log the error instead of panic or fatal.
|
// Log the error instead of panic or fatal.
|
||||||
@@ -61,16 +70,16 @@ func isWorkflowInDirs(path string, dirs []string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListWorkflows(commit *git.Commit) (string, git.Entries, error) {
|
func ListWorkflows(ctx context.Context, gitRepo *git.Repository, commit *git.Commit) (string, git.Entries, error) {
|
||||||
return listWorkflowsInDirs(commit, setting.Actions.WorkflowDirs)
|
return listWorkflowsInDirs(ctx, gitRepo, commit, setting.Actions.WorkflowDirs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func listWorkflowsInDirs(commit *git.Commit, dirs []string) (string, git.Entries, error) {
|
func listWorkflowsInDirs(ctx context.Context, gitRepo *git.Repository, commit *git.Commit, dirs []string) (string, git.Entries, error) {
|
||||||
var tree *git.Tree
|
var tree *git.Tree
|
||||||
var err error
|
var err error
|
||||||
var workflowDir string
|
var workflowDir string
|
||||||
for _, workflowDir = range dirs {
|
for _, workflowDir = range dirs {
|
||||||
tree, err = commit.SubTree(workflowDir)
|
tree, err = commit.SubTree(ctx, gitRepo, workflowDir)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -82,7 +91,7 @@ func listWorkflowsInDirs(commit *git.Commit, dirs []string) (string, git.Entries
|
|||||||
return "", nil, nil
|
return "", nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, err := tree.ListEntriesRecursiveFast()
|
entries, err := tree.ListEntriesRecursiveFast(ctx, gitRepo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
@@ -96,8 +105,8 @@ func listWorkflowsInDirs(commit *git.Commit, dirs []string) (string, git.Entries
|
|||||||
return workflowDir, ret, nil
|
return workflowDir, ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) {
|
func GetContentFromEntry(gitRepo *git.Repository, entry *git.TreeEntry) ([]byte, error) {
|
||||||
f, err := entry.Blob().DataAsync()
|
f, err := entry.Blob(gitRepo).DataAsync()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -167,23 +176,22 @@ func ShouldEventCreateCommitStatus(event string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DetectWorkflows(
|
func DetectWorkflows(
|
||||||
|
ctx context.Context,
|
||||||
gitRepo *git.Repository,
|
gitRepo *git.Repository,
|
||||||
commit *git.Commit,
|
commit *git.Commit,
|
||||||
triggedEvent webhook_module.HookEventType,
|
triggedEvent webhook_module.HookEventType,
|
||||||
payload api.Payloader,
|
payload api.Payloader,
|
||||||
detectSchedule bool,
|
detectSchedule bool,
|
||||||
) ([]*DetectedWorkflow, []*DetectedWorkflow, error) {
|
) (workflows, schedules, filtered []*DetectedWorkflow, err error) {
|
||||||
_, entries, err := ListWorkflows(commit)
|
_, entries, err := ListWorkflows(ctx, gitRepo, commit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
workflows := make([]*DetectedWorkflow, 0, len(entries))
|
|
||||||
schedules := make([]*DetectedWorkflow, 0, len(entries))
|
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
content, err := GetContentFromEntry(entry)
|
content, err := GetContentFromEntry(gitRepo, entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// one workflow may have multiple events
|
// one workflow may have multiple events
|
||||||
@@ -203,29 +211,35 @@ func DetectWorkflows(
|
|||||||
}
|
}
|
||||||
schedules = append(schedules, dwf)
|
schedules = append(schedules, dwf)
|
||||||
}
|
}
|
||||||
} else if detectMatched(gitRepo, commit, triggedEvent, payload, evt) {
|
} else {
|
||||||
dwf := &DetectedWorkflow{
|
dwf := &DetectedWorkflow{
|
||||||
EntryName: entry.Name(),
|
EntryName: entry.Name(),
|
||||||
TriggerEvent: evt,
|
TriggerEvent: evt,
|
||||||
Content: content,
|
Content: content,
|
||||||
}
|
}
|
||||||
|
switch detectWorkflowMatch(gitRepo, commit, triggedEvent, payload, evt) {
|
||||||
|
case detectMatched:
|
||||||
workflows = append(workflows, dwf)
|
workflows = append(workflows, dwf)
|
||||||
|
case detectFilteredOut:
|
||||||
|
filtered = append(filtered, dwf)
|
||||||
|
case detectNotApplicable:
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return workflows, schedules, nil
|
return workflows, schedules, filtered, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*DetectedWorkflow, error) {
|
func DetectScheduledWorkflows(ctx context.Context, gitRepo *git.Repository, commit *git.Commit) ([]*DetectedWorkflow, error) {
|
||||||
_, entries, err := ListWorkflows(commit)
|
_, entries, err := ListWorkflows(ctx, gitRepo, commit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
wfs := make([]*DetectedWorkflow, 0, len(entries))
|
wfs := make([]*DetectedWorkflow, 0, len(entries))
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
content, err := GetContentFromEntry(entry)
|
content, err := GetContentFromEntry(gitRepo, entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -252,9 +266,9 @@ func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*D
|
|||||||
return wfs, nil
|
return wfs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool {
|
func detectWorkflowMatch(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) detectResult {
|
||||||
if !canGithubEventMatch(evt.Name, triggedEvent) {
|
if !canGithubEventMatch(evt.Name, triggedEvent) {
|
||||||
return false
|
return detectNotApplicable
|
||||||
}
|
}
|
||||||
|
|
||||||
switch triggedEvent {
|
switch triggedEvent {
|
||||||
@@ -268,25 +282,31 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
|
|||||||
log.Warn("Ignore unsupported %s event arguments %v", triggedEvent, evt.Acts())
|
log.Warn("Ignore unsupported %s event arguments %v", triggedEvent, evt.Acts())
|
||||||
}
|
}
|
||||||
// no special filter parameters for these events, just return true if name matched
|
// no special filter parameters for these events, just return true if name matched
|
||||||
return true
|
return detectMatched
|
||||||
|
|
||||||
case // push
|
case // push
|
||||||
webhook_module.HookEventPush:
|
webhook_module.HookEventPush:
|
||||||
return matchPushEvent(commit, payload.(*api.PushPayload), evt)
|
return matchPushEvent(gitRepo, commit, payload.(*api.PushPayload), evt)
|
||||||
|
|
||||||
case // issues
|
case // issues
|
||||||
webhook_module.HookEventIssues,
|
webhook_module.HookEventIssues,
|
||||||
webhook_module.HookEventIssueAssign,
|
webhook_module.HookEventIssueAssign,
|
||||||
webhook_module.HookEventIssueLabel,
|
webhook_module.HookEventIssueLabel,
|
||||||
webhook_module.HookEventIssueMilestone:
|
webhook_module.HookEventIssueMilestone:
|
||||||
return matchIssuesEvent(payload.(*api.IssuePayload), evt)
|
if matchIssuesEvent(payload.(*api.IssuePayload), evt) {
|
||||||
|
return detectMatched
|
||||||
|
}
|
||||||
|
return detectNotApplicable
|
||||||
|
|
||||||
case // issue_comment
|
case // issue_comment
|
||||||
webhook_module.HookEventIssueComment,
|
webhook_module.HookEventIssueComment,
|
||||||
// `pull_request_comment` is same as `issue_comment`
|
// `pull_request_comment` is same as `issue_comment`
|
||||||
// See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_comment-use-issue_comment
|
// See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_comment-use-issue_comment
|
||||||
webhook_module.HookEventPullRequestComment:
|
webhook_module.HookEventPullRequestComment:
|
||||||
return matchIssueCommentEvent(payload.(*api.IssueCommentPayload), evt)
|
if matchIssueCommentEvent(payload.(*api.IssueCommentPayload), evt) {
|
||||||
|
return detectMatched
|
||||||
|
}
|
||||||
|
return detectNotApplicable
|
||||||
|
|
||||||
case // pull_request
|
case // pull_request
|
||||||
webhook_module.HookEventPullRequest,
|
webhook_module.HookEventPullRequest,
|
||||||
@@ -300,34 +320,49 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
|
|||||||
case // pull_request_review
|
case // pull_request_review
|
||||||
webhook_module.HookEventPullRequestReviewApproved,
|
webhook_module.HookEventPullRequestReviewApproved,
|
||||||
webhook_module.HookEventPullRequestReviewRejected:
|
webhook_module.HookEventPullRequestReviewRejected:
|
||||||
return matchPullRequestReviewEvent(payload.(*api.PullRequestPayload), evt)
|
if matchPullRequestReviewEvent(payload.(*api.PullRequestPayload), evt) {
|
||||||
|
return detectMatched
|
||||||
|
}
|
||||||
|
return detectNotApplicable
|
||||||
|
|
||||||
case // pull_request_review_comment
|
case // pull_request_review_comment
|
||||||
webhook_module.HookEventPullRequestReviewComment:
|
webhook_module.HookEventPullRequestReviewComment:
|
||||||
return matchPullRequestReviewCommentEvent(payload.(*api.PullRequestPayload), evt)
|
if matchPullRequestReviewCommentEvent(payload.(*api.PullRequestPayload), evt) {
|
||||||
|
return detectMatched
|
||||||
|
}
|
||||||
|
return detectNotApplicable
|
||||||
|
|
||||||
case // release
|
case // release
|
||||||
webhook_module.HookEventRelease:
|
webhook_module.HookEventRelease:
|
||||||
return matchReleaseEvent(payload.(*api.ReleasePayload), evt)
|
if matchReleaseEvent(payload.(*api.ReleasePayload), evt) {
|
||||||
|
return detectMatched
|
||||||
|
}
|
||||||
|
return detectNotApplicable
|
||||||
|
|
||||||
case // registry_package
|
case // registry_package
|
||||||
webhook_module.HookEventPackage:
|
webhook_module.HookEventPackage:
|
||||||
return matchPackageEvent(payload.(*api.PackagePayload), evt)
|
if matchPackageEvent(payload.(*api.PackagePayload), evt) {
|
||||||
|
return detectMatched
|
||||||
|
}
|
||||||
|
return detectNotApplicable
|
||||||
|
|
||||||
case // workflow_run
|
case // workflow_run
|
||||||
webhook_module.HookEventWorkflowRun:
|
webhook_module.HookEventWorkflowRun:
|
||||||
return matchWorkflowRunEvent(payload.(*api.WorkflowRunPayload), evt)
|
if matchWorkflowRunEvent(payload.(*api.WorkflowRunPayload), evt) {
|
||||||
|
return detectMatched
|
||||||
|
}
|
||||||
|
return detectNotApplicable
|
||||||
|
|
||||||
default:
|
default:
|
||||||
log.Warn("unsupported event %q", triggedEvent)
|
log.Warn("unsupported event %q", triggedEvent)
|
||||||
return false
|
return detectNotApplicable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobparser.Event) bool {
|
func matchPushEvent(gitRepo *git.Repository, commit *git.Commit, pushPayload *api.PushPayload, evt *jobparser.Event) detectResult {
|
||||||
// with no special filter parameters
|
// with no special filter parameters
|
||||||
if len(evt.Acts()) == 0 {
|
if len(evt.Acts()) == 0 {
|
||||||
return true
|
return detectMatched
|
||||||
}
|
}
|
||||||
|
|
||||||
matchTimes := 0
|
matchTimes := 0
|
||||||
@@ -390,10 +425,11 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
|
|||||||
matchTimes++
|
matchTimes++
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before)
|
filesChanged, err := commit.GetFilesChangedSinceCommit(gitRepo, pushPayload.Before)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)
|
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)
|
||||||
} else {
|
return detectNotApplicable
|
||||||
|
}
|
||||||
patterns, err := workflowpattern.CompilePatterns(vals...)
|
patterns, err := workflowpattern.CompilePatterns(vals...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
@@ -401,16 +437,16 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
|
|||||||
if !workflowpattern.Skip(patterns, filesChanged) {
|
if !workflowpattern.Skip(patterns, filesChanged) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
}
|
}
|
||||||
}
|
|
||||||
case "paths-ignore":
|
case "paths-ignore":
|
||||||
if refName.IsTag() {
|
if refName.IsTag() {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before)
|
filesChanged, err := commit.GetFilesChangedSinceCommit(gitRepo, pushPayload.Before)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)
|
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)
|
||||||
} else {
|
return detectNotApplicable
|
||||||
|
}
|
||||||
patterns, err := workflowpattern.CompilePatterns(vals...)
|
patterns, err := workflowpattern.CompilePatterns(vals...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
@@ -418,7 +454,6 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
|
|||||||
if !workflowpattern.Filter(patterns, filesChanged) {
|
if !workflowpattern.Filter(patterns, filesChanged) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
}
|
}
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
log.Warn("push event unsupported condition %q", cond)
|
log.Warn("push event unsupported condition %q", cond)
|
||||||
}
|
}
|
||||||
@@ -427,7 +462,10 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
|
|||||||
if hasBranchFilter && hasTagFilter {
|
if hasBranchFilter && hasTagFilter {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
}
|
}
|
||||||
return matchTimes == len(evt.Acts())
|
if matchTimes == len(evt.Acts()) {
|
||||||
|
return detectMatched
|
||||||
|
}
|
||||||
|
return detectFilteredOut
|
||||||
}
|
}
|
||||||
|
|
||||||
func matchIssuesEvent(issuePayload *api.IssuePayload, evt *jobparser.Event) bool {
|
func matchIssuesEvent(issuePayload *api.IssuePayload, evt *jobparser.Event) bool {
|
||||||
@@ -478,7 +516,7 @@ func matchIssuesEvent(issuePayload *api.IssuePayload, evt *jobparser.Event) bool
|
|||||||
return matchTimes == len(evt.Acts())
|
return matchTimes == len(evt.Acts())
|
||||||
}
|
}
|
||||||
|
|
||||||
func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayload *api.PullRequestPayload, evt *jobparser.Event) bool {
|
func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayload *api.PullRequestPayload, evt *jobparser.Event) detectResult {
|
||||||
acts := evt.Acts()
|
acts := evt.Acts()
|
||||||
activityTypeMatched := false
|
activityTypeMatched := false
|
||||||
matchTimes := 0
|
matchTimes := 0
|
||||||
@@ -525,7 +563,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
|||||||
headCommit, err = gitRepo.GetCommit(prPayload.PullRequest.Head.Sha)
|
headCommit, err = gitRepo.GetCommit(prPayload.PullRequest.Head.Sha)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetCommit [ref: %s]: %v", prPayload.PullRequest.Head.Sha, err)
|
log.Error("GetCommit [ref: %s]: %v", prPayload.PullRequest.Head.Sha, err)
|
||||||
return false
|
return detectNotApplicable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,10 +592,11 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
|||||||
matchTimes++
|
matchTimes++
|
||||||
}
|
}
|
||||||
case "paths":
|
case "paths":
|
||||||
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
|
filesChanged, err := headCommit.GetFilesChangedSinceCommit(gitRepo, prPayload.PullRequest.MergeBase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
|
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
|
||||||
} else {
|
return detectNotApplicable
|
||||||
|
}
|
||||||
patterns, err := workflowpattern.CompilePatterns(vals...)
|
patterns, err := workflowpattern.CompilePatterns(vals...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
@@ -565,12 +604,12 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
|||||||
if !workflowpattern.Skip(patterns, filesChanged) {
|
if !workflowpattern.Skip(patterns, filesChanged) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
}
|
}
|
||||||
}
|
|
||||||
case "paths-ignore":
|
case "paths-ignore":
|
||||||
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
|
filesChanged, err := headCommit.GetFilesChangedSinceCommit(gitRepo, prPayload.PullRequest.MergeBase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
|
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
|
||||||
} else {
|
return detectNotApplicable
|
||||||
|
}
|
||||||
patterns, err := workflowpattern.CompilePatterns(vals...)
|
patterns, err := workflowpattern.CompilePatterns(vals...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
@@ -578,12 +617,17 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
|||||||
if !workflowpattern.Filter(patterns, filesChanged) {
|
if !workflowpattern.Filter(patterns, filesChanged) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
}
|
}
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
log.Warn("pull request event unsupported condition %q", cond)
|
log.Warn("pull request event unsupported condition %q", cond)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return activityTypeMatched && matchTimes == len(evt.Acts())
|
if !activityTypeMatched {
|
||||||
|
return detectNotApplicable
|
||||||
|
}
|
||||||
|
if matchTimes != len(evt.Acts()) {
|
||||||
|
return detectFilteredOut
|
||||||
|
}
|
||||||
|
return detectMatched
|
||||||
}
|
}
|
||||||
|
|
||||||
func matchIssueCommentEvent(issueCommentPayload *api.IssueCommentPayload, evt *jobparser.Event) bool {
|
func matchIssueCommentEvent(issueCommentPayload *api.IssueCommentPayload, evt *jobparser.Event) bool {
|
||||||
|
|||||||
@@ -101,49 +101,49 @@ func TestDetectMatched(t *testing.T) {
|
|||||||
triggedEvent webhook_module.HookEventType
|
triggedEvent webhook_module.HookEventType
|
||||||
payload api.Payloader
|
payload api.Payloader
|
||||||
yamlOn string
|
yamlOn string
|
||||||
expected bool
|
expected detectResult
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
desc: "HookEventCreate(create) matches GithubEventCreate(create)",
|
desc: "HookEventCreate(create) matches GithubEventCreate(create)",
|
||||||
triggedEvent: webhook_module.HookEventCreate,
|
triggedEvent: webhook_module.HookEventCreate,
|
||||||
payload: nil,
|
payload: nil,
|
||||||
yamlOn: "on: create",
|
yamlOn: "on: create",
|
||||||
expected: true,
|
expected: detectMatched,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "HookEventIssues(issues) `opened` action matches GithubEventIssues(issues)",
|
desc: "HookEventIssues(issues) `opened` action matches GithubEventIssues(issues)",
|
||||||
triggedEvent: webhook_module.HookEventIssues,
|
triggedEvent: webhook_module.HookEventIssues,
|
||||||
payload: &api.IssuePayload{Action: api.HookIssueOpened},
|
payload: &api.IssuePayload{Action: api.HookIssueOpened},
|
||||||
yamlOn: "on: issues",
|
yamlOn: "on: issues",
|
||||||
expected: true,
|
expected: detectMatched,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "HookEventIssues(issues) `milestoned` action matches GithubEventIssues(issues)",
|
desc: "HookEventIssues(issues) `milestoned` action matches GithubEventIssues(issues)",
|
||||||
triggedEvent: webhook_module.HookEventIssues,
|
triggedEvent: webhook_module.HookEventIssues,
|
||||||
payload: &api.IssuePayload{Action: api.HookIssueMilestoned},
|
payload: &api.IssuePayload{Action: api.HookIssueMilestoned},
|
||||||
yamlOn: "on: issues",
|
yamlOn: "on: issues",
|
||||||
expected: true,
|
expected: detectMatched,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "HookEventPullRequestSync(pull_request_sync) matches GithubEventPullRequest(pull_request)",
|
desc: "HookEventPullRequestSync(pull_request_sync) matches GithubEventPullRequest(pull_request)",
|
||||||
triggedEvent: webhook_module.HookEventPullRequestSync,
|
triggedEvent: webhook_module.HookEventPullRequestSync,
|
||||||
payload: &api.PullRequestPayload{Action: api.HookIssueSynchronized},
|
payload: &api.PullRequestPayload{Action: api.HookIssueSynchronized},
|
||||||
yamlOn: "on: pull_request",
|
yamlOn: "on: pull_request",
|
||||||
expected: true,
|
expected: detectMatched,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "HookEventPullRequest(pull_request) `label_updated` action doesn't match GithubEventPullRequest(pull_request) with no activity type",
|
desc: "HookEventPullRequest(pull_request) `label_updated` action doesn't match GithubEventPullRequest(pull_request) with no activity type",
|
||||||
triggedEvent: webhook_module.HookEventPullRequest,
|
triggedEvent: webhook_module.HookEventPullRequest,
|
||||||
payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated},
|
payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated},
|
||||||
yamlOn: "on: pull_request",
|
yamlOn: "on: pull_request",
|
||||||
expected: false,
|
expected: detectNotApplicable,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "HookEventPullRequest(pull_request) `closed` action doesn't match GithubEventPullRequest(pull_request) with no activity type",
|
desc: "HookEventPullRequest(pull_request) `closed` action doesn't match GithubEventPullRequest(pull_request) with no activity type",
|
||||||
triggedEvent: webhook_module.HookEventPullRequest,
|
triggedEvent: webhook_module.HookEventPullRequest,
|
||||||
payload: &api.PullRequestPayload{Action: api.HookIssueClosed},
|
payload: &api.PullRequestPayload{Action: api.HookIssueClosed},
|
||||||
yamlOn: "on: pull_request",
|
yamlOn: "on: pull_request",
|
||||||
expected: false,
|
expected: detectNotApplicable,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "HookEventPullRequest(pull_request) `closed` action doesn't match GithubEventPullRequest(pull_request) with branches",
|
desc: "HookEventPullRequest(pull_request) `closed` action doesn't match GithubEventPullRequest(pull_request) with branches",
|
||||||
@@ -155,56 +155,56 @@ func TestDetectMatched(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
yamlOn: "on:\n pull_request:\n branches: [main]",
|
yamlOn: "on:\n pull_request:\n branches: [main]",
|
||||||
expected: false,
|
expected: detectNotApplicable,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "HookEventPullRequest(pull_request) `label_updated` action matches GithubEventPullRequest(pull_request) with `label` activity type",
|
desc: "HookEventPullRequest(pull_request) `label_updated` action matches GithubEventPullRequest(pull_request) with `label` activity type",
|
||||||
triggedEvent: webhook_module.HookEventPullRequest,
|
triggedEvent: webhook_module.HookEventPullRequest,
|
||||||
payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated},
|
payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated},
|
||||||
yamlOn: "on:\n pull_request:\n types: [labeled]",
|
yamlOn: "on:\n pull_request:\n types: [labeled]",
|
||||||
expected: true,
|
expected: detectMatched,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "HookEventPullRequestReviewComment(pull_request_review_comment) matches GithubEventPullRequestReviewComment(pull_request_review_comment)",
|
desc: "HookEventPullRequestReviewComment(pull_request_review_comment) matches GithubEventPullRequestReviewComment(pull_request_review_comment)",
|
||||||
triggedEvent: webhook_module.HookEventPullRequestReviewComment,
|
triggedEvent: webhook_module.HookEventPullRequestReviewComment,
|
||||||
payload: &api.PullRequestPayload{Action: api.HookIssueReviewed},
|
payload: &api.PullRequestPayload{Action: api.HookIssueReviewed},
|
||||||
yamlOn: "on:\n pull_request_review_comment:\n types: [created]",
|
yamlOn: "on:\n pull_request_review_comment:\n types: [created]",
|
||||||
expected: true,
|
expected: detectMatched,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "HookEventPullRequestReviewRejected(pull_request_review_rejected) doesn't match GithubEventPullRequestReview(pull_request_review) with `dismissed` activity type (we don't support `dismissed` at present)",
|
desc: "HookEventPullRequestReviewRejected(pull_request_review_rejected) doesn't match GithubEventPullRequestReview(pull_request_review) with `dismissed` activity type (we don't support `dismissed` at present)",
|
||||||
triggedEvent: webhook_module.HookEventPullRequestReviewRejected,
|
triggedEvent: webhook_module.HookEventPullRequestReviewRejected,
|
||||||
payload: &api.PullRequestPayload{Action: api.HookIssueReviewed},
|
payload: &api.PullRequestPayload{Action: api.HookIssueReviewed},
|
||||||
yamlOn: "on:\n pull_request_review:\n types: [dismissed]",
|
yamlOn: "on:\n pull_request_review:\n types: [dismissed]",
|
||||||
expected: false,
|
expected: detectNotApplicable,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "HookEventRelease(release) `published` action matches GithubEventRelease(release) with `published` activity type",
|
desc: "HookEventRelease(release) `published` action matches GithubEventRelease(release) with `published` activity type",
|
||||||
triggedEvent: webhook_module.HookEventRelease,
|
triggedEvent: webhook_module.HookEventRelease,
|
||||||
payload: &api.ReleasePayload{Action: api.HookReleasePublished},
|
payload: &api.ReleasePayload{Action: api.HookReleasePublished},
|
||||||
yamlOn: "on:\n release:\n types: [published]",
|
yamlOn: "on:\n release:\n types: [published]",
|
||||||
expected: true,
|
expected: detectMatched,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "HookEventPackage(package) `created` action doesn't match GithubEventRegistryPackage(registry_package) with `updated` activity type",
|
desc: "HookEventPackage(package) `created` action doesn't match GithubEventRegistryPackage(registry_package) with `updated` activity type",
|
||||||
triggedEvent: webhook_module.HookEventPackage,
|
triggedEvent: webhook_module.HookEventPackage,
|
||||||
payload: &api.PackagePayload{Action: api.HookPackageCreated},
|
payload: &api.PackagePayload{Action: api.HookPackageCreated},
|
||||||
yamlOn: "on:\n registry_package:\n types: [updated]",
|
yamlOn: "on:\n registry_package:\n types: [updated]",
|
||||||
expected: false,
|
expected: detectNotApplicable,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "HookEventWiki(wiki) matches GithubEventGollum(gollum)",
|
desc: "HookEventWiki(wiki) matches GithubEventGollum(gollum)",
|
||||||
triggedEvent: webhook_module.HookEventWiki,
|
triggedEvent: webhook_module.HookEventWiki,
|
||||||
payload: nil,
|
payload: nil,
|
||||||
yamlOn: "on: gollum",
|
yamlOn: "on: gollum",
|
||||||
expected: true,
|
expected: detectMatched,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "HookEventSchedule(schedule) matches GithubEventSchedule(schedule)",
|
desc: "HookEventSchedule(schedule) matches GithubEventSchedule(schedule)",
|
||||||
triggedEvent: webhook_module.HookEventSchedule,
|
triggedEvent: webhook_module.HookEventSchedule,
|
||||||
payload: nil,
|
payload: nil,
|
||||||
yamlOn: "on: schedule",
|
yamlOn: "on: schedule",
|
||||||
expected: true,
|
expected: detectMatched,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "push to tag matches workflow with paths condition (should skip paths check)",
|
desc: "push to tag matches workflow with paths condition (should skip paths check)",
|
||||||
@@ -222,7 +222,19 @@ func TestDetectMatched(t *testing.T) {
|
|||||||
},
|
},
|
||||||
commit: nil,
|
commit: nil,
|
||||||
yamlOn: "on:\n push:\n paths:\n - src/**",
|
yamlOn: "on:\n push:\n paths:\n - src/**",
|
||||||
expected: true,
|
expected: detectMatched,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "push branch filter excludes -> filtered out",
|
||||||
|
triggedEvent: webhook_module.HookEventPush,
|
||||||
|
payload: &api.PushPayload{
|
||||||
|
Ref: "refs/heads/feature/x",
|
||||||
|
Before: "0000000",
|
||||||
|
Commits: []*api.PayloadCommit{{ID: "abc", Added: []string{"a.go"}, Message: "x"}},
|
||||||
|
},
|
||||||
|
commit: nil,
|
||||||
|
yamlOn: "on:\n push:\n branches: [main]",
|
||||||
|
expected: detectFilteredOut,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +243,7 @@ func TestDetectMatched(t *testing.T) {
|
|||||||
evts, err := GetEventsFromContent(fullWorkflowContent(tc.yamlOn))
|
evts, err := GetEventsFromContent(fullWorkflowContent(tc.yamlOn))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, evts, 1)
|
assert.Len(t, evts, 1)
|
||||||
assert.Equal(t, tc.expected, detectMatched(nil, tc.commit, tc.triggedEvent, tc.payload, evts[0]))
|
assert.Equal(t, tc.expected, detectWorkflowMatch(nil, tc.commit, tc.triggedEvent, tc.payload, evts[0]))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,14 @@
|
|||||||
package openid
|
package openid
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitea.dev/modules/hostmatcher"
|
||||||
|
"gitea.dev/modules/proxy"
|
||||||
|
"gitea.dev/modules/setting"
|
||||||
|
|
||||||
"github.com/yohcop/openid-go"
|
"github.com/yohcop/openid-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,11 +25,23 @@ import (
|
|||||||
var (
|
var (
|
||||||
nonceStore = openid.NewSimpleNonceStore()
|
nonceStore = openid.NewSimpleNonceStore()
|
||||||
discoveryCache = newTimedDiscoveryCache(24 * time.Hour)
|
discoveryCache = newTimedDiscoveryCache(24 * time.Hour)
|
||||||
|
|
||||||
|
// openIDInstance does discovery/verification via an SSRF-protected client, so a user-supplied
|
||||||
|
// OpenID identifier can't reach internal/loopback/reserved addresses. It honors the operator's
|
||||||
|
// [security] ALLOWED_HOST_LIST (empty defaults to "external"), matching the avatar/webhook/migration
|
||||||
|
// clients, and validates the proxy path too. Lazy: reads proxy/settings once.
|
||||||
|
openIDInstance = sync.OnceValue(func() *openid.OpenID {
|
||||||
|
allowList := hostmatcher.ParseHostMatchList("security.ALLOWED_HOST_LIST", setting.Security.AllowedHostList)
|
||||||
|
return openid.NewOpenID(&http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
Transport: hostmatcher.NewHTTPTransport("openid", allowList, nil, proxy.Proxy(), setting.Proxy.ProxyURLFixed, nil),
|
||||||
|
})
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
// Verify handles response from OpenID provider
|
// Verify handles response from OpenID provider
|
||||||
func Verify(fullURL string) (id string, err error) {
|
func Verify(fullURL string) (id string, err error) {
|
||||||
return openid.Verify(fullURL, discoveryCache, nonceStore)
|
return openIDInstance().Verify(fullURL, discoveryCache, nonceStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize normalizes an OpenID URI
|
// Normalize normalizes an OpenID URI
|
||||||
@@ -33,5 +51,5 @@ func Normalize(url string) (id string, err error) {
|
|||||||
|
|
||||||
// RedirectURL redirects browser
|
// RedirectURL redirects browser
|
||||||
func RedirectURL(id, callbackURL, realm string) (string, error) {
|
func RedirectURL(id, callbackURL, realm string) (string, error) {
|
||||||
return openid.RedirectURL(id, callbackURL, realm)
|
return openIDInstance().RedirectURL(id, callbackURL, realm)
|
||||||
}
|
}
|
||||||
|
|||||||
29
modules/auth/openid/openid_test.go
Normal file
29
modules/auth/openid/openid_test.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package openid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOpenIDDiscoveryBlocksInternalHost(t *testing.T) {
|
||||||
|
var reached atomic.Bool
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
reached.Store(true)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
// RedirectURL performs server-side discovery of the identifier URL; a loopback URL
|
||||||
|
// must be refused at dial time instead of reaching the internal server
|
||||||
|
_, err := RedirectURL(srv.URL, "http://example.com/callback", "http://example.com/")
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.False(t, reached.Load(), "OpenID discovery must not reach an internal/loopback host")
|
||||||
|
}
|
||||||
@@ -3,7 +3,11 @@
|
|||||||
|
|
||||||
package fileicon
|
package fileicon
|
||||||
|
|
||||||
import "gitea.dev/modules/git"
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.dev/modules/git"
|
||||||
|
)
|
||||||
|
|
||||||
type EntryInfo struct {
|
type EntryInfo struct {
|
||||||
BaseName string
|
BaseName string
|
||||||
@@ -12,10 +16,10 @@ type EntryInfo struct {
|
|||||||
IsOpen bool
|
IsOpen bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func EntryInfoFromGitTreeEntry(commit *git.Commit, fullPath string, gitEntry *git.TreeEntry) *EntryInfo {
|
func EntryInfoFromGitTreeEntry(ctx context.Context, gitRepo *git.Repository, commit *git.Commit, fullPath string, gitEntry *git.TreeEntry) *EntryInfo {
|
||||||
ret := &EntryInfo{BaseName: gitEntry.Name(), EntryMode: gitEntry.Mode()}
|
ret := &EntryInfo{BaseName: gitEntry.Name(), EntryMode: gitEntry.Mode()}
|
||||||
if gitEntry.IsLink() {
|
if gitEntry.IsLink() {
|
||||||
if res, err := git.EntryFollowLink(commit, fullPath, gitEntry); err == nil && res.TargetEntry.IsDir() {
|
if res, err := git.EntryFollowLink(ctx, gitRepo, commit, fullPath, gitEntry); err == nil && res.TargetEntry.IsDir() {
|
||||||
ret.SymlinkToMode = res.TargetEntry.Mode()
|
ret.SymlinkToMode = res.TargetEntry.Mode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,17 +17,17 @@ import (
|
|||||||
|
|
||||||
// Commit represents a git commit.
|
// Commit represents a git commit.
|
||||||
type Commit struct {
|
type Commit struct {
|
||||||
Tree // FIXME: bad design, this field can be nil if the commit is from "last commit cache"
|
|
||||||
|
|
||||||
CommitMessage
|
CommitMessage
|
||||||
|
|
||||||
ID ObjectID
|
ID ObjectID
|
||||||
|
TreeID ObjectID
|
||||||
|
Parents []ObjectID
|
||||||
Author *Signature // never nil
|
Author *Signature // never nil
|
||||||
Committer *Signature // never nil
|
Committer *Signature // never nil
|
||||||
Signature *CommitSignature
|
Signature *CommitSignature
|
||||||
|
|
||||||
Parents []ObjectID // ID strings
|
|
||||||
submoduleCache *ObjectCache[*SubModule]
|
submoduleCache *ObjectCache[*SubModule]
|
||||||
|
treeCache *Tree
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommitSignature represents a git commit signature part.
|
// CommitSignature represents a git commit signature part.
|
||||||
@@ -46,12 +46,12 @@ func (c *Commit) ParentID(n int) (ObjectID, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parent returns n-th parent (0-based index) of the commit.
|
// Parent returns n-th parent (0-based index) of the commit.
|
||||||
func (c *Commit) Parent(n int) (*Commit, error) {
|
func (c *Commit) Parent(gitRepo *Repository, n int) (*Commit, error) {
|
||||||
id, err := c.ParentID(n)
|
id, err := c.ParentID(n)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
parent, err := c.repo.getCommit(id)
|
parent, err := gitRepo.getCommit(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -65,25 +65,44 @@ func (c *Commit) ParentCount() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetCommitByPath return the commit of relative path object.
|
// GetCommitByPath return the commit of relative path object.
|
||||||
func (c *Commit) GetCommitByPath(relpath string) (*Commit, error) {
|
func (c *Commit) GetCommitByPath(gitRepo *Repository, relpath string) (*Commit, error) {
|
||||||
if c.repo.LastCommitCache != nil {
|
if gitRepo.LastCommitCache != nil {
|
||||||
return c.repo.LastCommitCache.GetCommitByPath(c.ID.String(), relpath)
|
return gitRepo.LastCommitCache.GetCommitByPath(c.ID.String(), relpath)
|
||||||
}
|
}
|
||||||
return c.repo.getCommitByPathWithID(c.ID, relpath)
|
return gitRepo.getCommitByPathWithID(c.ID, relpath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commit) Tree() *Tree {
|
||||||
|
if c.treeCache == nil {
|
||||||
|
c.treeCache = newTree(c.TreeID)
|
||||||
|
}
|
||||||
|
return c.treeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commit) GetBlobByPath(ctx context.Context, gitRepo *Repository, relpath string) (*Blob, error) {
|
||||||
|
return c.Tree().GetBlobByPath(ctx, gitRepo, relpath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commit) GetTreeEntryByPath(ctx context.Context, gitRepo *Repository, relpath string) (_ *TreeEntry, err error) {
|
||||||
|
return c.Tree().GetTreeEntryByPath(ctx, gitRepo, relpath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commit) SubTree(ctx context.Context, gitRepo *Repository, relpath string) (*Tree, error) {
|
||||||
|
return c.Tree().SubTree(ctx, gitRepo, relpath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommitsByRange returns the specific page commits before current revision, every page's number default by CommitsRangeSize
|
// CommitsByRange returns the specific page commits before current revision, every page's number default by CommitsRangeSize
|
||||||
func (c *Commit) CommitsByRange(page, pageSize int, not, since, until string) ([]*Commit, error) {
|
func (c *Commit) CommitsByRange(gitRepo *Repository, page, pageSize int, not, since, until string) ([]*Commit, error) {
|
||||||
return c.repo.commitsByRangeWithTime(c.ID, page, pageSize, not, since, until)
|
return gitRepo.commitsByRangeWithTime(c.ID, page, pageSize, not, since, until)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommitsBefore returns all the commits before current revision
|
// CommitsBefore returns all the commits before current revision
|
||||||
func (c *Commit) CommitsBefore() ([]*Commit, error) {
|
func (c *Commit) CommitsBefore(gitRepo *Repository) ([]*Commit, error) {
|
||||||
return c.repo.getCommitsBefore(c.ID)
|
return gitRepo.getCommitsBefore(c.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasPreviousCommit returns true if a given commitHash is contained in commit's parents
|
// HasPreviousCommit returns true if a given commitHash is contained in commit's parents
|
||||||
func (c *Commit) HasPreviousCommit(objectID ObjectID) (bool, error) {
|
func (c *Commit) HasPreviousCommit(ctx context.Context, gitRepo *Repository, objectID ObjectID) (bool, error) {
|
||||||
this := c.ID.String()
|
this := c.ID.String()
|
||||||
that := objectID.String()
|
that := objectID.String()
|
||||||
|
|
||||||
@@ -93,8 +112,8 @@ func (c *Commit) HasPreviousCommit(objectID ObjectID) (bool, error) {
|
|||||||
|
|
||||||
_, _, err := gitcmd.NewCommand("merge-base", "--is-ancestor").
|
_, _, err := gitcmd.NewCommand("merge-base", "--is-ancestor").
|
||||||
AddDynamicArguments(that, this).
|
AddDynamicArguments(that, this).
|
||||||
WithDir(c.repo.Path).
|
WithDir(gitRepo.Path).
|
||||||
RunStdString(c.repo.Ctx)
|
RunStdString(ctx)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
@@ -108,8 +127,8 @@ func (c *Commit) HasPreviousCommit(objectID ObjectID) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IsForcePush returns true if a push from oldCommitHash to this is a force push
|
// IsForcePush returns true if a push from oldCommitHash to this is a force push
|
||||||
func (c *Commit) IsForcePush(oldCommitID string) (bool, error) {
|
func (c *Commit) IsForcePush(ctx context.Context, gitRepo *Repository, oldCommitID string) (bool, error) {
|
||||||
objectFormat, err := c.repo.GetObjectFormat()
|
objectFormat, err := gitRepo.GetObjectFormat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -117,22 +136,22 @@ func (c *Commit) IsForcePush(oldCommitID string) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
oldCommit, err := c.repo.GetCommit(oldCommitID)
|
oldCommit, err := gitRepo.GetCommit(oldCommitID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
hasPreviousCommit, err := c.HasPreviousCommit(oldCommit.ID)
|
hasPreviousCommit, err := c.HasPreviousCommit(ctx, gitRepo, oldCommit.ID)
|
||||||
return !hasPreviousCommit, err
|
return !hasPreviousCommit, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommitsBeforeLimit returns num commits before current revision
|
// CommitsBeforeLimit returns num commits before current revision
|
||||||
func (c *Commit) CommitsBeforeLimit(num int) ([]*Commit, error) {
|
func (c *Commit) CommitsBeforeLimit(gitRepo *Repository, num int) ([]*Commit, error) {
|
||||||
return c.repo.getCommitsBeforeLimit(c.ID, num)
|
return gitRepo.getCommitsBeforeLimit(c.ID, num)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommitsBeforeUntil returns the commits in range "[cur, ref)"
|
// CommitsBeforeUntil returns the commits in range "[cur, ref)"
|
||||||
func (c *Commit) CommitsBeforeUntil(ref RefName) ([]*Commit, error) {
|
func (c *Commit) CommitsBeforeUntil(gitRepo *Repository, ref RefName) ([]*Commit, error) {
|
||||||
return c.repo.CommitsBetween(c.ID.RefName(), ref, -1)
|
return gitRepo.CommitsBetween(c.ID.RefName(), ref, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchCommitsOptions specify the parameters for SearchCommits
|
// SearchCommitsOptions specify the parameters for SearchCommits
|
||||||
@@ -175,39 +194,29 @@ func NewSearchCommitsOptions(searchString string, forAllRefs bool) SearchCommits
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SearchCommits returns the commits match the keyword before current revision
|
// SearchCommits returns the commits match the keyword before current revision
|
||||||
func (c *Commit) SearchCommits(opts SearchCommitsOptions) ([]*Commit, error) {
|
func (c *Commit) SearchCommits(gitRepo *Repository, opts SearchCommitsOptions) ([]*Commit, error) {
|
||||||
return c.repo.searchCommits(c.ID, opts)
|
return gitRepo.searchCommits(c.ID, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFilesChangedSinceCommit get all changed file names between pastCommit to current revision
|
// GetFilesChangedSinceCommit get all changed file names between pastCommit to current revision
|
||||||
func (c *Commit) GetFilesChangedSinceCommit(pastCommit string) ([]string, error) {
|
func (c *Commit) GetFilesChangedSinceCommit(gitRepo *Repository, pastCommit string) ([]string, error) {
|
||||||
return c.repo.GetFilesChangedBetween(pastCommit, c.ID.String())
|
return gitRepo.GetFilesChangedBetween(pastCommit, c.ID.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileChangedSinceCommit Returns true if the file given has changed since the past commit
|
// FileChangedSinceCommit Returns true if the file given has changed since the past commit
|
||||||
// YOU MUST ENSURE THAT pastCommit is a valid commit ID.
|
// YOU MUST ENSURE THAT pastCommit is a valid commit ID.
|
||||||
func (c *Commit) FileChangedSinceCommit(filename, pastCommit string) (bool, error) {
|
func (c *Commit) FileChangedSinceCommit(gitRepo *Repository, filename, pastCommit string) (bool, error) {
|
||||||
return c.repo.FileChangedBetweenCommits(filename, pastCommit, c.ID.String())
|
return gitRepo.FileChangedBetweenCommits(filename, pastCommit, c.ID.String())
|
||||||
}
|
|
||||||
|
|
||||||
// HasFile returns true if the file given exists on this commit
|
|
||||||
// This does only mean it's there - it does not mean the file was changed during the commit.
|
|
||||||
func (c *Commit) HasFile(filename string) (bool, error) {
|
|
||||||
_, err := c.GetBlobByPath(filename)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFileContent reads a file content as a string or returns false if this was not possible
|
// GetFileContent reads a file content as a string or returns false if this was not possible
|
||||||
func (c *Commit) GetFileContent(filename string, limit int) (string, error) {
|
func (c *Commit) GetFileContent(ctx context.Context, gitRepo *Repository, filename string, limit int) (string, error) {
|
||||||
entry, err := c.GetTreeEntryByPath(filename)
|
entry, err := c.GetTreeEntryByPath(ctx, gitRepo, filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := entry.Blob().DataAsync()
|
r, err := entry.Blob(gitRepo).DataAsync()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
// CommitInfo describes the first commit with the provided entry
|
// CommitInfo describes the first commit with the provided entry
|
||||||
type CommitInfo struct {
|
type CommitInfo struct {
|
||||||
Entry *TreeEntry
|
Entry *TreeEntry
|
||||||
@@ -10,8 +12,8 @@ type CommitInfo struct {
|
|||||||
SubmoduleFile *CommitSubmoduleFile
|
SubmoduleFile *CommitSubmoduleFile
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetCommitInfoSubmoduleFile(repoLink, fullPath string, commit *Commit, refCommitID ObjectID) (*CommitSubmoduleFile, error) {
|
func GetCommitInfoSubmoduleFile(ctx context.Context, repoLink, fullPath string, gitRepo *Repository, commit *Commit, refCommitID ObjectID) (*CommitSubmoduleFile, error) {
|
||||||
submodule, err := commit.GetSubModule(fullPath)
|
submodule, err := commit.GetSubModule(ctx, gitRepo, fullPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// GetCommitsInfo gets information of all commits that are corresponding to these entries
|
// GetCommitsInfo gets information of all commits that are corresponding to these entries
|
||||||
func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
|
func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, gitRepo *Repository, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
|
||||||
entryPaths := make([]string, len(tes)+1)
|
entryPaths := make([]string, len(tes)+1)
|
||||||
// Get the commit for the treePath itself
|
// Get the commit for the treePath itself
|
||||||
entryPaths[0] = ""
|
entryPaths[0] = ""
|
||||||
@@ -25,25 +25,16 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *
|
|||||||
entryPaths[i+1] = entry.Name()
|
entryPaths[i+1] = entry.Name()
|
||||||
}
|
}
|
||||||
|
|
||||||
commitNodeIndex, commitGraphFile := commit.repo.CommitNodeIndex()
|
|
||||||
if commitGraphFile != nil {
|
|
||||||
defer commitGraphFile.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := commitNodeIndex.Get(plumbing.Hash(commit.ID.RawValue()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var revs map[string]*Commit
|
var revs map[string]*Commit
|
||||||
if commit.repo.LastCommitCache != nil {
|
var err error
|
||||||
|
if gitRepo.LastCommitCache != nil {
|
||||||
var unHitPaths []string
|
var unHitPaths []string
|
||||||
revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, commit.repo.LastCommitCache)
|
revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, gitRepo.LastCommitCache)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
if len(unHitPaths) > 0 {
|
if len(unHitPaths) > 0 {
|
||||||
revs2, err := GetLastCommitForPaths(ctx, commit.repo.LastCommitCache, c, treePath, unHitPaths)
|
revs2, err := GetLastCommitForPaths(ctx, gitRepo, commit, treePath, unHitPaths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -51,13 +42,13 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *
|
|||||||
maps.Copy(revs, revs2)
|
maps.Copy(revs, revs2)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
revs, err = GetLastCommitForPaths(ctx, nil, c, treePath, entryPaths)
|
revs, err = GetLastCommitForPaths(ctx, gitRepo, commit, treePath, entryPaths)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
commit.repo.gogitStorage.Close()
|
gitRepo.gogitStorage.Close()
|
||||||
|
|
||||||
commitsInfo := make([]CommitInfo, len(tes))
|
commitsInfo := make([]CommitInfo, len(tes))
|
||||||
for i, entry := range tes {
|
for i, entry := range tes {
|
||||||
@@ -72,7 +63,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *
|
|||||||
|
|
||||||
// If the entry is a submodule, add a submodule file for this
|
// If the entry is a submodule, add a submodule file for this
|
||||||
if entry.IsSubModule() {
|
if entry.IsSubModule() {
|
||||||
commitsInfo[i].SubmoduleFile, err = GetCommitInfoSubmoduleFile(repoLink, path.Join(treePath, entry.Name()), commit, entry.ID)
|
commitsInfo[i].SubmoduleFile, err = GetCommitInfoSubmoduleFile(ctx, repoLink, path.Join(treePath, entry.Name()), gitRepo, commit, entry.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -83,11 +74,10 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *
|
|||||||
// get it for free during the tree traversal and it's used for listing
|
// get it for free during the tree traversal and it's used for listing
|
||||||
// pages to display information about newest commit for a given path.
|
// pages to display information about newest commit for a given path.
|
||||||
var treeCommit *Commit
|
var treeCommit *Commit
|
||||||
var ok bool
|
|
||||||
if treePath == "" {
|
if treePath == "" {
|
||||||
treeCommit = commit
|
treeCommit = commit
|
||||||
} else if treeCommit, ok = revs[""]; ok {
|
} else {
|
||||||
treeCommit.repo = commit.repo
|
treeCommit = revs[""]
|
||||||
}
|
}
|
||||||
return commitsInfo, treeCommit, nil
|
return commitsInfo, treeCommit, nil
|
||||||
}
|
}
|
||||||
@@ -162,7 +152,20 @@ func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cac
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetLastCommitForPaths returns last commit information
|
// GetLastCommitForPaths returns last commit information
|
||||||
func GetLastCommitForPaths(ctx context.Context, cache *LastCommitCache, c cgobject.CommitNode, treePath string, paths []string) (map[string]*Commit, error) {
|
func GetLastCommitForPaths(ctx context.Context, gitRepo *Repository, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) {
|
||||||
|
commitNodeIndex, commitGraphFile := gitRepo.CommitNodeIndex()
|
||||||
|
if commitGraphFile != nil {
|
||||||
|
defer commitGraphFile.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
refSha := c.ID().String()
|
||||||
|
|
||||||
// We do a tree traversal with nodes sorted by commit time
|
// We do a tree traversal with nodes sorted by commit time
|
||||||
@@ -243,7 +246,7 @@ heaploop:
|
|||||||
// match any of the hashes being merged. This is more common for directories,
|
// 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.
|
// but it can also happen if a file is changed through conflict resolution.
|
||||||
resultNodes[pth] = current.commit
|
resultNodes[pth] = current.commit
|
||||||
if err := cache.Put(refSha, path.Join(treePath, pth), current.commit.ID().String()); err != nil {
|
if err := gitRepo.LastCommitCache.Put(refSha, path.Join(treePath, pth), current.commit.ID().String()); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// GetCommitsInfo gets information of all commits that are corresponding to these entries
|
// GetCommitsInfo gets information of all commits that are corresponding to these entries
|
||||||
func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
|
func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, gitRepo *Repository, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
|
||||||
entryPaths := make([]string, len(tes)+1)
|
entryPaths := make([]string, len(tes)+1)
|
||||||
// Get the commit for the treePath itself
|
// Get the commit for the treePath itself
|
||||||
entryPaths[0] = ""
|
entryPaths[0] = ""
|
||||||
@@ -26,15 +26,15 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
var revs map[string]*Commit
|
var revs map[string]*Commit
|
||||||
if commit.repo.LastCommitCache != nil {
|
if gitRepo.LastCommitCache != nil {
|
||||||
var unHitPaths []string
|
var unHitPaths []string
|
||||||
revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, commit.repo.LastCommitCache)
|
revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, gitRepo.LastCommitCache)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
if len(unHitPaths) > 0 {
|
if len(unHitPaths) > 0 {
|
||||||
sort.Strings(unHitPaths)
|
sort.Strings(unHitPaths)
|
||||||
commits, err := GetLastCommitForPaths(ctx, commit, treePath, unHitPaths)
|
commits, err := GetLastCommitForPaths(ctx, gitRepo, commit, treePath, unHitPaths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sort.Strings(entryPaths)
|
sort.Strings(entryPaths)
|
||||||
revs, err = GetLastCommitForPaths(ctx, commit, treePath, entryPaths)
|
revs, err = GetLastCommitForPaths(ctx, gitRepo, commit, treePath, entryPaths)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@@ -64,7 +64,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *
|
|||||||
|
|
||||||
// If the entry is a submodule, add a submodule file for this
|
// If the entry is a submodule, add a submodule file for this
|
||||||
if entry.IsSubModule() {
|
if entry.IsSubModule() {
|
||||||
commitsInfo[i].SubmoduleFile, err = GetCommitInfoSubmoduleFile(repoLink, path.Join(treePath, entry.Name()), commit, entry.ID)
|
commitsInfo[i].SubmoduleFile, err = GetCommitInfoSubmoduleFile(ctx, repoLink, path.Join(treePath, entry.Name()), gitRepo, commit, entry.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -75,11 +75,10 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *
|
|||||||
// get it for free during the tree traversal, and it's used for listing
|
// get it for free during the tree traversal, and it's used for listing
|
||||||
// pages to display information about the newest commit for a given path.
|
// pages to display information about the newest commit for a given path.
|
||||||
var treeCommit *Commit
|
var treeCommit *Commit
|
||||||
var ok bool
|
|
||||||
if treePath == "" {
|
if treePath == "" {
|
||||||
treeCommit = commit
|
treeCommit = commit
|
||||||
} else if treeCommit, ok = revs[""]; ok {
|
} else {
|
||||||
treeCommit.repo = commit.repo
|
treeCommit = revs[""]
|
||||||
}
|
}
|
||||||
return commitsInfo, treeCommit, nil
|
return commitsInfo, treeCommit, nil
|
||||||
}
|
}
|
||||||
@@ -104,9 +103,9 @@ func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cac
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetLastCommitForPaths returns last commit information
|
// GetLastCommitForPaths returns last commit information
|
||||||
func GetLastCommitForPaths(ctx context.Context, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) {
|
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
|
// We read backwards from the commit to obtain all of the commits
|
||||||
revs, err := walkGitLog(ctx, commit.repo, commit, treePath, paths...)
|
revs, err := walkGitLog(ctx, gitRepo, commit, treePath, paths...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -126,7 +125,7 @@ func GetLastCommitForPaths(ctx context.Context, commit *Commit, treePath string,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := commit.repo.GetCommit(commitID) // Ensure the commit exists in the repository
|
c, err := gitRepo.GetCommit(commitID) // Ensure the commit exists in the repository
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func TestEntries_GetCommitsInfo_ContextErr(t *testing.T) {
|
|||||||
|
|
||||||
commit, err := repo.GetCommit("feaf4ba6bc635fec442f46ddd4512416ec43c2c2")
|
commit, err := repo.GetCommit("feaf4ba6bc635fec442f46ddd4512416ec43c2c2")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
entries, err := commit.Tree.ListEntries()
|
entries, err := commit.Tree().ListEntries(t.Context(), repo)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
countCommitInfosCommit := func(infos []CommitInfo) (nilCommits, nonNilCommits int) {
|
countCommitInfosCommit := func(infos []CommitInfo) (nilCommits, nonNilCommits int) {
|
||||||
@@ -39,14 +39,14 @@ func TestEntries_GetCommitsInfo_ContextErr(t *testing.T) {
|
|||||||
defer test.MockVariableValue(&walkGitLogDebugBeforeNext)()
|
defer test.MockVariableValue(&walkGitLogDebugBeforeNext)()
|
||||||
|
|
||||||
walkGitLogDebugBeforeNext = cancel
|
walkGitLogDebugBeforeNext = cancel
|
||||||
commitInfos, _, err := entries.GetCommitsInfo(ctx, "/any/repo-link", commit, "")
|
commitInfos, _, err := entries.GetCommitsInfo(ctx, "/any/repo-link", repo, commit, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
nilCommits, nonNilCommits := countCommitInfosCommit(commitInfos)
|
nilCommits, nonNilCommits := countCommitInfosCommit(commitInfos)
|
||||||
assert.Equal(t, 0, nonNilCommits) // no commit info due to canceled (or deadline-exceeded) context
|
assert.Equal(t, 0, nonNilCommits) // no commit info due to canceled (or deadline-exceeded) context
|
||||||
assert.Equal(t, 3, nilCommits)
|
assert.Equal(t, 3, nilCommits)
|
||||||
|
|
||||||
walkGitLogDebugBeforeNext = nil
|
walkGitLogDebugBeforeNext = nil
|
||||||
commitInfos, _, err = entries.GetCommitsInfo(t.Context(), "/any/repo-link", commit, "")
|
commitInfos, _, err = entries.GetCommitsInfo(t.Context(), "/any/repo-link", repo, commit, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
nilCommits, nonNilCommits = countCommitInfosCommit(commitInfos)
|
nilCommits, nonNilCommits = countCommitInfosCommit(commitInfos)
|
||||||
assert.Equal(t, 3, nonNilCommits)
|
assert.Equal(t, 3, nonNilCommits)
|
||||||
|
|||||||
@@ -91,10 +91,9 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
assert.NotNil(t, commit)
|
assert.NotNil(t, commit)
|
||||||
assert.NotNil(t, commit.Tree)
|
assert.NotNil(t, commit.TreeID)
|
||||||
assert.NotNil(t, commit.Tree.repo)
|
|
||||||
|
|
||||||
tree, err := commit.Tree.SubTree(testCase.Path)
|
tree, err := commit.SubTree(t.Context(), repo1, testCase.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
assert.NoError(t, err, "Unable to get subtree: %s of commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err)
|
assert.NoError(t, err, "Unable to get subtree: %s of commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err)
|
||||||
// no point trying to do anything else for this test.
|
// no point trying to do anything else for this test.
|
||||||
@@ -102,9 +101,8 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert.NotNil(t, tree, "tree is nil for testCase CommitID %s in Path %s", testCase.CommitID, testCase.Path)
|
assert.NotNil(t, tree, "tree is nil for testCase CommitID %s in Path %s", testCase.CommitID, testCase.Path)
|
||||||
assert.NotNil(t, tree.repo, "repo is nil for testCase CommitID %s in Path %s", testCase.CommitID, testCase.Path)
|
|
||||||
|
|
||||||
entries, err := tree.ListEntries()
|
entries, err := tree.ListEntries(t.Context(), repo1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
assert.NoError(t, err, "Unable to get entries of subtree: %s in commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err)
|
assert.NoError(t, err, "Unable to get entries of subtree: %s in commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err)
|
||||||
// no point trying to do anything else for this test.
|
// no point trying to do anything else for this test.
|
||||||
@@ -112,7 +110,7 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Context.TODO() - if graceful has started we should use its Shutdown context otherwise use install signals in TestMain.
|
// FIXME: Context.TODO() - if graceful has started we should use its Shutdown context otherwise use install signals in TestMain.
|
||||||
commitsInfo, treeCommit, err := entries.GetCommitsInfo(t.Context(), "/any/repo-link", commit, testCase.Path)
|
commitsInfo, treeCommit, err := entries.GetCommitsInfo(t.Context(), "/any/repo-link", repo1, commit, testCase.Path)
|
||||||
assert.NoError(t, err, "Unable to get commit information for entries of subtree: %s in commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err)
|
assert.NoError(t, err, "Unable to get commit information for entries of subtree: %s in commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
@@ -127,7 +125,7 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
assert.Equal(t, expectedInfo.CommitID, commit.ID.String())
|
assert.Equal(t, expectedInfo.CommitID, commit.ID.String())
|
||||||
assert.Equal(t, expectedInfo.Size, entry.Size(), entry.Name())
|
assert.Equal(t, expectedInfo.Size, entry.GetSize(t.Context(), repo1), entry.Name())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,9 +153,9 @@ func TestEntries_GetCommitsInfo(t *testing.T) {
|
|||||||
t.Run("NonExistingSubmoduleAsNil", func(t *testing.T) {
|
t.Run("NonExistingSubmoduleAsNil", func(t *testing.T) {
|
||||||
commit, err := bareRepo1.GetCommit("HEAD")
|
commit, err := bareRepo1.GetCommit("HEAD")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
treeEntry, err := commit.GetTreeEntryByPath("file1.txt")
|
treeEntry, err := commit.GetTreeEntryByPath(t.Context(), bareRepo1, "file1.txt")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
cisf, err := GetCommitInfoSubmoduleFile("/any/repo-link", "file1.txt", commit, treeEntry.ID)
|
cisf, err := GetCommitInfoSubmoduleFile(t.Context(), "/any/repo-link", "file1.txt", bareRepo1, commit, treeEntry.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, &CommitSubmoduleFile{
|
assert.Equal(t, &CommitSubmoduleFile{
|
||||||
repoLink: "/any/repo-link",
|
repoLink: "/any/repo-link",
|
||||||
@@ -169,52 +167,3 @@ func TestEntries_GetCommitsInfo(t *testing.T) {
|
|||||||
assert.Nil(t, cisf.SubmoduleWebLinkTree(t.Context()))
|
assert.Nil(t, cisf.SubmoduleWebLinkTree(t.Context()))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkEntries_GetCommitsInfo(b *testing.B) {
|
|
||||||
type benchmarkType struct {
|
|
||||||
url string
|
|
||||||
name string
|
|
||||||
}
|
|
||||||
|
|
||||||
benchmarks := []benchmarkType{
|
|
||||||
{url: "https://github.com/go-gitea/gitea.git", name: "gitea"},
|
|
||||||
{url: "https://github.com/ethantkoenig/manyfiles.git", name: "manyfiles"},
|
|
||||||
{url: "https://github.com/moby/moby.git", name: "moby"},
|
|
||||||
{url: "https://github.com/golang/go.git", name: "go"},
|
|
||||||
{url: "https://github.com/torvalds/linux.git", name: "linux"},
|
|
||||||
}
|
|
||||||
|
|
||||||
doBenchmark := func(benchmark benchmarkType) {
|
|
||||||
var commit *Commit
|
|
||||||
var entries Entries
|
|
||||||
var repo *Repository
|
|
||||||
repoPath, err := cloneRepo(b, benchmark.url)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if repo, err = OpenRepository(b.Context(), repoPath); err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
defer repo.Close()
|
|
||||||
|
|
||||||
if commit, err = repo.GetBranchCommit("master"); err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
} else if entries, err = commit.Tree.ListEntries(); err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
b.ResetTimer()
|
|
||||||
b.Run(benchmark.name, func(b *testing.B) {
|
|
||||||
for b.Loop() {
|
|
||||||
_, _, err := entries.GetCommitsInfo(b.Context(), "/any/repo-link", commit, "")
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, benchmark := range benchmarks {
|
|
||||||
doBenchmark(benchmark)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,16 +10,22 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"gitea.dev/modules/charset"
|
"gitea.dev/modules/charset"
|
||||||
"gitea.dev/modules/container"
|
|
||||||
"gitea.dev/modules/util"
|
"gitea.dev/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CoAuthoredByTrailer is the canonical token for the `Co-authored-by:` git trailer.
|
// CoAuthoredByTrailer is the canonical token for the `Co-authored-by:` git trailer.
|
||||||
const CoAuthoredByTrailer = "Co-authored-by"
|
const CoAuthoredByTrailer = "Co-authored-by"
|
||||||
|
|
||||||
|
const (
|
||||||
|
commitIdentityRoleAuthor = 1
|
||||||
|
commitIdentityRoleCommitter = 2
|
||||||
|
commitIdentityRoleCoAuthor = 3
|
||||||
|
)
|
||||||
|
|
||||||
type CommitIdentity struct {
|
type CommitIdentity struct {
|
||||||
Name string
|
Name string
|
||||||
Email string
|
Email string
|
||||||
|
role int
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommitMessageTrailerValues keys are all in lower-case
|
// CommitMessageTrailerValues keys are all in lower-case
|
||||||
@@ -34,6 +40,8 @@ type CommitMessage struct {
|
|||||||
trailerValues CommitMessageTrailerValues
|
trailerValues CommitMessageTrailerValues
|
||||||
|
|
||||||
allParticipants []*CommitIdentity
|
allParticipants []*CommitIdentity
|
||||||
|
committerCoAuthorIdx int
|
||||||
|
committerCoAuthor *CommitIdentity
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CommitMessage) MessageUTF8() string {
|
func (c *CommitMessage) MessageUTF8() string {
|
||||||
@@ -105,29 +113,57 @@ func (c *Commit) AllParticipantIdentities() []*CommitIdentity {
|
|||||||
return c.allParticipants
|
return c.allParticipants
|
||||||
}
|
}
|
||||||
|
|
||||||
exclude := container.Set[string]{}
|
exclude := map[string]int{}
|
||||||
c.allParticipants = append(c.allParticipants, &CommitIdentity{Name: c.Author.Name, Email: c.Author.Email})
|
addParticipant := func(name, email string, role int) (existingRole int) {
|
||||||
exclude.Add(strings.ToLower(c.Author.Email))
|
|
||||||
|
|
||||||
addParticipant := func(name, email string) {
|
|
||||||
if name == "" && email == "" {
|
if name == "" && email == "" {
|
||||||
return
|
return 0
|
||||||
}
|
}
|
||||||
emailLower := strings.ToLower(email)
|
emailLower := strings.ToLower(email)
|
||||||
if emailLower != "" && exclude.Contains(emailLower) {
|
if existingRole = exclude[emailLower]; emailLower != "" && existingRole != 0 {
|
||||||
return
|
return existingRole
|
||||||
}
|
}
|
||||||
c.allParticipants = append(c.allParticipants, &CommitIdentity{Name: name, Email: email})
|
c.allParticipants = append(c.allParticipants, &CommitIdentity{Name: name, Email: email, role: role})
|
||||||
exclude.Add(emailLower)
|
exclude[emailLower] = role
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
addParticipant(c.Committer.Name, c.Committer.Email)
|
|
||||||
|
c.committerCoAuthorIdx = -1
|
||||||
|
addParticipant(c.Author.Name, c.Author.Email, commitIdentityRoleAuthor)
|
||||||
|
addParticipant(c.Committer.Name, c.Committer.Email, commitIdentityRoleCommitter)
|
||||||
for _, coAuthorValue := range c.MessageTrailer()["co-authored-by"] {
|
for _, coAuthorValue := range c.MessageTrailer()["co-authored-by"] {
|
||||||
addr, err := mail.ParseAddress(coAuthorValue)
|
addr, err := mail.ParseAddress(coAuthorValue)
|
||||||
|
coAuthorName, coAuthorEmail := coAuthorValue, ""
|
||||||
if err == nil {
|
if err == nil {
|
||||||
addParticipant(addr.Name, addr.Address)
|
coAuthorName, coAuthorEmail = addr.Name, addr.Address
|
||||||
} else {
|
}
|
||||||
addParticipant(coAuthorValue, "")
|
existingRole := addParticipant(coAuthorName, coAuthorEmail, commitIdentityRoleCoAuthor)
|
||||||
|
if existingRole == commitIdentityRoleCommitter && c.committerCoAuthorIdx == -1 {
|
||||||
|
c.committerCoAuthorIdx = len(c.allParticipants)
|
||||||
|
c.committerCoAuthor = &CommitIdentity{coAuthorName, coAuthorEmail, commitIdentityRoleCoAuthor}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return c.allParticipants
|
return c.allParticipants
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CoAuthorIdentities returns co-author identities defined by "Co-authored-by:" in the git message trailer
|
||||||
|
// Only the commit's author is excluded. If committer is declared as co-author, it will be included in the result.
|
||||||
|
// * Author & Co-author: they changed the code (attribution)
|
||||||
|
// * Committer: they submitted the commit but didn't change the code (e.g.: maintainer signed a commit)
|
||||||
|
// So, a committer can also be a co-author if they changed the code.
|
||||||
|
func (c *Commit) CoAuthorIdentities() (coAuthors []*CommitIdentity) {
|
||||||
|
all := c.AllParticipantIdentities()
|
||||||
|
if len(all) <= 1 {
|
||||||
|
return nil // no co-author list
|
||||||
|
}
|
||||||
|
if all[1].role != commitIdentityRoleCommitter {
|
||||||
|
return all[1:] // no committer, so all after author are co-authors
|
||||||
|
}
|
||||||
|
if c.committerCoAuthorIdx == -1 {
|
||||||
|
return all[2:] // the committer is not in the co-author list, so just return the co-author list
|
||||||
|
}
|
||||||
|
// the committer is in the co-author list but de-duplicated, so include them as co-author again
|
||||||
|
coAuthors = append(coAuthors, all[2:c.committerCoAuthorIdx]...)
|
||||||
|
coAuthors = append(coAuthors, c.committerCoAuthor)
|
||||||
|
coAuthors = append(coAuthors, all[c.committerCoAuthorIdx:]...)
|
||||||
|
return coAuthors
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,36 +47,83 @@ func TestCommitMessageTrailer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCommitMessageAllParticipantIdentities(t *testing.T) {
|
func TestCommitMessageParticipants(t *testing.T) {
|
||||||
sig := func(n, e string) *Signature { return &Signature{Name: n, Email: e} }
|
sig := func(n, e string) *Signature { return &Signature{Name: n, Email: e} }
|
||||||
idt := func(n, e string) *CommitIdentity { return &CommitIdentity{Name: n, Email: e} }
|
idt := func(n, e string, r int) *CommitIdentity { return &CommitIdentity{n, e, r} }
|
||||||
cases := []struct {
|
roleAuthor, roleCommitter, roleCoAuthor := commitIdentityRoleAuthor, commitIdentityRoleCommitter, commitIdentityRoleCoAuthor
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
commit *Commit
|
commit *Commit
|
||||||
participant []*CommitIdentity
|
identities []*CommitIdentity
|
||||||
}{
|
}
|
||||||
|
t.Run("AllParticipants", func(t *testing.T) {
|
||||||
|
cases := []testCase{
|
||||||
{
|
{
|
||||||
|
"DifferentUsers",
|
||||||
&Commit{
|
&Commit{
|
||||||
Author: sig("a", "a@m.com"), Committer: sig("c", "c@m.com"),
|
Author: sig("a", "a@m.com"), Committer: sig("c", "c@m.com"),
|
||||||
CommitMessage: CommitMessage{MessageRaw: "CO-Authored-BY: x@m.com"},
|
CommitMessage: CommitMessage{MessageRaw: "CO-Authored-BY: x@m.com"},
|
||||||
},
|
},
|
||||||
[]*CommitIdentity{idt("a", "a@m.com"), idt("c", "c@m.com"), idt("", "x@m.com")},
|
[]*CommitIdentity{idt("a", "a@m.com", roleAuthor), idt("c", "c@m.com", roleCommitter), idt("", "x@m.com", roleCoAuthor)},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"SameUser",
|
||||||
&Commit{
|
&Commit{
|
||||||
Author: sig("a", "a@m.com"), Committer: sig("a", "A@M.com"),
|
Author: sig("a", "a@m.com"), Committer: sig("a", "A@M.com"),
|
||||||
CommitMessage: CommitMessage{MessageRaw: "CO-Authored-BY: a@m.com"},
|
CommitMessage: CommitMessage{MessageRaw: "CO-Authored-BY: a@m.com"},
|
||||||
},
|
},
|
||||||
[]*CommitIdentity{idt("a", "a@m.com")},
|
[]*CommitIdentity{idt("a", "a@m.com", roleAuthor)},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"NoCommitter",
|
||||||
&Commit{
|
&Commit{
|
||||||
Author: sig("a", "a@m.com"), Committer: sig("", ""),
|
Author: sig("a", "a@m.com"), Committer: sig("", ""),
|
||||||
CommitMessage: CommitMessage{MessageRaw: "Co-authored-by: Full Name <X@M.com>"},
|
CommitMessage: CommitMessage{MessageRaw: "Co-authored-by: Full Name <X@M.com>"},
|
||||||
},
|
},
|
||||||
[]*CommitIdentity{idt("a", "a@m.com"), idt("Full Name", "X@M.com")},
|
[]*CommitIdentity{idt("a", "a@m.com", roleAuthor), idt("Full Name", "X@M.com", roleCoAuthor)},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
assert.Equal(t, c.participant, c.commit.AllParticipantIdentities())
|
assert.Equal(t, c.identities, c.commit.AllParticipantIdentities(), "case: %s", c.name)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
t.Run("CoAuthors", func(t *testing.T) {
|
||||||
|
cases := []testCase{
|
||||||
|
{
|
||||||
|
"GenuineCoAuthor",
|
||||||
|
&Commit{
|
||||||
|
Author: sig("a", "a@m.com"), Committer: sig("c", "c@m.com"),
|
||||||
|
CommitMessage: CommitMessage{MessageRaw: "Co-authored-by: x <x@m.com>"},
|
||||||
|
},
|
||||||
|
[]*CommitIdentity{idt("x", "x@m.com", roleCoAuthor)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CoAuthorIsCommitter",
|
||||||
|
&Commit{
|
||||||
|
Author: sig("a", "a@m.com"), Committer: sig("c", "c@m.com"),
|
||||||
|
CommitMessage: CommitMessage{MessageRaw: "Co-authored-by: c <c@m.com>"},
|
||||||
|
},
|
||||||
|
[]*CommitIdentity{idt("c", "c@m.com", roleCoAuthor)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CoAuthorIsAuthor",
|
||||||
|
&Commit{
|
||||||
|
Author: sig("a", "a@m.com"), Committer: sig("c", "c@m.com"),
|
||||||
|
CommitMessage: CommitMessage{MessageRaw: "Co-authored-by: a <a@m.com>"},
|
||||||
|
},
|
||||||
|
[]*CommitIdentity{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CoAuthorCommitterNameWithIndex", // restore the committer co-author to the co-author list by the index with correct name
|
||||||
|
&Commit{
|
||||||
|
Author: sig("a", "a@m.com"), Committer: sig("c", "c@m.com"),
|
||||||
|
CommitMessage: CommitMessage{MessageRaw: "Co-authored-by: x <x@m.com>\nCo-authored-by: c-other <c@m.com>\nCo-authored-by: y <y@m.com>"},
|
||||||
|
},
|
||||||
|
[]*CommitIdentity{idt("x", "x@m.com", roleCoAuthor), idt("c-other", "c@m.com", roleCoAuthor), idt("y", "y@m.com", roleCoAuthor)},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
assert.Equal(t, c.identities, c.commit.CoAuthorIdentities(), "case: %s", c.name)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const (
|
|||||||
commitHeaderGpgsigSha256 = "gpgsig-sha256"
|
commitHeaderGpgsigSha256 = "gpgsig-sha256"
|
||||||
)
|
)
|
||||||
|
|
||||||
func assignCommitFields(gitRepo *Repository, commit *Commit, headerKey string, headerValue []byte) error {
|
func assignCommitFields(commit *Commit, headerKey string, headerValue []byte) error {
|
||||||
if len(headerValue) > 0 && headerValue[len(headerValue)-1] == '\n' {
|
if len(headerValue) > 0 && headerValue[len(headerValue)-1] == '\n' {
|
||||||
headerValue = headerValue[:len(headerValue)-1] // remove trailing newline
|
headerValue = headerValue[:len(headerValue)-1] // remove trailing newline
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ func assignCommitFields(gitRepo *Repository, commit *Commit, headerKey string, h
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid tree ID %q: %w", string(headerValue), err)
|
return fmt.Errorf("invalid tree ID %q: %w", string(headerValue), err)
|
||||||
}
|
}
|
||||||
commit.Tree = *NewTree(gitRepo, objID)
|
commit.TreeID = objID
|
||||||
case "parent":
|
case "parent":
|
||||||
objID, err := NewIDFromString(string(headerValue))
|
objID, err := NewIDFromString(string(headerValue))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -48,7 +48,7 @@ func assignCommitFields(gitRepo *Repository, commit *Commit, headerKey string, h
|
|||||||
// We need this to interpret commits from cat-file or cat-file --batch
|
// We need this to interpret commits from cat-file or cat-file --batch
|
||||||
//
|
//
|
||||||
// If used as part of a cat-file --batch stream you need to limit the reader to the correct size
|
// If used as part of a cat-file --batch stream you need to limit the reader to the correct size
|
||||||
func CommitFromReader(gitRepo *Repository, objectID ObjectID, reader io.Reader) (*Commit, error) {
|
func CommitFromReader(objectID ObjectID, reader io.Reader) (*Commit, error) {
|
||||||
commit := &Commit{
|
commit := &Commit{
|
||||||
ID: objectID,
|
ID: objectID,
|
||||||
Author: &Signature{},
|
Author: &Signature{},
|
||||||
@@ -74,7 +74,7 @@ func CommitFromReader(gitRepo *Repository, objectID ObjectID, reader io.Reader)
|
|||||||
k, v, _ := bytes.Cut(line, []byte{' '})
|
k, v, _ := bytes.Cut(line, []byte{' '})
|
||||||
if len(k) != 0 || !inHeader {
|
if len(k) != 0 || !inHeader {
|
||||||
if headerKey != "" {
|
if headerKey != "" {
|
||||||
if err = assignCommitFields(gitRepo, commit, headerKey, headerValue); err != nil {
|
if err = assignCommitFields(commit, headerKey, headerValue); err != nil {
|
||||||
return nil, fmt.Errorf("unable to parse commit %q: %w", objectID.String(), err)
|
return nil, fmt.Errorf("unable to parse commit %q: %w", objectID.String(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ signed commit`
|
|||||||
assert.NotNil(t, gitRepo)
|
assert.NotNil(t, gitRepo)
|
||||||
defer gitRepo.Close()
|
defer gitRepo.Close()
|
||||||
|
|
||||||
commitFromReader, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString))
|
commitFromReader, err := CommitFromReader(sha, strings.NewReader(commitString))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
require.NotNil(t, commitFromReader)
|
require.NotNil(t, commitFromReader)
|
||||||
assert.EqualValues(t, sha, commitFromReader.ID)
|
assert.EqualValues(t, sha, commitFromReader.ID)
|
||||||
@@ -93,7 +93,7 @@ committer Adam Majer <amajer@suse.de> 1698676906 +0100
|
|||||||
signed commit`, commitFromReader.Signature.Payload)
|
signed commit`, commitFromReader.Signature.Payload)
|
||||||
assert.Equal(t, "Adam Majer <amajer@suse.de>", commitFromReader.Author.String())
|
assert.Equal(t, "Adam Majer <amajer@suse.de>", commitFromReader.Author.String())
|
||||||
|
|
||||||
commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n"))
|
commitFromReader2, err := CommitFromReader(sha, strings.NewReader(commitString+"\n\n"))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
commitFromReader.CommitMessage.MessageRaw += "\n\n"
|
commitFromReader.CommitMessage.MessageRaw += "\n\n"
|
||||||
commitFromReader.Signature.Payload += "\n\n"
|
commitFromReader.Signature.Payload += "\n\n"
|
||||||
@@ -118,15 +118,15 @@ func TestHasPreviousCommitSha256(t *testing.T) {
|
|||||||
assert.Equal(t, objectFormat, parentSHA.Type())
|
assert.Equal(t, objectFormat, parentSHA.Type())
|
||||||
assert.Equal(t, "sha256", objectFormat.Name())
|
assert.Equal(t, "sha256", objectFormat.Name())
|
||||||
|
|
||||||
haz, err := commit.HasPreviousCommit(parentSHA)
|
haz, err := commit.HasPreviousCommit(t.Context(), repo, parentSHA)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, haz)
|
assert.True(t, haz)
|
||||||
|
|
||||||
hazNot, err := commit.HasPreviousCommit(notParentSHA)
|
hazNot, err := commit.HasPreviousCommit(t.Context(), repo, notParentSHA)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.False(t, hazNot)
|
assert.False(t, hazNot)
|
||||||
|
|
||||||
selfNot, err := commit.HasPreviousCommit(commit.ID)
|
selfNot, err := commit.HasPreviousCommit(t.Context(), repo, commit.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.False(t, selfNot)
|
assert.False(t, selfNot)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,19 @@
|
|||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
type SubmoduleWebLink struct {
|
type SubmoduleWebLink struct {
|
||||||
RepoWebLink, CommitWebLink string
|
RepoWebLink, CommitWebLink string
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSubModules get all the submodules of current revision git tree
|
// GetSubModules get all the submodules of current revision git tree
|
||||||
func (c *Commit) GetSubModules() (*ObjectCache[*SubModule], error) {
|
func (c *Commit) GetSubModules(ctx context.Context, gitRepo *Repository) (*ObjectCache[*SubModule], error) {
|
||||||
if c.submoduleCache != nil {
|
if c.submoduleCache != nil {
|
||||||
return c.submoduleCache, nil
|
return c.submoduleCache, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
entry, err := c.GetTreeEntryByPath(".gitmodules")
|
entry, err := c.GetTreeEntryByPath(ctx, gitRepo, ".gitmodules")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(ErrNotExist); ok {
|
if _, ok := err.(ErrNotExist); ok {
|
||||||
return nil, nil //nolint:nilnil // return nil to indicate that the submodule does not exist
|
return nil, nil //nolint:nilnil // return nil to indicate that the submodule does not exist
|
||||||
@@ -21,7 +23,7 @@ func (c *Commit) GetSubModules() (*ObjectCache[*SubModule], error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rd, err := entry.Blob().DataAsync()
|
rd, err := entry.Blob(gitRepo).DataAsync()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -37,8 +39,8 @@ func (c *Commit) GetSubModules() (*ObjectCache[*SubModule], error) {
|
|||||||
|
|
||||||
// GetSubModule gets the submodule by the entry name.
|
// GetSubModule gets the submodule by the entry name.
|
||||||
// It returns "nil, nil" if the submodule does not exist, caller should always remember to check the "nil"
|
// It returns "nil, nil" if the submodule does not exist, caller should always remember to check the "nil"
|
||||||
func (c *Commit) GetSubModule(entryName string) (*SubModule, error) {
|
func (c *Commit) GetSubModule(ctx context.Context, gitRepo *Repository, entryName string) (*SubModule, error) {
|
||||||
modules, err := c.GetSubModules()
|
modules, err := c.GetSubModules(ctx, gitRepo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ empty commit`
|
|||||||
assert.NotNil(t, gitRepo)
|
assert.NotNil(t, gitRepo)
|
||||||
defer gitRepo.Close()
|
defer gitRepo.Close()
|
||||||
|
|
||||||
commitFromReader, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString))
|
commitFromReader, err := CommitFromReader(sha, strings.NewReader(commitString))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
require.NotNil(t, commitFromReader)
|
require.NotNil(t, commitFromReader)
|
||||||
assert.EqualValues(t, sha, commitFromReader.ID)
|
assert.EqualValues(t, sha, commitFromReader.ID)
|
||||||
@@ -89,7 +89,7 @@ committer silverwind <me@silverwind.io> 1563741793 +0200
|
|||||||
empty commit`, commitFromReader.Signature.Payload)
|
empty commit`, commitFromReader.Signature.Payload)
|
||||||
assert.Equal(t, "silverwind <me@silverwind.io>", commitFromReader.Author.String())
|
assert.Equal(t, "silverwind <me@silverwind.io>", commitFromReader.Author.String())
|
||||||
|
|
||||||
commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n"))
|
commitFromReader2, err := CommitFromReader(sha, strings.NewReader(commitString+"\n\n"))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
commitFromReader.CommitMessage.MessageRaw += "\n\n"
|
commitFromReader.CommitMessage.MessageRaw += "\n\n"
|
||||||
commitFromReader.Signature.Payload += "\n\n"
|
commitFromReader.Signature.Payload += "\n\n"
|
||||||
@@ -125,7 +125,7 @@ ISO-8859-1`
|
|||||||
assert.NotNil(t, gitRepo)
|
assert.NotNil(t, gitRepo)
|
||||||
defer gitRepo.Close()
|
defer gitRepo.Close()
|
||||||
|
|
||||||
commitFromReader, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString))
|
commitFromReader, err := CommitFromReader(sha, strings.NewReader(commitString))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
require.NotNil(t, commitFromReader)
|
require.NotNil(t, commitFromReader)
|
||||||
assert.EqualValues(t, sha, commitFromReader.ID)
|
assert.EqualValues(t, sha, commitFromReader.ID)
|
||||||
@@ -152,7 +152,7 @@ encoding ISO-8859-1
|
|||||||
ISO-8859-1`, commitFromReader.Signature.Payload)
|
ISO-8859-1`, commitFromReader.Signature.Payload)
|
||||||
assert.Equal(t, "KN4CK3R <admin@oldschoolhack.me>", commitFromReader.Author.String())
|
assert.Equal(t, "KN4CK3R <admin@oldschoolhack.me>", commitFromReader.Author.String())
|
||||||
|
|
||||||
commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n"))
|
commitFromReader2, err := CommitFromReader(sha, strings.NewReader(commitString+"\n\n"))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
commitFromReader.CommitMessage.MessageRaw += "\n\n"
|
commitFromReader.CommitMessage.MessageRaw += "\n\n"
|
||||||
commitFromReader.Signature.Payload += "\n\n"
|
commitFromReader.Signature.Payload += "\n\n"
|
||||||
@@ -172,15 +172,15 @@ func TestHasPreviousCommit(t *testing.T) {
|
|||||||
parentSHA := MustIDFromString("8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2")
|
parentSHA := MustIDFromString("8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2")
|
||||||
notParentSHA := MustIDFromString("2839944139e0de9737a044f78b0e4b40d989a9e3")
|
notParentSHA := MustIDFromString("2839944139e0de9737a044f78b0e4b40d989a9e3")
|
||||||
|
|
||||||
haz, err := commit.HasPreviousCommit(parentSHA)
|
haz, err := commit.HasPreviousCommit(t.Context(), repo, parentSHA)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, haz)
|
assert.True(t, haz)
|
||||||
|
|
||||||
hazNot, err := commit.HasPreviousCommit(notParentSHA)
|
hazNot, err := commit.HasPreviousCommit(t.Context(), repo, notParentSHA)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.False(t, hazNot)
|
assert.False(t, hazNot)
|
||||||
|
|
||||||
selfNot, err := commit.HasPreviousCommit(commit.ID)
|
selfNot, err := commit.HasPreviousCommit(t.Context(), repo, commit.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.False(t, selfNot)
|
assert.False(t, selfNot)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func getRepoRawDiffForFileCmd(_ context.Context, repo *Repository, startCommit,
|
|||||||
} else if commit.ParentCount() == 0 {
|
} else if commit.ParentCount() == 0 {
|
||||||
cmd.AddArguments("show").AddDynamicArguments(endCommit).AddDashesAndList(files...)
|
cmd.AddArguments("show").AddDynamicArguments(endCommit).AddDashesAndList(files...)
|
||||||
} else {
|
} else {
|
||||||
c, err := commit.Parent(0)
|
c, err := commit.Parent(repo, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,7 @@ func getRepoRawDiffForFileCmd(_ context.Context, repo *Repository, startCommit,
|
|||||||
} else if commit.ParentCount() == 0 {
|
} else if commit.ParentCount() == 0 {
|
||||||
cmd.AddArguments("format-patch", "--no-signature", "--stdout", "--root").AddDynamicArguments(endCommit).AddDashesAndList(files...)
|
cmd.AddArguments("format-patch", "--no-signature", "--stdout", "--root").AddDynamicArguments(endCommit).AddDashesAndList(files...)
|
||||||
} else {
|
} else {
|
||||||
c, err := commit.Parent(0)
|
c, err := commit.Parent(repo, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ package languagestats
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"gitea.dev/modules/analyze"
|
"gitea.dev/modules/analyze"
|
||||||
@@ -21,7 +22,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// GetLanguageStats calculates language stats for git repository at specified commit
|
// GetLanguageStats calculates language stats for git repository at specified commit
|
||||||
func GetLanguageStats(repo *git_module.Repository, commitID string) (map[string]int64, error) {
|
func GetLanguageStats(_ context.Context, repo *git_module.Repository, commitID string) (map[string]int64, error) {
|
||||||
r, err := git.PlainOpen(repo.Path)
|
r, err := git.PlainOpen(repo.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ package languagestats
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"gitea.dev/modules/analyze"
|
"gitea.dev/modules/analyze"
|
||||||
@@ -19,10 +20,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// GetLanguageStats calculates language stats for git repository at specified commit
|
// GetLanguageStats calculates language stats for git repository at specified commit
|
||||||
func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64, error) {
|
func GetLanguageStats(ctx context.Context, repo *git.Repository, commitID string) (map[string]int64, error) {
|
||||||
// We will feed the commit IDs in order into cat-file --batch, followed by blobs as necessary.
|
// We will feed the commit IDs in order into cat-file --batch, followed by blobs as necessary.
|
||||||
// so let's create a batch stdin and stdout
|
// so let's create a batch stdin and stdout
|
||||||
batch, cancel, err := repo.CatFileBatch(repo.Ctx)
|
batch, cancel, err := repo.CatFileBatch(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -43,7 +44,7 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64,
|
|||||||
return nil, git.ErrNotExist{ID: commitID}
|
return nil, git.ErrNotExist{ID: commitID}
|
||||||
}
|
}
|
||||||
|
|
||||||
commit, err := git.CommitFromReader(repo, sha, io.LimitReader(batchReader, commitInfo.Size))
|
commit, err := git.CommitFromReader(sha, io.LimitReader(batchReader, commitInfo.Size))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("Unable to get commit for: %s. Err: %v", commitID, err)
|
log.Debug("Unable to get commit for: %s. Err: %v", commitID, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -52,9 +53,7 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tree := commit.Tree
|
entries, err := commit.Tree().ListEntriesRecursiveWithSize(ctx, repo)
|
||||||
|
|
||||||
entries, err := tree.ListEntriesRecursiveWithSize()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -81,13 +80,15 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64,
|
|||||||
select {
|
select {
|
||||||
case <-repo.Ctx.Done():
|
case <-repo.Ctx.Done():
|
||||||
return sizes, repo.Ctx.Err()
|
return sizes, repo.Ctx.Err()
|
||||||
|
case <-ctx.Done():
|
||||||
|
return sizes, ctx.Err()
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
contentBuf.Reset()
|
contentBuf.Reset()
|
||||||
content = contentBuf.Bytes()
|
content = contentBuf.Bytes()
|
||||||
|
|
||||||
if f.Size() == 0 {
|
if f.GetSize(ctx, repo) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +125,7 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// this language will always be added to the size
|
// this language will always be added to the size
|
||||||
sizes[language] += f.Size()
|
sizes[language] += f.GetSize(ctx, repo)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,7 +139,7 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64,
|
|||||||
|
|
||||||
// If content can not be read or file is too big just do detection by filename
|
// If content can not be read or file is too big just do detection by filename
|
||||||
|
|
||||||
if f.Size() <= bigFileSize {
|
if f.GetSize(ctx, repo) <= bigFileSize {
|
||||||
info, _, err := batch.QueryContent(f.ID.String())
|
info, _, err := batch.QueryContent(f.ID.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -192,10 +193,10 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64,
|
|||||||
includedLanguage[language] = included
|
includedLanguage[language] = included
|
||||||
}
|
}
|
||||||
if included || isDetectable.ValueOrDefault(false) {
|
if included || isDetectable.ValueOrDefault(false) {
|
||||||
sizes[language] += f.Size()
|
sizes[language] += f.GetSize(ctx, repo)
|
||||||
} else if len(sizes) == 0 && (firstExcludedLanguage == "" || firstExcludedLanguage == language) {
|
} else if len(sizes) == 0 && (firstExcludedLanguage == "" || firstExcludedLanguage == language) {
|
||||||
firstExcludedLanguage = language
|
firstExcludedLanguage = language
|
||||||
firstExcludedLanguageSize += f.Size()
|
firstExcludedLanguageSize += f.GetSize(ctx, repo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func TestRepository_GetLanguageStats(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer gitRepo.Close()
|
defer gitRepo.Close()
|
||||||
|
|
||||||
stats, err := GetLanguageStats(gitRepo, "8fee858da5796dfb37704761701bb8e800ad9ef3")
|
stats, err := GetLanguageStats(t.Context(), gitRepo, "8fee858da5796dfb37704761701bb8e800ad9ef3")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, map[string]int64{
|
assert.Equal(t, map[string]int64{
|
||||||
|
|||||||
@@ -13,26 +13,26 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// CacheCommit will cache the commit from the gitRepository
|
// CacheCommit will cache the commit from the gitRepository
|
||||||
func (c *Commit) CacheCommit(ctx context.Context) error {
|
func (c *Commit) CacheCommit(ctx context.Context, gitRepo *Repository) error {
|
||||||
if c.repo.LastCommitCache == nil {
|
if gitRepo.LastCommitCache == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
commitNodeIndex, _ := c.repo.CommitNodeIndex()
|
commitNodeIndex, _ := gitRepo.CommitNodeIndex()
|
||||||
|
|
||||||
index, err := commitNodeIndex.Get(plumbing.Hash(c.ID.RawValue()))
|
index, err := commitNodeIndex.Get(plumbing.Hash(c.ID.RawValue()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.recursiveCache(ctx, index, &c.Tree, "", 1)
|
return c.recursiveCache(ctx, gitRepo, index, c.Tree(), "", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commit) recursiveCache(ctx context.Context, index cgobject.CommitNode, tree *Tree, treePath string, level int) error {
|
func (c *Commit) recursiveCache(ctx context.Context, gitRepo *Repository, index cgobject.CommitNode, tree *Tree, treePath string, level int) error {
|
||||||
if level == 0 {
|
if level == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, err := tree.ListEntries()
|
entries, err := tree.ListEntries(ctx, gitRepo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -44,18 +44,18 @@ func (c *Commit) recursiveCache(ctx context.Context, index cgobject.CommitNode,
|
|||||||
entryMap[entry.Name()] = entry
|
entryMap[entry.Name()] = entry
|
||||||
}
|
}
|
||||||
|
|
||||||
commits, err := GetLastCommitForPaths(ctx, c.repo.LastCommitCache, index, treePath, entryPaths)
|
commits, err := getLastCommitForPathsByCommitNode(ctx, gitRepo, index, treePath, entryPaths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for entry := range commits {
|
for entry := range commits {
|
||||||
if entryMap[entry].IsDir() {
|
if entryMap[entry].IsDir() {
|
||||||
subTree, err := tree.SubTree(entry)
|
subTree, err := tree.SubTree(ctx, gitRepo, entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := c.recursiveCache(ctx, index, subTree, entry, level-1); err != nil {
|
if err := c.recursiveCache(ctx, gitRepo, index, subTree, entry, level-1); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,19 +10,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// CacheCommit will cache the commit from the gitRepository
|
// CacheCommit will cache the commit from the gitRepository
|
||||||
func (c *Commit) CacheCommit(ctx context.Context) error {
|
func (c *Commit) CacheCommit(ctx context.Context, gitRepo *Repository) error {
|
||||||
if c.repo.LastCommitCache == nil {
|
if gitRepo.LastCommitCache == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return c.recursiveCache(ctx, &c.Tree, "", 1)
|
return c.recursiveCache(ctx, gitRepo, c.Tree(), "", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commit) recursiveCache(ctx context.Context, tree *Tree, treePath string, level int) error {
|
func (c *Commit) recursiveCache(ctx context.Context, gitRepo *Repository, tree *Tree, treePath string, level int) error {
|
||||||
if level == 0 {
|
if level == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
entries, err := tree.ListEntries(ctx, gitRepo)
|
||||||
entries, err := tree.ListEntries()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -32,7 +31,7 @@ func (c *Commit) recursiveCache(ctx context.Context, tree *Tree, treePath string
|
|||||||
entryPaths[i] = entry.Name()
|
entryPaths[i] = entry.Name()
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = walkGitLog(ctx, c.repo, c, treePath, entryPaths...)
|
_, err = walkGitLog(ctx, gitRepo, c, treePath, entryPaths...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -40,11 +39,11 @@ func (c *Commit) recursiveCache(ctx context.Context, tree *Tree, treePath string
|
|||||||
for _, treeEntry := range entries {
|
for _, treeEntry := range entries {
|
||||||
// entryMap won't contain "" therefore skip this.
|
// entryMap won't contain "" therefore skip this.
|
||||||
if treeEntry.IsDir() {
|
if treeEntry.IsDir() {
|
||||||
subTree, err := tree.SubTree(treeEntry.Name())
|
subTree, err := tree.SubTree(ctx, gitRepo, treeEntry.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := c.recursiveCache(ctx, subTree, treeEntry.Name(), level-1); err != nil {
|
if err := c.recursiveCache(ctx, gitRepo, subTree, treeEntry.Name(), level-1); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,13 +263,12 @@ var walkGitLogDebugBeforeNext func() // is used to simulate various edge git pro
|
|||||||
// walkGitLog walks the git log --name-status for the head commit in the provided treepath and files
|
// walkGitLog walks the git log --name-status for the head commit in the provided treepath and files
|
||||||
func walkGitLog(ctx context.Context, repo *Repository, head *Commit, treepath string, paths ...string) (map[string]string, error) {
|
func walkGitLog(ctx context.Context, repo *Repository, head *Commit, treepath string, paths ...string) (map[string]string, error) {
|
||||||
headRef := head.ID.String()
|
headRef := head.ID.String()
|
||||||
|
tree, err := head.SubTree(ctx, repo, treepath)
|
||||||
tree, err := head.SubTree(treepath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, err := tree.ListEntries()
|
entries, err := tree.ListEntries(ctx, repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.dev/modules/log"
|
||||||
|
)
|
||||||
|
|
||||||
// NotesRef is the git ref where Gitea will look for git-notes data.
|
// NotesRef is the git ref where Gitea will look for git-notes data.
|
||||||
// The value ("refs/notes/commits") is the default ref used by git-notes.
|
// The value ("refs/notes/commits") is the default ref used by git-notes.
|
||||||
const NotesRef = "refs/notes/commits"
|
const NotesRef = "refs/notes/commits"
|
||||||
@@ -12,3 +20,80 @@ type Note struct {
|
|||||||
Message []byte
|
Message []byte
|
||||||
Commit *Commit
|
Commit *Commit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetNote retrieves the git-notes data for a given commit.
|
||||||
|
// FIXME: Add LastCommitCache support
|
||||||
|
func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) error {
|
||||||
|
log.Trace("Searching for git note corresponding to the commit %q in the repository %q", commitID, repo.Path)
|
||||||
|
notes, err := repo.GetCommit(NotesRef)
|
||||||
|
if err != nil {
|
||||||
|
if IsErrNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Error("Unable to get commit from ref %q. Error: %v", NotesRef, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
path := ""
|
||||||
|
|
||||||
|
tree := notes.Tree()
|
||||||
|
log.Trace("Found tree with ID %q while searching for git note corresponding to the commit %q", tree.ID, commitID)
|
||||||
|
|
||||||
|
var entry *TreeEntry
|
||||||
|
originalCommitID := commitID
|
||||||
|
for len(commitID) > 2 {
|
||||||
|
entry, err = tree.GetTreeEntryByPath(ctx, repo, commitID)
|
||||||
|
if err == nil {
|
||||||
|
path += commitID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if IsErrNotExist(err) {
|
||||||
|
tree, err = tree.SubTree(ctx, repo, commitID[0:2])
|
||||||
|
path += commitID[0:2] + "/"
|
||||||
|
commitID = commitID[2:]
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
// Err may have been updated by the SubTree we need to recheck if it's again an ErrNotExist
|
||||||
|
if !IsErrNotExist(err) {
|
||||||
|
log.Error("Unable to find git note corresponding to the commit %q. Error: %v", originalCommitID, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blob := entry.Blob(repo)
|
||||||
|
dataRc, err := blob.DataAsync()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
closed := false
|
||||||
|
defer func() {
|
||||||
|
if !closed {
|
||||||
|
_ = dataRc.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
d, err := io.ReadAll(dataRc)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = dataRc.Close()
|
||||||
|
closed = true
|
||||||
|
note.Message = d
|
||||||
|
|
||||||
|
treePath := ""
|
||||||
|
if idx := strings.LastIndex(path, "/"); idx > -1 {
|
||||||
|
treePath = path[:idx]
|
||||||
|
path = path[idx+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
lastCommits, err := GetLastCommitForPaths(ctx, repo, notes, treePath, []string{path})
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to get the commit for the path %q. Error: %v", treePath, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
note.Commit = lastCommits[path]
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build gogit
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gitea.dev/modules/log"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetNote retrieves the git-notes data for a given commit.
|
|
||||||
// FIXME: Add LastCommitCache support
|
|
||||||
func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) error {
|
|
||||||
log.Trace("Searching for git note corresponding to the commit %q in the repository %q", commitID, repo.Path)
|
|
||||||
notes, err := repo.GetCommit(NotesRef)
|
|
||||||
if err != nil {
|
|
||||||
if IsErrNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Error("Unable to get commit from ref %q. Error: %v", NotesRef, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
remainingCommitID := commitID
|
|
||||||
var path strings.Builder
|
|
||||||
currentTree, err := notes.Tree.gogitTreeObject()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to get tree object for notes commit %q: %w", notes.ID.String(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Trace("Found tree with ID %q while searching for git note corresponding to the commit %q", currentTree.Entries[0].Name, commitID)
|
|
||||||
var file *object.File
|
|
||||||
for len(remainingCommitID) > 2 {
|
|
||||||
file, err = currentTree.File(remainingCommitID)
|
|
||||||
if err == nil {
|
|
||||||
path.WriteString(remainingCommitID)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err == object.ErrFileNotFound {
|
|
||||||
currentTree, err = currentTree.Tree(remainingCommitID[0:2])
|
|
||||||
path.WriteString(remainingCommitID[0:2] + "/")
|
|
||||||
remainingCommitID = remainingCommitID[2:]
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
if err == object.ErrDirectoryNotFound {
|
|
||||||
return ErrNotExist{ID: remainingCommitID, RelPath: path.String()}
|
|
||||||
}
|
|
||||||
log.Error("Unable to find git note corresponding to the commit %q. Error: %v", commitID, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
blob := file.Blob
|
|
||||||
dataRc, err := blob.Reader()
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer dataRc.Close()
|
|
||||||
d, err := io.ReadAll(dataRc)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
note.Message = d
|
|
||||||
|
|
||||||
commitNodeIndex, commitGraphFile := repo.CommitNodeIndex()
|
|
||||||
if commitGraphFile != nil {
|
|
||||||
defer commitGraphFile.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
commitNode, err := commitNodeIndex.Get(plumbing.Hash(notes.ID.RawValue()))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
lastCommits, err := GetLastCommitForPaths(ctx, nil, commitNode, "", []string{path.String()})
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to get the commit for the path %q. Error: %v", path.String(), err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
note.Commit = lastCommits[path.String()]
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gitea.dev/modules/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetNote retrieves the git-notes data for a given commit.
|
|
||||||
// FIXME: Add LastCommitCache support
|
|
||||||
func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) error {
|
|
||||||
log.Trace("Searching for git note corresponding to the commit %q in the repository %q", commitID, repo.Path)
|
|
||||||
notes, err := repo.GetCommit(NotesRef)
|
|
||||||
if err != nil {
|
|
||||||
if IsErrNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Error("Unable to get commit from ref %q. Error: %v", NotesRef, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
path := ""
|
|
||||||
|
|
||||||
tree := ¬es.Tree
|
|
||||||
log.Trace("Found tree with ID %q while searching for git note corresponding to the commit %q", tree.ID, commitID)
|
|
||||||
|
|
||||||
var entry *TreeEntry
|
|
||||||
originalCommitID := commitID
|
|
||||||
for len(commitID) > 2 {
|
|
||||||
entry, err = tree.GetTreeEntryByPath(commitID)
|
|
||||||
if err == nil {
|
|
||||||
path += commitID
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if IsErrNotExist(err) {
|
|
||||||
tree, err = tree.SubTree(commitID[0:2])
|
|
||||||
path += commitID[0:2] + "/"
|
|
||||||
commitID = commitID[2:]
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
// Err may have been updated by the SubTree we need to recheck if it's again an ErrNotExist
|
|
||||||
if !IsErrNotExist(err) {
|
|
||||||
log.Error("Unable to find git note corresponding to the commit %q. Error: %v", originalCommitID, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
blob := entry.Blob()
|
|
||||||
dataRc, err := blob.DataAsync()
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
closed := false
|
|
||||||
defer func() {
|
|
||||||
if !closed {
|
|
||||||
_ = dataRc.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
d, err := io.ReadAll(dataRc)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_ = dataRc.Close()
|
|
||||||
closed = true
|
|
||||||
note.Message = d
|
|
||||||
|
|
||||||
treePath := ""
|
|
||||||
if idx := strings.LastIndex(path, "/"); idx > -1 {
|
|
||||||
treePath = path[:idx]
|
|
||||||
path = path[idx+1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
lastCommits, err := GetLastCommitForPaths(ctx, notes, treePath, []string{path})
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to get the commit for the path %q. Error: %v", treePath, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
note.Commit = lastCommits[path]
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -10,10 +10,10 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// sha1Pattern can be used to determine if a string is an valid sha
|
// sha1Pattern can be used to determine if a string is a valid sha
|
||||||
var sha1Pattern = regexp.MustCompile(`^[0-9a-f]{4,40}$`)
|
var sha1Pattern = regexp.MustCompile(`^[0-9a-f]{4,40}$`)
|
||||||
|
|
||||||
// sha256Pattern can be used to determine if a string is an valid sha
|
// sha256Pattern can be used to determine if a string is a valid sha
|
||||||
var sha256Pattern = regexp.MustCompile(`^[0-9a-f]{4,64}$`)
|
var sha256Pattern = regexp.MustCompile(`^[0-9a-f]{4,64}$`)
|
||||||
|
|
||||||
type ObjectFormat interface {
|
type ObjectFormat interface {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ func findLFSFileFunc(repo *git.Repository, objectID git.ObjectID, revListReader
|
|||||||
continue
|
continue
|
||||||
case "commit":
|
case "commit":
|
||||||
// Read in the commit to get its tree and in case this is one of the last used commits
|
// Read in the commit to get its tree and in case this is one of the last used commits
|
||||||
curCommit, err = git.CommitFromReader(repo, git.MustIDFromString(commitID), io.LimitReader(batchReader, info.Size))
|
curCommit, err = git.CommitFromReader(git.MustIDFromString(commitID), io.LimitReader(batchReader, info.Size))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ func findLFSFileFunc(repo *git.Repository, objectID git.ObjectID, revListReader
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if info, _, err = batch.QueryContent(curCommit.Tree.ID.String()); err != nil {
|
if info, _, err = batch.QueryContent(curCommit.TreeID.String()); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
curPath = ""
|
curPath = ""
|
||||||
|
|||||||
@@ -92,15 +92,14 @@ func (repo *Repository) getCommit(id ObjectID) (*Commit, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
commit := convertCommit(gogitCommit)
|
commit := convertCommit(gogitCommit)
|
||||||
commit.repo = repo
|
|
||||||
|
|
||||||
tree, err := gogitCommit.Tree()
|
tree, err := gogitCommit.Tree()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
commit.Tree.ID = ParseGogitHash(tree.Hash)
|
commit.TreeID = ParseGogitHash(tree.Hash)
|
||||||
commit.Tree.resolvedGogitTreeObject = tree
|
commit.Tree().resolvedGogitTreeObject = tree
|
||||||
|
|
||||||
return commit, nil
|
return commit, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ func (repo *Repository) getCommitWithBatch(batch CatFileBatch, id ObjectID) (*Co
|
|||||||
}
|
}
|
||||||
return repo.getCommitWithBatch(batch, tag.Object)
|
return repo.getCommitWithBatch(batch, tag.Object)
|
||||||
case "commit":
|
case "commit":
|
||||||
commit, err := CommitFromReader(repo, id, io.LimitReader(rd, info.Size))
|
commit, err := CommitFromReader(id, io.LimitReader(rd, info.Size))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,5 +149,5 @@ func (repo *Repository) WriteTree() (*Tree, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return NewTree(repo, id), nil
|
return newTree(id), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tree := NewTree(repo, id)
|
tree := newTree(id)
|
||||||
tree.resolvedGogitTreeObject = gogitTree
|
tree.resolvedGogitTreeObject = gogitTree
|
||||||
return tree, nil
|
return tree, nil
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,6 @@ func (repo *Repository) GetTree(idStr string) (*Tree, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
resolvedID := id
|
|
||||||
commitObject, err := repo.gogitRepo.CommitObject(plumbing.Hash(id.RawValue()))
|
commitObject, err := repo.gogitRepo.CommitObject(plumbing.Hash(id.RawValue()))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
id = ParseGogitHash(commitObject.TreeHash)
|
id = ParseGogitHash(commitObject.TreeHash)
|
||||||
@@ -62,6 +61,5 @@ func (repo *Repository) GetTree(idStr string) (*Tree, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
treeObject.ResolvedID = resolvedID
|
|
||||||
return treeObject, nil
|
return treeObject, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) {
|
|||||||
|
|
||||||
switch info.Type {
|
switch info.Type {
|
||||||
case "tag":
|
case "tag":
|
||||||
resolvedID := id
|
|
||||||
data, err := io.ReadAll(io.LimitReader(rd, info.Size))
|
data, err := io.ReadAll(io.LimitReader(rd, info.Size))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -37,21 +36,20 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
commit.Tree.ResolvedID = resolvedID
|
tree := commit.Tree()
|
||||||
return &commit.Tree, nil
|
return tree, nil
|
||||||
case "commit":
|
case "commit":
|
||||||
commit, err := CommitFromReader(repo, id, io.LimitReader(rd, info.Size))
|
commit, err := CommitFromReader(id, io.LimitReader(rd, info.Size))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, err := rd.Discard(1); err != nil {
|
if _, err := rd.Discard(1); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
commit.Tree.ResolvedID = commit.ID
|
tree := commit.Tree()
|
||||||
return &commit.Tree, nil
|
return tree, nil
|
||||||
case "tree":
|
case "tree":
|
||||||
tree := NewTree(repo, id)
|
tree := newTree(id)
|
||||||
tree.ResolvedID = id
|
|
||||||
objectFormat, err := repo.GetObjectFormat()
|
objectFormat, err := repo.GetObjectFormat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package git
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.dev/modules/git/gitcmd"
|
"gitea.dev/modules/git/gitcmd"
|
||||||
@@ -13,24 +14,14 @@ import (
|
|||||||
|
|
||||||
type TreeCommon struct {
|
type TreeCommon struct {
|
||||||
ID ObjectID
|
ID ObjectID
|
||||||
ResolvedID ObjectID
|
|
||||||
|
|
||||||
repo *Repository
|
|
||||||
ptree *Tree // parent tree
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTree create a new tree according the repository and tree id
|
func newTree(id ObjectID) *Tree {
|
||||||
func NewTree(repo *Repository, id ObjectID) *Tree {
|
return &Tree{TreeCommon: TreeCommon{ID: id}}
|
||||||
return &Tree{
|
|
||||||
TreeCommon: TreeCommon{
|
|
||||||
ID: id,
|
|
||||||
repo: repo,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubTree get a subtree by the sub dir path
|
// SubTree get a subtree by the sub dir path
|
||||||
func (t *Tree) SubTree(rpath string) (*Tree, error) {
|
func (t *Tree) SubTree(ctx context.Context, gitRepo *Repository, rpath string) (*Tree, error) {
|
||||||
if len(rpath) == 0 {
|
if len(rpath) == 0 {
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
@@ -43,16 +34,15 @@ func (t *Tree) SubTree(rpath string) (*Tree, error) {
|
|||||||
te *TreeEntry
|
te *TreeEntry
|
||||||
)
|
)
|
||||||
for _, name := range paths {
|
for _, name := range paths {
|
||||||
te, err = p.GetTreeEntryByPath(name)
|
te, err = p.GetTreeEntryByPath(ctx, gitRepo, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
g, err = t.repo.getTree(te.ID)
|
g, err = gitRepo.getTree(te.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
g.ptree = p
|
|
||||||
p = g
|
p = g
|
||||||
}
|
}
|
||||||
return g, nil
|
return g, nil
|
||||||
|
|||||||
@@ -4,15 +4,17 @@
|
|||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
// GetBlobByPath get the blob object according the path
|
// GetBlobByPath get the blob object according the path
|
||||||
func (t *Tree) GetBlobByPath(relpath string) (*Blob, error) {
|
func (t *Tree) GetBlobByPath(ctx context.Context, gitRepo *Repository, relpath string) (*Blob, error) {
|
||||||
entry, err := t.GetTreeEntryByPath(relpath)
|
entry, err := t.GetTreeEntryByPath(ctx, gitRepo, relpath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !entry.IsDir() && !entry.IsSubModule() {
|
if !entry.IsDir() && !entry.IsSubModule() {
|
||||||
return entry.Blob(), nil
|
return entry.Blob(gitRepo), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, ErrNotExist{"", relpath}
|
return nil, ErrNotExist{"", relpath}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// GetTreeEntryByPath get the tree entries according the sub dir
|
// GetTreeEntryByPath get the tree entries according the sub dir
|
||||||
func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
|
func (t *Tree) GetTreeEntryByPath(ctx context.Context, gitRepo *Repository, relpath string) (*TreeEntry, error) {
|
||||||
if len(relpath) == 0 {
|
if len(relpath) == 0 {
|
||||||
return &TreeEntry{
|
return &TreeEntry{
|
||||||
ID: t.ID,
|
ID: t.ID,
|
||||||
@@ -30,7 +31,7 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
|
|||||||
tree := t
|
tree := t
|
||||||
for i, name := range parts {
|
for i, name := range parts {
|
||||||
if i == len(parts)-1 {
|
if i == len(parts)-1 {
|
||||||
entries, err := tree.ListEntries()
|
entries, err := tree.ListEntries(ctx, gitRepo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == plumbing.ErrObjectNotFound {
|
if err == plumbing.ErrObjectNotFound {
|
||||||
return nil, ErrNotExist{
|
return nil, ErrNotExist{
|
||||||
@@ -45,7 +46,7 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tree, err = tree.SubTree(name)
|
tree, err = tree.SubTree(ctx, gitRepo, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == plumbing.ErrObjectNotFound {
|
if err == plumbing.ErrObjectNotFound {
|
||||||
return nil, ErrNotExist{
|
return nil, ErrNotExist{
|
||||||
|
|||||||
@@ -6,12 +6,13 @@
|
|||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetTreeEntryByPath get the tree entries according the sub dir
|
// GetTreeEntryByPath get the tree entries according the sub dir
|
||||||
func (t *Tree) GetTreeEntryByPath(relpath string) (_ *TreeEntry, err error) {
|
func (t *Tree) GetTreeEntryByPath(ctx context.Context, gitRepo *Repository, relpath string) (_ *TreeEntry, err error) {
|
||||||
if len(relpath) == 0 {
|
if len(relpath) == 0 {
|
||||||
return &TreeEntry{
|
return &TreeEntry{
|
||||||
ptree: t,
|
ptree: t,
|
||||||
@@ -26,14 +27,14 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (_ *TreeEntry, err error) {
|
|||||||
|
|
||||||
tree := t
|
tree := t
|
||||||
for _, name := range parts[:len(parts)-1] {
|
for _, name := range parts[:len(parts)-1] {
|
||||||
tree, err = tree.SubTree(name)
|
tree, err = tree.SubTree(ctx, gitRepo, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
name := parts[len(parts)-1]
|
name := parts[len(parts)-1]
|
||||||
entries, err := tree.ListEntries()
|
entries, err := tree.ListEntries(ctx, gitRepo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"path"
|
"path"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -78,18 +79,18 @@ type EntryFollowResult struct {
|
|||||||
TargetEntry *TreeEntry
|
TargetEntry *TreeEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
func EntryFollowLink(commit *Commit, fullPath string, te *TreeEntry) (*EntryFollowResult, error) {
|
func EntryFollowLink(ctx context.Context, gitRepo *Repository, commit *Commit, fullPath string, te *TreeEntry) (*EntryFollowResult, error) {
|
||||||
if !te.IsLink() {
|
if !te.IsLink() {
|
||||||
return nil, util.ErrorWrap(util.ErrUnprocessableContent, "%q is not a symlink", fullPath)
|
return nil, util.ErrorWrap(util.ErrUnprocessableContent, "%q is not a symlink", fullPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// git's filename max length is 4096, hopefully a link won't be longer than multiple of that
|
// git's filename max length is 4096, hopefully a link won't be longer than multiple of that
|
||||||
const maxSymlinkSize = 20 * 4096
|
const maxSymlinkSize = 20 * 4096
|
||||||
if te.Blob().Size() > maxSymlinkSize {
|
if te.Blob(gitRepo).Size() > maxSymlinkSize {
|
||||||
return nil, util.ErrorWrap(util.ErrUnprocessableContent, "%q content exceeds symlink limit", fullPath)
|
return nil, util.ErrorWrap(util.ErrUnprocessableContent, "%q content exceeds symlink limit", fullPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
link, err := te.Blob().GetBlobContent(maxSymlinkSize)
|
link, err := te.Blob(gitRepo).GetBlobContent(maxSymlinkSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -99,18 +100,18 @@ func EntryFollowLink(commit *Commit, fullPath string, te *TreeEntry) (*EntryFoll
|
|||||||
}
|
}
|
||||||
|
|
||||||
targetFullPath := path.Join(path.Dir(fullPath), link)
|
targetFullPath := path.Join(path.Dir(fullPath), link)
|
||||||
targetEntry, err := commit.GetTreeEntryByPath(targetFullPath)
|
targetEntry, err := commit.GetTreeEntryByPath(ctx, gitRepo, targetFullPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &EntryFollowResult{SymlinkContent: link}, err
|
return &EntryFollowResult{SymlinkContent: link}, err
|
||||||
}
|
}
|
||||||
return &EntryFollowResult{SymlinkContent: link, TargetFullPath: targetFullPath, TargetEntry: targetEntry}, nil
|
return &EntryFollowResult{SymlinkContent: link, TargetFullPath: targetFullPath, TargetEntry: targetEntry}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func EntryFollowLinks(commit *Commit, firstFullPath string, firstTreeEntry *TreeEntry, optLimit ...int) (res *EntryFollowResult, err error) {
|
func EntryFollowLinks(ctx context.Context, gitRepo *Repository, commit *Commit, firstFullPath string, firstTreeEntry *TreeEntry, optLimit ...int) (res *EntryFollowResult, err error) {
|
||||||
limit := util.OptionalArg(optLimit, 10)
|
limit := util.OptionalArg(optLimit, 10)
|
||||||
treeEntry, fullPath := firstTreeEntry, firstFullPath
|
treeEntry, fullPath := firstTreeEntry, firstFullPath
|
||||||
for range limit {
|
for range limit {
|
||||||
res, err = EntryFollowLink(commit, fullPath, treeEntry)
|
res, err = EntryFollowLink(ctx, gitRepo, commit, fullPath, treeEntry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
@@ -125,28 +126,26 @@ func EntryFollowLinks(commit *Commit, firstFullPath string, firstTreeEntry *Tree
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns the Tree pointed to by this TreeEntry, or nil if this is not a tree
|
func (te *TreeEntry) Tree(gitRepo *Repository) *Tree {
|
||||||
func (te *TreeEntry) Tree() *Tree {
|
t, err := gitRepo.getTree(te.ID)
|
||||||
t, err := te.ptree.repo.getTree(te.ID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
t.ptree = te.ptree
|
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSubJumpablePathName return the full path of subdirectory jumpable ( contains only one directory )
|
// GetSubJumpablePathName return the full path of subdirectory jumpable ( contains only one directory )
|
||||||
func (te *TreeEntry) GetSubJumpablePathName() string {
|
func (te *TreeEntry) GetSubJumpablePathName(ctx context.Context, gitRepo *Repository) string {
|
||||||
if te.IsSubModule() || !te.IsDir() {
|
if te.IsSubModule() || !te.IsDir() {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
tree, err := te.ptree.SubTree(te.Name())
|
tree, err := te.ptree.SubTree(ctx, gitRepo, te.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return te.Name()
|
return te.Name()
|
||||||
}
|
}
|
||||||
entries, _ := tree.ListEntries()
|
entries, _ := tree.ListEntries(ctx, gitRepo)
|
||||||
if len(entries) == 1 && entries[0].IsDir() {
|
if len(entries) == 1 && entries[0].IsDir() {
|
||||||
name := entries[0].GetSubJumpablePathName()
|
name := entries[0].GetSubJumpablePathName(ctx, gitRepo)
|
||||||
if name != "" {
|
if name != "" {
|
||||||
return te.Name() + "/" + name
|
return te.Name() + "/" + name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ func TestFollowLink(t *testing.T) {
|
|||||||
// get the symlink
|
// get the symlink
|
||||||
{
|
{
|
||||||
lnkFullPath := "foo/bar/link_to_hello"
|
lnkFullPath := "foo/bar/link_to_hello"
|
||||||
lnk, err := commit.Tree.GetTreeEntryByPath("foo/bar/link_to_hello")
|
lnk, err := commit.Tree().GetTreeEntryByPath(t.Context(), r, "foo/bar/link_to_hello")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, lnk.IsLink())
|
assert.True(t, lnk.IsLink())
|
||||||
|
|
||||||
// should be able to dereference to target
|
// should be able to dereference to target
|
||||||
res, err := EntryFollowLink(commit, lnkFullPath, lnk)
|
res, err := EntryFollowLink(t.Context(), r, commit, lnkFullPath, lnk)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "hello", res.TargetEntry.Name())
|
assert.Equal(t, "hello", res.TargetEntry.Name())
|
||||||
assert.Equal(t, "foo/nar/hello", res.TargetFullPath)
|
assert.Equal(t, "foo/nar/hello", res.TargetFullPath)
|
||||||
@@ -38,38 +38,38 @@ func TestFollowLink(t *testing.T) {
|
|||||||
|
|
||||||
{
|
{
|
||||||
// should error when called on a normal file
|
// should error when called on a normal file
|
||||||
entry, err := commit.Tree.GetTreeEntryByPath("file1.txt")
|
entry, err := commit.Tree().GetTreeEntryByPath(t.Context(), r, "file1.txt")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
res, err := EntryFollowLink(commit, "file1.txt", entry)
|
res, err := EntryFollowLink(t.Context(), r, commit, "file1.txt", entry)
|
||||||
assert.ErrorIs(t, err, util.ErrUnprocessableContent)
|
assert.ErrorIs(t, err, util.ErrUnprocessableContent)
|
||||||
assert.Nil(t, res)
|
assert.Nil(t, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// should error for broken links
|
// should error for broken links
|
||||||
entry, err := commit.Tree.GetTreeEntryByPath("foo/broken_link")
|
entry, err := commit.Tree().GetTreeEntryByPath(t.Context(), r, "foo/broken_link")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, entry.IsLink())
|
assert.True(t, entry.IsLink())
|
||||||
res, err := EntryFollowLink(commit, "foo/broken_link", entry)
|
res, err := EntryFollowLink(t.Context(), r, commit, "foo/broken_link", entry)
|
||||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||||
assert.Equal(t, "nar/broken_link", res.SymlinkContent)
|
assert.Equal(t, "nar/broken_link", res.SymlinkContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// should error for external links
|
// should error for external links
|
||||||
entry, err := commit.Tree.GetTreeEntryByPath("foo/outside_repo")
|
entry, err := commit.Tree().GetTreeEntryByPath(t.Context(), r, "foo/outside_repo")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, entry.IsLink())
|
assert.True(t, entry.IsLink())
|
||||||
res, err := EntryFollowLink(commit, "foo/outside_repo", entry)
|
res, err := EntryFollowLink(t.Context(), r, commit, "foo/outside_repo", entry)
|
||||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||||
assert.Equal(t, "../../outside_repo", res.SymlinkContent)
|
assert.Equal(t, "../../outside_repo", res.SymlinkContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// testing fix for short link bug
|
// testing fix for short link bug
|
||||||
entry, err := commit.Tree.GetTreeEntryByPath("foo/link_short")
|
entry, err := commit.Tree().GetTreeEntryByPath(t.Context(), r, "foo/link_short")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
res, err := EntryFollowLink(commit, "foo/link_short", entry)
|
res, err := EntryFollowLink(t.Context(), r, commit, "foo/link_short", entry)
|
||||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||||
assert.Equal(t, "a", res.SymlinkContent)
|
assert.Equal(t, "a", res.SymlinkContent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
"github.com/go-git/go-git/v5/plumbing/filemode"
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
@@ -29,15 +31,15 @@ func (te *TreeEntry) toGogitTreeEntry() *object.TreeEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Size returns the size of the entry
|
// GetSize returns the size of the entry
|
||||||
func (te *TreeEntry) Size() int64 {
|
func (te *TreeEntry) GetSize(ctx context.Context, gitRepo *Repository) int64 {
|
||||||
if te.IsDir() {
|
if te.IsDir() {
|
||||||
return 0
|
return 0
|
||||||
} else if te.sized {
|
} else if te.sized {
|
||||||
return te.size
|
return te.size
|
||||||
}
|
}
|
||||||
|
|
||||||
ptreeGogitTree, err := te.ptree.gogitTreeObject()
|
ptreeGogitTree, err := te.ptree.gogitTreeObject(gitRepo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -52,10 +54,10 @@ func (te *TreeEntry) Size() int64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Blob returns the blob object the entry
|
// Blob returns the blob object the entry
|
||||||
func (te *TreeEntry) Blob() *Blob {
|
func (te *TreeEntry) Blob(gitRepo *Repository) *Blob {
|
||||||
return &Blob{
|
return &Blob{
|
||||||
ID: te.ID,
|
ID: te.ID,
|
||||||
repo: te.ptree.repo,
|
repo: gitRepo,
|
||||||
name: te.Name(),
|
name: te.Name(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,25 +5,28 @@
|
|||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import "gitea.dev/modules/log"
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
// Size returns the size of the entry
|
"gitea.dev/modules/log"
|
||||||
func (te *TreeEntry) Size() int64 {
|
)
|
||||||
|
|
||||||
|
func (te *TreeEntry) GetSize(ctx context.Context, gitRepo *Repository) int64 {
|
||||||
if te.IsDir() {
|
if te.IsDir() {
|
||||||
return 0
|
return 0
|
||||||
} else if te.sized {
|
} else if te.sized {
|
||||||
return te.size
|
return te.size
|
||||||
}
|
}
|
||||||
|
|
||||||
batch, cancel, err := te.ptree.repo.CatFileBatch(te.ptree.repo.Ctx)
|
batch, cancel, err := gitRepo.CatFileBatch(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("error whilst reading size for %s in %s. Error: %v", te.ID.String(), te.ptree.repo.Path, err)
|
log.Debug("error whilst reading size for %s in %s. Error: %v", te.ID.String(), gitRepo.Path, err)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
defer cancel()
|
defer cancel()
|
||||||
info, err := batch.QueryInfo(te.ID.String())
|
info, err := batch.QueryInfo(te.ID.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("error whilst reading size for %s in %s. Error: %v", te.ID.String(), te.ptree.repo.Path, err)
|
log.Debug("error whilst reading size for %s in %s. Error: %v", te.ID.String(), gitRepo.Path, err)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,12 +36,12 @@ func (te *TreeEntry) Size() int64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Blob returns the blob object the entry
|
// Blob returns the blob object the entry
|
||||||
func (te *TreeEntry) Blob() *Blob {
|
func (te *TreeEntry) Blob(gitRepo *Repository) *Blob {
|
||||||
return &Blob{
|
return &Blob{
|
||||||
ID: te.ID,
|
ID: te.ID,
|
||||||
name: te.Name(),
|
name: te.Name(),
|
||||||
size: te.size,
|
size: te.size,
|
||||||
gotSize: te.sized,
|
gotSize: te.sized,
|
||||||
repo: te.ptree.repo,
|
repo: gitRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
@@ -20,9 +21,9 @@ type Tree struct {
|
|||||||
resolvedGogitTreeObject *object.Tree
|
resolvedGogitTreeObject *object.Tree
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tree) gogitTreeObject() (_ *object.Tree, err error) {
|
func (t *Tree) gogitTreeObject(gitRepo *Repository) (_ *object.Tree, err error) {
|
||||||
if t.resolvedGogitTreeObject == nil {
|
if t.resolvedGogitTreeObject == nil {
|
||||||
t.resolvedGogitTreeObject, err = t.repo.gogitRepo.TreeObject(plumbing.Hash(t.ID.RawValue()))
|
t.resolvedGogitTreeObject, err = gitRepo.gogitRepo.TreeObject(plumbing.Hash(t.ID.RawValue()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -31,8 +32,8 @@ func (t *Tree) gogitTreeObject() (_ *object.Tree, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListEntries returns all entries of current tree.
|
// ListEntries returns all entries of current tree.
|
||||||
func (t *Tree) ListEntries() (Entries, error) {
|
func (t *Tree) ListEntries(_ context.Context, gitRepo *Repository) (Entries, error) {
|
||||||
gogitTree, err := t.gogitTreeObject()
|
gogitTree, err := t.gogitTreeObject(gitRepo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -50,8 +51,8 @@ func (t *Tree) ListEntries() (Entries, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees
|
// ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees
|
||||||
func (t *Tree) ListEntriesRecursiveWithSize() (entries Entries, _ error) {
|
func (t *Tree) ListEntriesRecursiveWithSize(_ context.Context, gitRepo *Repository) (entries Entries, _ error) {
|
||||||
gogitTree, err := t.gogitTreeObject()
|
gogitTree, err := t.gogitTreeObject(gitRepo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -76,6 +77,6 @@ func (t *Tree) ListEntriesRecursiveWithSize() (entries Entries, _ error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListEntriesRecursiveFast is the alias of ListEntriesRecursiveWithSize for the gogit version
|
// ListEntriesRecursiveFast is the alias of ListEntriesRecursiveWithSize for the gogit version
|
||||||
func (t *Tree) ListEntriesRecursiveFast() (Entries, error) {
|
func (t *Tree) ListEntriesRecursiveFast(ctx context.Context, gitRepo *Repository) (Entries, error) {
|
||||||
return t.ListEntriesRecursiveWithSize()
|
return t.ListEntriesRecursiveWithSize(ctx, gitRepo)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"gitea.dev/modules/git/gitcmd"
|
"gitea.dev/modules/git/gitcmd"
|
||||||
@@ -20,13 +21,12 @@ type Tree struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListEntries returns all entries of current tree.
|
// ListEntries returns all entries of current tree.
|
||||||
func (t *Tree) ListEntries() (Entries, error) {
|
func (t *Tree) ListEntries(ctx context.Context, gitRepo *Repository) (Entries, error) {
|
||||||
if t.entriesParsed {
|
if t.entriesParsed {
|
||||||
return t.entries, nil
|
return t.entries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.repo != nil {
|
batch, cancel, err := gitRepo.CatFileBatch(ctx)
|
||||||
batch, cancel, err := t.repo.CatFileBatch(t.repo.Ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -60,9 +60,8 @@ func (t *Tree) ListEntries() (Entries, error) {
|
|||||||
if err := DiscardFull(rd, info.Size+1); err != nil {
|
if err := DiscardFull(rd, info.Size+1); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-l").AddDynamicArguments(t.ID.String()).WithDir(t.repo.Path).RunStdBytes(t.repo.Ctx)
|
stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-l").AddDynamicArguments(t.ID.String()).WithDir(gitRepo.Path).RunStdBytes(ctx)
|
||||||
if runErr != nil {
|
if runErr != nil {
|
||||||
if gitcmd.IsStderr(runErr, gitcmd.StderrNotValidObjectName) || gitcmd.IsStderr(runErr, gitcmd.StderrNotTreeObject) {
|
if gitcmd.IsStderr(runErr, gitcmd.StderrNotValidObjectName) || gitcmd.IsStderr(runErr, gitcmd.StderrNotTreeObject) {
|
||||||
return nil, ErrNotExist{
|
return nil, ErrNotExist{
|
||||||
@@ -72,7 +71,6 @@ func (t *Tree) ListEntries() (Entries, error) {
|
|||||||
return nil, runErr
|
return nil, runErr
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
t.entries, err = parseTreeEntries(stdout, t)
|
t.entries, err = parseTreeEntries(stdout, t)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.entriesParsed = true
|
t.entriesParsed = true
|
||||||
@@ -83,12 +81,12 @@ func (t *Tree) ListEntries() (Entries, error) {
|
|||||||
|
|
||||||
// listEntriesRecursive returns all entries of current tree recursively including all subtrees
|
// listEntriesRecursive returns all entries of current tree recursively including all subtrees
|
||||||
// extraArgs could be "-l" to get the size, which is slower
|
// extraArgs could be "-l" to get the size, which is slower
|
||||||
func (t *Tree) listEntriesRecursive(extraArgs gitcmd.TrustedCmdArgs) (Entries, error) {
|
func (t *Tree) listEntriesRecursive(ctx context.Context, gitRepo *Repository, extraArgs gitcmd.TrustedCmdArgs) (Entries, error) {
|
||||||
stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-t", "-r").
|
stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-t", "-r").
|
||||||
AddArguments(extraArgs...).
|
AddArguments(extraArgs...).
|
||||||
AddDynamicArguments(t.ID.String()).
|
AddDynamicArguments(t.ID.String()).
|
||||||
WithDir(t.repo.Path).
|
WithDir(gitRepo.Path).
|
||||||
RunStdBytes(t.repo.Ctx)
|
RunStdBytes(ctx)
|
||||||
if runErr != nil {
|
if runErr != nil {
|
||||||
return nil, runErr
|
return nil, runErr
|
||||||
}
|
}
|
||||||
@@ -99,11 +97,11 @@ func (t *Tree) listEntriesRecursive(extraArgs gitcmd.TrustedCmdArgs) (Entries, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListEntriesRecursiveFast returns all entries of current tree recursively including all subtrees, no size
|
// ListEntriesRecursiveFast returns all entries of current tree recursively including all subtrees, no size
|
||||||
func (t *Tree) ListEntriesRecursiveFast() (Entries, error) {
|
func (t *Tree) ListEntriesRecursiveFast(ctx context.Context, gitRepo *Repository) (Entries, error) {
|
||||||
return t.listEntriesRecursive(nil)
|
return t.listEntriesRecursive(ctx, gitRepo, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees, with size
|
// ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees, with size
|
||||||
func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) {
|
func (t *Tree) ListEntriesRecursiveWithSize(ctx context.Context, gitRepo *Repository) (Entries, error) {
|
||||||
return t.listEntriesRecursive(gitcmd.TrustedCmdArgs{"--long"})
|
return t.listEntriesRecursive(ctx, gitRepo, gitcmd.TrustedCmdArgs{"--long"})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func TestSubTree_Issue29101(t *testing.T) {
|
|||||||
|
|
||||||
// old code could produce a different error if called multiple times
|
// old code could produce a different error if called multiple times
|
||||||
for range 10 {
|
for range 10 {
|
||||||
_, err = commit.SubTree("file1.txt")
|
_, err = commit.SubTree(t.Context(), repo, "file1.txt")
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.True(t, IsErrNotExist(err))
|
assert.True(t, IsErrNotExist(err))
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user