Merge branch 'main' into ssh-mirror-migrations

Signed-off-by: pomidorry <106489913+Pomidorry@users.noreply.github.com>
This commit is contained in:
pomidorry
2026-05-21 20:16:52 +03:00
committed by GitHub
108 changed files with 1497 additions and 966 deletions
-42
View File
@@ -1,42 +0,0 @@
<!-- NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue -->
<!--
1. Please speak English, this is the language all maintainers can speak and write.
2. Please ask questions or configuration/deploy problems on our Discord
server (https://discord.gg/gitea) or forum (https://forum.gitea.com).
3. Please take a moment to check that your issue doesn't already exist.
4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq)
5. Please give all relevant information below for bug reports, because
incomplete details will be handled as an invalid report.
-->
- Gitea version (or commit ref):
- Git version:
- Operating system:
<!-- Please include information on whether you built gitea yourself, used one of our downloads or are using some other package -->
<!-- Please also tell us how you are running gitea, e.g. if it is being run from docker, a command-line, systemd etc. --->
<!-- If you are using a package or systemd tell us what distribution you are using -->
- Database (use `[x]`):
- [ ] PostgreSQL
- [ ] MySQL
- [ ] MSSQL
- [ ] SQLite
- Can you reproduce the bug at https://demo.gitea.com:
- [ ] Yes (provide example URL)
- [ ] No
- Log gist:
<!-- It really is important to provide pertinent logs -->
<!-- Please read https://docs.gitea.com/administration/logging-config#collecting-logs-for-help -->
<!-- In addition, if your problem relates to git commands set `RUN_MODE=dev` at the top of app.ini -->
## Description
<!-- If using a proxy or a CDN (e.g. CloudFlare) in front of gitea, please
disable the proxy/CDN fully and connect to gitea directly to confirm
the issue still persists without those services. -->
...
## Screenshots
<!-- **If this issue involves the Web Interface, please include a screenshot** -->
+10 -73
View File
@@ -1,91 +1,28 @@
name: Bug Report
description: Found something you weren't expecting? Report it here!
description: Something isn't working as expected.
labels: ["type/bug"]
body:
- type: markdown
attributes:
value: |
NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue.
- type: markdown
attributes:
value: |
1. Please speak English, this is the language all maintainers can speak and write.
2. Please ask questions or configuration/deploy problems on our Discord
server (https://discord.gg/gitea) or forum (https://forum.gitea.com).
3. Make sure you are using the latest release and
take a moment to check that your issue hasn't been reported before.
4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq)
5. It's really important to provide pertinent details and logs (https://docs.gitea.com/help/support),
incomplete details will be handled as an invalid report.
- type: textarea
id: description
attributes:
label: Description
description: |
Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below)
If you are using a proxy or a CDN (e.g. Cloudflare) in front of Gitea, please disable the proxy/CDN fully and access Gitea directly to confirm the issue still persists without those services.
- **Security issue?** Email security@gitea.io instead of opening a public issue.
- **Need help** with setup or configuration? Ask on [Discord](https://discord.gg/Gitea) or the [forum](https://forum.gitea.com).
- Search [existing issues](https://github.com/go-gitea/gitea/issues?q=is%3Aissue) first.
- type: input
id: gitea-ver
attributes:
label: Gitea Version
description: Gitea version (or commit reference) of your instance
validations:
required: true
- type: dropdown
id: can-reproduce
- type: textarea
id: description
attributes:
label: Can you reproduce the bug on the Gitea demo site?
description: |
If so, please provide a URL in the Description field
URL of Gitea demo: https://demo.gitea.com
options:
- "Yes"
- "No"
label: What happened?
description: What you did, what you expected to happen, and what happened instead. Include logs if relevant.
validations:
required: true
- type: markdown
attributes:
value: |
It's really important to provide pertinent logs
Please read https://docs.gitea.com/administration/logging-config#collecting-logs-for-help
In addition, if your problem relates to git commands set `RUN_MODE=dev` at the top of app.ini
- type: input
id: logs
attributes:
label: Log Gist
description: Please provide a gist URL of your logs, with any sensitive information (e.g. API keys) removed/hidden
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If this issue involves the Web Interface, please provide one or more screenshots
- type: input
id: git-ver
attributes:
label: Git Version
description: The version of git running on the server
- type: input
id: os-ver
attributes:
label: Operating System
description: The operating system you are using to run Gitea
- type: textarea
id: run-info
id: environment
attributes:
label: How are you running Gitea?
description: |
Please include information on whether you built Gitea yourself, used one of our downloads, are using https://demo.gitea.com or are using some other package
Please also tell us how you are running Gitea, e.g. if it is being run from docker, a command-line, systemd etc.
If you are using a package or systemd tell us what distribution you are using
validations:
required: true
- type: dropdown
id: database
attributes:
label: Database
description: What database system are you running?
options:
- PostgreSQL
- MySQL/MariaDB
- MSSQL
- SQLite
description: Install method (binary, Docker, package), operating system, and database.
+8 -12
View File
@@ -1,24 +1,20 @@
name: Feature Request
description: Got an idea for a feature that Gitea doesn't have currently? Submit your idea here!
description: Suggest an idea for Gitea.
labels: ["type/proposal"]
body:
- type: markdown
attributes:
value: |
1. Please speak English, this is the language all maintainers can speak and write.
2. Please ask questions or configuration/deploy problems on our Discord
server (https://discord.gg/gitea) or forum (https://forum.gitea.com).
3. Please take a moment to check that your feature hasn't already been suggested.
Search [existing issues](https://github.com/go-gitea/gitea/issues?q=is%3Aissue) first.
- type: textarea
id: description
id: problem
attributes:
label: Feature Description
placeholder: |
I think it would be great if Gitea had...
label: What problem would this solve?
validations:
required: true
- type: textarea
id: screenshots
id: proposal
attributes:
label: Screenshots
description: If you can, provide screenshots of an implementation on another site e.g. GitHub
label: What do you propose?
validations:
required: true
-66
View File
@@ -1,66 +0,0 @@
name: Web Interface Bug Report
description: Something doesn't look quite as it should? Report it here!
labels: ["type/bug", "topic/ui"]
body:
- type: markdown
attributes:
value: |
NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue.
- type: markdown
attributes:
value: |
1. Please speak English, this is the language all maintainers can speak and write.
2. Please ask questions or configuration/deploy problems on our Discord
server (https://discord.gg/gitea) or forum (https://forum.gitea.com).
3. Please take a moment to check that your issue doesn't already exist.
4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq)
5. Please give all relevant information below for bug reports, because
incomplete details will be handled as an invalid report.
6. In particular it's really important to provide pertinent logs. If you are certain that this is a javascript
error, show us the javascript console. If the error appears to relate to Gitea the server you must also give us
DEBUG level logs. (See https://docs.gitea.com/administration/logging-config#collecting-logs-for-help)
- type: textarea
id: description
attributes:
label: Description
description: |
Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below)
If using a proxy or a CDN (e.g. CloudFlare) in front of gitea, please disable the proxy/CDN fully and connect to gitea directly to confirm the issue still persists without those services.
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: Please provide at least 1 screenshot showing the issue.
validations:
required: true
- type: input
id: gitea-ver
attributes:
label: Gitea Version
description: Gitea version (or commit reference) your instance is running
validations:
required: true
- type: dropdown
id: can-reproduce
attributes:
label: Can you reproduce the bug on the Gitea demo site?
description: |
If so, please provide a URL in the Description field
URL of Gitea demo: https://demo.gitea.com
options:
- "Yes"
- "No"
validations:
required: true
- type: input
id: os-ver
attributes:
label: Operating System
description: The operating system you are using to access Gitea
- type: input
id: browser-ver
attributes:
label: Browser Version
description: The browser and version that you are using to access Gitea
validations:
required: true
+29 -25
View File
@@ -1,47 +1,51 @@
name: go-caches
description: Restore and save go module, build, and golangci-lint caches
description: Restore the go module, build, and golangci-lint caches. Save only on the cache-seeder workflow.
# Only the cache-seeder workflow saves; rename requires updating cache-seeder.yml.
# The lint job restores but does not save the gobuild cache, so only one writer
# (the gobuild job) populates it and there is no contention on the cache key.
inputs:
cache-name:
description: Short identifier used in the per-caller build cache key
required: true
build-cache:
description: Whether to include ~/.cache/go-build
default: "true"
build-cache-rotate:
description: Whether to rotate the build cache key per run so Go's test result cache can accumulate across runs
default: "false"
lint-cache:
description: Whether to include ~/.cache/golangci-lint
description: Restore (and save in cache-seeder) ~/.cache/golangci-lint
default: "false"
runs:
using: composite
steps:
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
- if: ${{ github.workflow == 'cache-seeder' }}
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/go/pkg/mod
key: gomod-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
restore-keys: gomod-${{ runner.os }}-${{ runner.arch }}
- if: ${{ inputs.build-cache == 'true' && inputs.build-cache-rotate == 'true' }}
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
- if: ${{ github.workflow != 'cache-seeder' }}
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.cache/go-build
key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }}-${{ hashFiles('go.sum') }}-${{ github.run_id }}
restore-keys: |
gobuild-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }}-${{ hashFiles('go.sum') }}
gobuild-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }}
gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
gobuild-${{ runner.os }}-${{ runner.arch }}
- if: ${{ inputs.build-cache == 'true' && inputs.build-cache-rotate != 'true' }}
path: ~/go/pkg/mod
key: gomod-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
restore-keys: gomod-${{ runner.os }}-${{ runner.arch }}
- if: ${{ github.workflow == 'cache-seeder' && inputs.lint-cache != 'true' }}
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.cache/go-build
key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
restore-keys: gobuild-${{ runner.os }}-${{ runner.arch }}
- if: ${{ inputs.lint-cache == 'true' }}
- if: ${{ github.workflow != 'cache-seeder' || inputs.lint-cache == 'true' }}
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.cache/go-build
key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
restore-keys: gobuild-${{ runner.os }}-${{ runner.arch }}
- if: ${{ inputs.lint-cache == 'true' && github.workflow == 'cache-seeder' }}
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.cache/golangci-lint
key: golangci-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }}-${{ hashFiles('go.sum', '.golangci.yml') }}
restore-keys: golangci-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }}
key: golint-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum', '.golangci.yml') }}
restore-keys: golint-${{ runner.os }}-${{ runner.arch }}
- if: ${{ inputs.lint-cache == 'true' && github.workflow != 'cache-seeder' }}
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.cache/golangci-lint
key: golint-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum', '.golangci.yml') }}
restore-keys: golint-${{ runner.os }}-${{ runner.arch }}
+9 -11
View File
@@ -1,11 +1,9 @@
<!-- start tips -->
Please check the following:
1. Make sure you are targeting the `main` branch, pull requests on release branches are only allowed for backports.
2. Use a Conventional Commits PR title, for example `fix(repo): handle empty branch names`.
3. Make sure you have read contributing guidelines: https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md .
4. For documentations contribution, please go to https://gitea.com/gitea/docs
5. Describe what your pull request does and which issue you're targeting (if any).
6. It is recommended to enable "Allow edits by maintainers", so maintainers can help more easily.
7. Your input here will be included in the commit message when this PR has been merged. If you don't want some content to be included, please separate them with a line like `---`.
8. Delete all these tips before posting.
<!-- end tips -->
<!--
Before submitting:
- Target the `main` branch; release branches are for backports only.
- Use a Conventional Commits title, e.g. `fix(repo): handle empty branch names`.
- Read the contributing guidelines: https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md
- Documentation changes go to https://gitea.com/gitea/docs
Describe your change below and link any issue it fixes.
-->
+26 -16
View File
@@ -1,15 +1,9 @@
# Populates the go module, build, and golangci-lint caches under the default
# branch's cache scope so that PR runs have a warm fallback to restore from.
# Populates main's cache scope so PR runs warm-start from it. Saves the go
# module, go build (incl. test compile), and golangci-lint caches.
#
# GitHub Actions caches are scoped per ref: a PR run can only write to its own
# branch's scope, but can read from the base branch's scope as a fallback.
# PRs therefore cannot seed main's scope themselves. Running the same cache
# steps on push-to-main is the only opportunity to populate that fallback
# scope so fresh PR branches start with a useful cache on first run.
# A PR job's exact key lives in its own PR-scope (empty on first run, filled
# by later runs of the same PR); on miss, actions/cache's restore-keys fall
# back to prefix matches against entries this seeder saves in main's scope.
# Caches are ref-scoped: PR runs read their own scope then fall back to the
# base branch. Per .github/actions/go-cache/action.yml, PRs are restore-only,
# so push-to-main is the only opportunity to populate the fallback scope.
name: cache-seeder
@@ -41,11 +35,25 @@ jobs:
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: seed
- run: make deps-backend
- run: make deps-backend deps-tools
- run: TAGS="bindata" make backend
- run: TAGS="bindata gogit" GOEXPERIMENT="" make backend
- name: warm test compile cache (bindata)
env:
TAGS: bindata
GOTEST_FLAGS: -race -list=^$$ -count=1
run: make test-backend
- name: warm test compile cache (bindata gogit)
env:
TAGS: bindata gogit
GOEXPERIMENT:
GOTEST_FLAGS: -race -list=^$$ -count=1
run: make test-backend
- name: warm integration compile cache
run: |
TAGS="bindata" make test-integration-compile
TAGS="bindata gogit" GOEXPERIMENT="" make test-integration-compile
TAGS="bindata gogit" GOTEST_FLAGS="-race" make test-integration-compile
lint:
runs-on: ubuntu-latest
@@ -53,7 +61,7 @@ jobs:
fail-fast: false
matrix:
include:
- { job: lint-backend, tags: "bindata", target: "lint-backend" }
- { tags: "bindata", target: "lint-backend" }
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
@@ -63,9 +71,11 @@ jobs:
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: ${{ matrix.job }}
lint-cache: "true"
- run: make deps-backend deps-tools
- run: make generate-go
env:
TAGS: ${{ matrix.tags }}
- run: make ${{ matrix.target }}
env:
TAGS: ${{ matrix.tags }}
+26
View File
@@ -0,0 +1,26 @@
name: giteabot backport
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
jobs:
giteabot:
if: github.repository == 'go-gitea/gitea'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: go-gitea/giteabot@d4f19d5b4a88059d8c3ca78d660631506fc0c286 # add retry logic to giteabot
with:
github_token: ${{ secrets.GITEABOT_TOKEN }}
gitea_fork: giteabot/gitea
checks: backport
+13 -6
View File
@@ -1,9 +1,6 @@
name: giteabot
on:
push:
branches:
- main
# pull_request_target gives this workflow access to GITEABOT_TOKEN on PRs from
# forks, which the bot needs to write labels, statuses and comments. Safe here
# because the job only runs a pinned action and never checks out PR HEAD.
@@ -24,9 +21,17 @@ on:
schedule:
- cron: "15 3 * * *"
workflow_dispatch:
inputs:
checks:
description: Comma-separated list of non-backport checks to run
required: false
default: labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions
permissions:
contents: read
issues: write
pull-requests: write
statuses: write
concurrency:
group: ${{ format('{0}-{1}', github.workflow, (github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review') && format('pr-{0}', github.event.pull_request.number) || 'maintenance') }}
@@ -38,7 +43,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: go-gitea/giteabot@8996d0b0e6c4ab066e3adcaf2c49b5d4cd15d7af # v1.0.1
# pull_request_review runs without repository secrets on fork PRs, so fall
# back to the workflow token for the non-backport checks handled here.
- uses: go-gitea/giteabot@d4f19d5b4a88059d8c3ca78d660631506fc0c286 # add retry logic to giteabot
with:
github_token: ${{ secrets.GITEABOT_TOKEN }}
gitea_fork: giteabot/gitea
github_token: ${{ secrets.GITEABOT_TOKEN || github.token }}
checks: ${{ github.event.inputs.checks || 'labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }}
-8
View File
@@ -27,7 +27,6 @@ jobs:
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: lint-backend
lint-cache: "true"
- run: make deps-backend deps-tools
- run: TAGS="bindata" make generate-go # lint-go also lints with "bindata" tags which requires "_bindata.go"
@@ -47,8 +46,6 @@ jobs:
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make lint-spell
@@ -77,9 +74,6 @@ jobs:
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: checks-backend
build-cache: "false"
- run: make deps-backend deps-tools
- run: make --always-make checks-backend # ensure the "go-licenses" make target runs
@@ -113,8 +107,6 @@ jobs:
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: compliance-backend
- run: make deps-backend generate-go
# no frontend build here as backend should be able to build, even without any frontend files
# CGO is not used when cross-compile, so these steps also test if the code is compatible with CGO disabled
+15 -19
View File
@@ -14,10 +14,14 @@ jobs:
files-changed:
uses: ./.github/workflows/files-changed.yml
test-pgsql:
test-pgsql-shards:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2]
services:
pgsql:
image: postgres:14
@@ -48,8 +52,6 @@ jobs:
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: pgsql
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 pgsql ldap minio" | sudo tee -a /etc/hosts'
- run: make deps-backend
@@ -57,6 +59,7 @@ jobs:
env:
TAGS: bindata
- name: run migration tests
if: matrix.shard == 1
run: GITEA_TEST_DATABASE=pgsql make test-migration
- name: run tests
run: GITEA_TEST_DATABASE=pgsql make test-integration
@@ -66,6 +69,8 @@ jobs:
GOTEST_FLAGS: -race -timeout=40m
TAGS: bindata gogit
TEST_LDAP: 1
TEST_SHARD: ${{ matrix.shard }}
TEST_TOTAL_SHARDS: ${{ strategy.job-total }}
test-sqlite:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
@@ -79,15 +84,12 @@ jobs:
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: sqlite
- run: make deps-backend
- run: make backend
env:
TAGS: bindata gogit
GOEXPERIMENT:
- name: run migration tests
run: GITEA_TEST_DATABASE=sqlite make test-migration
- run: GITEA_TEST_DATABASE=sqlite make test-migration
env:
TAGS: bindata gogit
- name: run tests
@@ -145,28 +147,27 @@ jobs:
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: unit
build-cache-rotate: "true"
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 minio devstoreaccount1.azurite.local mysql elasticsearch meilisearch smtpimap" | sudo tee -a /etc/hosts'
- run: make deps-backend
- run: make backend
- run: make generate-go
env:
TAGS: bindata
- name: unit-tests
run: make test-backend test-check
run: make test-backend
env:
GOTEST_FLAGS: -race -timeout=20m
TAGS: bindata
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
- name: unit-tests-gogit
run: make test-backend test-check
run: make test-backend
env:
GOTEST_FLAGS: -race -timeout=20m
TAGS: bindata gogit
GOEXPERIMENT:
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
GITEA_TEST_CI_SKIP_EXTERNAL: true
- run: make test-check
test-mysql:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
@@ -205,16 +206,13 @@ jobs:
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: mysql
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mysql elasticsearch smtpimap" | sudo tee -a /etc/hosts'
- run: make deps-backend
- run: make backend
env:
TAGS: bindata
- name: run migration tests
run: GITEA_TEST_DATABASE=mysql make test-migration
- run: GITEA_TEST_DATABASE=mysql make test-migration
- name: run tests
run: GITEA_TEST_DATABASE=mysql make test-integration
env:
@@ -246,8 +244,6 @@ jobs:
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: mssql
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mssql devstoreaccount1.azurite.local" | sudo tee -a /etc/hosts'
- run: make deps-backend
-3
View File
@@ -26,9 +26,6 @@ jobs:
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: e2e
build-cache: "false"
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
+62
View File
@@ -4,6 +4,68 @@ This changelog goes through the changes that have been made in each release
without substantial changes to our git log; to see the highlights of what has
been added to each release, please refer to the [blog](https://blog.gitea.com).
## [1.26.2](https://github.com/go-gitea/gitea/releases/tag/1.26.2) - 2026-05-20
* SECURITY
* fix(permissions): Fix reading permission (#37769)
* fix(actions): make artifact signature payloads unambiguous (#37707)
* fix: Unify public-only token filtering in API queries and repo access checks (#37118)
* fix: Add missed token scope checking (#37735)
* fix(oauth): bind token exchanges to the original client request (#37704)
* fix(oauth): strengthen PKCE validation and refresh token replay protection (#37706)
* fix(web): enforce token scopes on raw, media, and attachment downloads (#37698)
* fix(security): enforce wiki git writes and LFS token access at request time (#37695)
* feat(api): encrypt AWS creds (#37679)
* fix(deps): update dependency mermaid to v11.15.0 [security], add e2e test
* fix(packages): Add label for private and internal package and fix composor package source permission check (#37610)
* fix(git): Fix smart http request scope bug (#37583)
* Fix basic auth bug (#37503)
* Fix allow maintainer edit permission check (#37479) (#37484)
* Fix URL sanitization to handle schemeless credentials (#37440) (#37471)
* Fix attachment Content-Security-Policy (#37455) (#37464)
* chore(deps): bump go-git/go-git/v5 to 5.19.0 (#37608)
* BUGFIXES
* fix(pull): handle empty pull request files view to allow reviews (#37783)
* fix(markup): make RenderString never fail (#37779)
* fix: add natural sort to sortTreeViewNodes (#37772)
* fix: package creation unique conflict (#37774)
* fix!: add DEFAULT_TITLE_SOURCE setting for pull request title default behavior (#37465)
* fix: Allow direct commits for unprotected files with push restrictions (#37657)
* fix(actions): wrong assumption that run id always >= job id (#37737)
* fix(auth): set User-Agent on avatar fetch and sync avatar on link-account register (#37564) (#37588)
* fix(actions): deadlock between PrepareRunAndInsert and UpdateTaskByState (#37692)
* fix(repo): /generate must sync the branch table for the new repo (#37693)
* build: Fix snap build (1.26)
* fix(actions): run TransferLogs on UpdateLog{Rows:[], NoMore:true} (#37631)
* fix show correct mergebase
* fix: make clone URL respect public URL detection setting (#37615)
* fix: "run as root" check (#37622)
* chore(deps): update dependency go to v1.26.3 (#37601)
* Compare dropdown fails when selecting branch with no common merge-base (#37470)
* fix: treat email addresses case-insensitively (#37600)
* fix(actions): fix blank lines after ::endgroup:: (#37597)
* fix(actions): report individual step status in workflow job API response (#37592)
* fix: Invalid UTF-8 commit messages in JSON API responses (#37542)
* fix: use consistent GetUser family functions (#37553)
* fix(api): return 409 message instead of empty JSON for wrong commit id (#37572)
* fix(actions): prevent panic when workflow contains null jobs (#37570)
* Make ServeSetHeaders default to download attachment if filename exists (#37552) (#37555)
* Fix(actions): validate workflow param to prevent 500 error (#37546) (#37554)
* Don't unblock run-level-concurrency-blocked runs in the resolver (#37461) (#37538)
* Fix(packages): use file names for generic web downloads (#37514) (#37520)
* Fix merge autodetect can't close other PRs but only the last one when multiple PRs are pushed at once (#37512) (#37516)
* Fix update branch protection order (#37508) (#37513)
* Fix mCaptcha broken after Vite migration (#37492) (#37509)
* Fix review submission from single-commit PR view (#37475) (#37485)
* Fix scheduled action panic with null event payload (#37459) (#37466)
* Make GetPossibleUserByID can handle deleted user (#37430) (#37431)
* Remove excessive quote from terraform instructions (#37424) (#37426)
* Fix color regressions, add `priority` color (#37417) (#37421)
* MISC
* Add CurrentURL template variable back (#37444) (#37449)
## [1.26.1](https://github.com/go-gitea/gitea/releases/tag/v1.26.1) - 2026-04-21
* BUGFIXES
+5 -1
View File
@@ -445,7 +445,11 @@ test-integration:
@# would flood output per passing test. testcache can't help these tests anyway —
@# they mutate the work directory, so cache inputs change between runs.
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -c code.gitea.io/gitea/tests/integration -o ./test-integration-$(GITEA_TEST_DATABASE).test
./test-integration-$(GITEA_TEST_DATABASE).test
./tools/test-integration.sh ./test-integration-$(GITEA_TEST_DATABASE).test
.PHONY: test-integration-compile
test-integration-compile:
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -c -o /dev/null code.gitea.io/gitea/tests/integration
.PHONY: test-integration\#%
test-integration\#%:
+1 -1
View File
@@ -1172,7 +1172,7 @@ LEVEL = Info
;; Default source for the pull request title when opening a new PR.
;; "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.
;DEFAULT_TITLE_SOURCE = first-commit
;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.
;; Use "-1" to always check all pull requests (old behavior). Use "0" to always delay the checks.
+3 -6
View File
@@ -55,7 +55,7 @@ require (
github.com/go-co-op/gocron/v2 v2.21.2
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-git/v5 v5.19.0
github.com/go-git/go-git/v5 v5.19.1
github.com/go-ldap/ldap/v3 v3.4.13
github.com/go-redsync/redsync/v4 v4.16.0
github.com/go-sql-driver/mysql v1.10.0
@@ -105,11 +105,10 @@ require (
github.com/urfave/cli-docs/v3 v3.1.0
github.com/urfave/cli/v3 v3.6.1
github.com/wneessen/go-mail v0.7.3
github.com/xeipuuv/gojsonschema v1.2.0
github.com/yohcop/openid-go v1.0.1
github.com/yuin/goldmark v1.8.2
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
gitlab.com/gitlab-org/api/client-go/v2 v2.25.0
gitlab.com/gitlab-org/api/client-go/v2 v2.26.0
go.yaml.in/yaml/v4 v4.0.0-rc.3
golang.org/x/crypto v0.51.0
golang.org/x/image v0.40.0
@@ -118,7 +117,7 @@ require (
golang.org/x/sync v0.20.0
golang.org/x/sys v0.44.0
golang.org/x/text v0.37.0
google.golang.org/grpc v1.81.0
google.golang.org/grpc v1.81.1
google.golang.org/protobuf v1.36.11
gopkg.in/ini.v1 v1.67.2
gopkg.in/yaml.v3 v3.0.1
@@ -272,8 +271,6 @@ require (
github.com/woodsbury/decimal128 v1.3.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
github.com/zeebo/xxh3 v1.1.0 // indirect
+6 -13
View File
@@ -304,8 +304,8 @@ github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmm
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.19.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc=
github.com/go-git/go-git/v5 v5.19.0/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
@@ -731,13 +731,6 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
@@ -760,8 +753,8 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
gitlab.com/gitlab-org/api/client-go/v2 v2.25.0 h1:ATTBB0Iiup5SRox2IPNSkkrGy/Any7FWBL1BOpZrpCU=
gitlab.com/gitlab-org/api/client-go/v2 v2.25.0/go.mod h1:OSJITkIrT0UuA3JCucEK9UEGcC1PWBkQg5WW6W4nWuo=
gitlab.com/gitlab-org/api/client-go/v2 v2.26.0 h1:1E35d1GRLb22Yq7Jr4gWptjW1+Vg7ZDGogK/SZ6ijV8=
gitlab.com/gitlab-org/api/client-go/v2 v2.26.0/go.mod h1:wx4w52UDENs3jolsL9cYT4byPNN9Pc6UpfGtCcHEIWY=
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/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
@@ -922,8 +915,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401020348-3a24fdc17823 h1:YedBIttDguBl/zy2wJauEUm+DZZg4UXseWj0g/3N+yo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401020348-3a24fdc17823/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
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-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+6
View File
@@ -436,6 +436,12 @@ type GetFeedsOptions struct {
DontCount bool // do counting in GetFeeds
}
func (opts *GetFeedsOptions) ApplyPublicOnly(publicOnly bool) {
if publicOnly {
opts.IncludePrivate = false
}
}
// ActivityReadable return whether doer can read activities of user
func ActivityReadable(user, doer *user_model.User) bool {
return !user.KeepActivityPrivate ||
+6
View File
@@ -54,6 +54,12 @@ type FindOrgOptions struct {
IncludeVisibility structs.VisibleType
}
func (opts *FindOrgOptions) ApplyPublicOnly(publicOnly bool) {
if publicOnly {
opts.IncludeVisibility = structs.VisibleTypePublic
}
}
func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder {
cond := builder.Eq{"uid": userID}
if !includePrivate {
+7
View File
@@ -5,12 +5,19 @@ package renderhelper
import (
"context"
"strings"
"testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/markup"
)
func testRenderString(ctx *markup.RenderContext, content string) (string, error) {
var buf strings.Builder
err := markup.Render(ctx, strings.NewReader(content), &buf)
return buf.String(), err
}
func TestMain(m *testing.M) {
unittest.MainTest(m, &unittest.TestOptions{
FixtureFiles: []string{"repository.yml", "user.yml"},
+4 -5
View File
@@ -8,7 +8,6 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"github.com/stretchr/testify/assert"
@@ -21,7 +20,7 @@ func TestRepoComment(t *testing.T) {
t.Run("AutoLink", func(t *testing.T) {
rctx := NewRenderContextRepoComment(t.Context(), repo1).WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
rendered, err := testRenderString(rctx, `
65f1bf27bc3bf70f64657658635e66094edbcb4d
#1
@user2
@@ -39,7 +38,7 @@ func TestRepoComment(t *testing.T) {
// It is Gitea's old behavior, the relative path is resolved to the repo path
// It is different from GitHub, GitHub resolves relative links to current page's path
rendered, err := markup.RenderString(rctx, `
rendered, err := testRenderString(rctx, `
[/test](/test)
[./test](./test)
![/image](/image)
@@ -59,7 +58,7 @@ func TestRepoComment(t *testing.T) {
WithMarkupType(markdown.MarkupName)
// the ref path is only used to render commit message: a commit message is rendered at the commit page with its commit ID path
rendered, err := markup.RenderString(rctx, `
rendered, err := testRenderString(rctx, `
[/test](/test)
[./test](./test)
![/image](/image)
@@ -75,7 +74,7 @@ func TestRepoComment(t *testing.T) {
t.Run("NoRepo", func(t *testing.T) {
rctx := NewRenderContextRepoComment(t.Context(), nil).WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, "any")
rendered, err := testRenderString(rctx, "any")
assert.NoError(t, err)
assert.Equal(t, "<p>any</p>\n", rendered)
})
+6 -7
View File
@@ -8,7 +8,6 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
_ "code.gitea.io/gitea/modules/markup/orgmode"
@@ -22,7 +21,7 @@ func TestRepoFile(t *testing.T) {
t.Run("AutoLink", func(t *testing.T) {
rctx := NewRenderContextRepoFile(t.Context(), repo1).WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
rendered, err := testRenderString(rctx, `
65f1bf27bc3bf70f64657658635e66094edbcb4d
#1
@user2
@@ -38,7 +37,7 @@ func TestRepoFile(t *testing.T) {
t.Run("AbsoluteAndRelative", func(t *testing.T) {
rctx := NewRenderContextRepoFile(t.Context(), repo1, RepoFileOptions{CurrentRefSubURL: "branch/main"}).
WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
rendered, err := testRenderString(rctx, `
[/test](/test)
[./test](./test)
![/image](/image)
@@ -56,7 +55,7 @@ func TestRepoFile(t *testing.T) {
t.Run("WithCurrentRefSubURL", func(t *testing.T) {
rctx := NewRenderContextRepoFile(t.Context(), repo1, RepoFileOptions{CurrentRefSubURL: "/commit/1234"}).
WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
rendered, err := testRenderString(rctx, `
[/test](/test)
![/image](/image)
`)
@@ -72,7 +71,7 @@ func TestRepoFile(t *testing.T) {
CurrentTreePath: "my-dir",
}).
WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
rendered, err := testRenderString(rctx, `
<img src="LINK">
<video src="LINK">
`)
@@ -93,7 +92,7 @@ func TestRepoFileOrgMode(t *testing.T) {
CurrentTreePath: "my-dir",
}).WithRelativePath("my-dir/a.org")
rendered, err := markup.RenderString(rctx, `
rendered, err := testRenderString(rctx, `
[[https://google.com/]]
[[ImageLink.svg][The Image Desc]]
`)
@@ -107,7 +106,7 @@ func TestRepoFileOrgMode(t *testing.T) {
t.Run("CodeHighlight", func(t *testing.T) {
rctx := NewRenderContextRepoFile(t.Context(), repo1, RepoFileOptions{}).WithRelativePath("my-dir/a.org")
rendered, err := markup.RenderString(rctx, `
rendered, err := testRenderString(rctx, `
#+begin_src c
int a = 1;
#+end_src
+3 -4
View File
@@ -8,7 +8,6 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"github.com/stretchr/testify/assert"
@@ -20,7 +19,7 @@ func TestRepoWiki(t *testing.T) {
t.Run("AutoLink", func(t *testing.T) {
rctx := NewRenderContextRepoWiki(t.Context(), repo1).WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
rendered, err := testRenderString(rctx, `
65f1bf27bc3bf70f64657658635e66094edbcb4d
#1
@user2
@@ -35,7 +34,7 @@ func TestRepoWiki(t *testing.T) {
t.Run("AbsoluteAndRelative", func(t *testing.T) {
rctx := NewRenderContextRepoWiki(t.Context(), repo1).WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
rendered, err := testRenderString(rctx, `
[/test](/test)
[./test](./test)
![/image](/image)
@@ -52,7 +51,7 @@ func TestRepoWiki(t *testing.T) {
t.Run("PathInTag", func(t *testing.T) {
rctx := NewRenderContextRepoWiki(t.Context(), repo1).WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
rendered, err := testRenderString(rctx, `
<img src="LINK">
<video src="LINK">
`)
+1 -2
View File
@@ -7,7 +7,6 @@ import (
"testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"github.com/stretchr/testify/assert"
@@ -16,7 +15,7 @@ import (
func TestSimpleDocument(t *testing.T) {
unittest.PrepareTestEnv(t)
rctx := NewRenderContextSimpleDocument(t.Context(), "/base").WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
rendered, err := testRenderString(rctx, `
65f1bf27bc3bf70f64657658635e66094edbcb4d
#1
@user2
+7
View File
@@ -212,6 +212,13 @@ type SearchRepoOptions struct {
OnlyShowRelevant bool
}
func (opts *SearchRepoOptions) ApplyPublicOnly(publicOnly bool) {
if publicOnly {
opts.Private = false
opts.AllLimited = false
}
}
// UserOwnedRepoCond returns user ownered repositories
func UserOwnedRepoCond(userID int64) builder.Cond {
return builder.Eq{
+12
View File
@@ -24,6 +24,12 @@ type StarredReposOptions struct {
IncludePrivate bool
}
func (opts *StarredReposOptions) ApplyPublicOnly(publicOnly bool) {
if publicOnly {
opts.IncludePrivate = false
}
}
func (opts *StarredReposOptions) ToConds() builder.Cond {
var cond builder.Cond = builder.Eq{
"star.uid": opts.StarrerID,
@@ -62,6 +68,12 @@ type WatchedReposOptions struct {
IncludePrivate bool
}
func (opts *WatchedReposOptions) ApplyPublicOnly(publicOnly bool) {
if publicOnly {
opts.IncludePrivate = false
}
}
func (opts *WatchedReposOptions) ToConds() builder.Cond {
var cond builder.Cond = builder.Eq{
"watch.user_id": opts.WatcherID,
+6
View File
@@ -58,6 +58,12 @@ type SearchUserOptions struct {
IncludeReserved bool
}
func (opts *SearchUserOptions) ApplyPublicOnly(publicOnly bool) {
if publicOnly {
opts.Visible = []structs.VisibleType{structs.VisibleTypePublic}
}
}
func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) db.Session {
var cond builder.Cond
cond = builder.In("type", opts.Types)
+29
View File
@@ -0,0 +1,29 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"io"
"code.gitea.io/gitea/modules/setting"
)
type tagType string
// BuildSignature builds a hmac signature for the input values.
// "tag" is an internal pre-defined static string to distinguish the signatures for different purpose.
func BuildSignature(tag tagType, vals ...string) []byte {
m := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret())
_, _ = io.WriteString(m, string(tag))
var buf8 [8]byte
for _, v := range vals {
binary.LittleEndian.PutUint64(buf8[:], uint64(len(v)))
_, _ = m.Write(buf8[:])
_, _ = io.WriteString(m, v)
}
return m.Sum(nil)
}
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestBuildSignature(t *testing.T) {
a := BuildSignature("v0", "x")
b := BuildSignature("v0", "x")
assert.Equal(t, a, b)
a = BuildSignature("v0", "x", "yz")
b = BuildSignature("v0", "xy", "z")
assert.NotEqual(t, a, b)
a = BuildSignature("v1", "x")
b = BuildSignature("v2", "x")
assert.NotEqual(t, a, b)
a = BuildSignature("v0", "x")
b = BuildSignature("v0x")
assert.NotEqual(t, a, b)
a = BuildSignature("v0", "", "x")
b = BuildSignature("v0", "x", "")
assert.NotEqual(t, a, b)
a = BuildSignature("v0")
b = BuildSignature("v0")
assert.Equal(t, a, b)
}
+1 -1
View File
@@ -22,7 +22,7 @@ func TestRenderCodePreview(t *testing.T) {
},
})
test := func(input, expected string) {
buffer, err := markup.RenderString(markup.NewTestRenderContext().WithMarkupType(markdown.MarkupName), input)
buffer, err := testRenderString(markup.NewTestRenderContext().WithMarkupType(markdown.MarkupName), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
+11 -5
View File
@@ -24,10 +24,16 @@ var (
localMetas = map[string]string{"user": testRepoOwnerName, "repo": testRepoName}
)
func testRenderString(ctx *markup.RenderContext, content string) (string, error) {
var buf strings.Builder
err := markup.Render(ctx, strings.NewReader(content), &buf)
return buf.String(), err
}
func TestRender_Commits(t *testing.T) {
test := func(input, expected string) {
rctx := markup.NewTestRenderContext(markup.TestAppURL, localMetas).WithRelativePath("a.md")
buffer, err := markup.RenderString(rctx, input)
buffer, err := testRenderString(rctx, input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
@@ -75,7 +81,7 @@ func TestRender_CrossReferences(t *testing.T) {
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
test := func(input, expected string) {
rctx := markup.NewTestRenderContext(markup.TestAppURL, localMetas).WithRelativePath("a.md")
buffer, err := markup.RenderString(rctx, input)
buffer, err := testRenderString(rctx, input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
@@ -119,7 +125,7 @@ func TestRender_links(t *testing.T) {
setting.AppURL = markup.TestAppURL
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
test := func(input, expected string) {
buffer, err := markup.RenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input)
buffer, err := testRenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
@@ -234,7 +240,7 @@ func TestRender_email(t *testing.T) {
setting.AppURL = markup.TestAppURL
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
test := func(input, expected string) {
res, err := markup.RenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input)
res, err := testRenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res), "input: %s", input)
}
@@ -321,7 +327,7 @@ func TestRender_emoji(t *testing.T) {
test := func(input, expected string) {
expected = strings.ReplaceAll(expected, "&", "&amp;")
buffer, err := markup.RenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input)
buffer, err := testRenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
+21
View File
@@ -119,12 +119,33 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(KindDetails, r.renderDetails)
reg.Register(KindSummary, r.renderSummary)
reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
reg.Register(ast.KindCodeBlock, r.renderCodeBlock)
reg.Register(KindAttention, r.renderAttention)
reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem)
reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
reg.Register(KindRawHTML, r.renderRawHTML)
}
// renderCodeBlock wraps indented code blocks like the fenced renderer
func (r *HTMLRenderer) renderCodeBlock(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
if entering {
opening := r.renderInternal.ProtectSafeAttrs(`<div class="code-block-container code-overflow-scroll"><pre class="code-block"><code>`)
if _, err := w.WriteString(string(opening)); err != nil {
return ast.WalkStop, err
}
lines := n.Lines()
for i := 0; i < lines.Len(); i++ {
line := lines.At(i)
r.Writer.RawWrite(w, line.Value(source))
}
} else {
if _, err := w.WriteString("</code></pre></div>"); err != nil {
return ast.WalkStop, err
}
}
return ast.WalkContinue, nil
}
func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.Document)
+4 -1
View File
@@ -11,6 +11,7 @@ import (
"io"
"strings"
"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/common"
@@ -266,7 +267,9 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
func RenderString(ctx *markup.RenderContext, content string) (template.HTML, error) {
var buf strings.Builder
if err := Render(ctx, strings.NewReader(content), &buf); err != nil {
return "", err
log.Warn("Unable to RenderString: %v, content: %s", err, giteautil.TruncateRunes(content, 200))
err = nil
return htmlutil.EscapeString(content), err
}
return template.HTML(buf.String()), nil
}
+3 -1
View File
@@ -601,7 +601,7 @@ func TestMarkdownUlDir(t *testing.T) {
`, string(result))
}
func TestMarkdownFencedCodeBlock(t *testing.T) {
func TestMarkdownCodeBlock(t *testing.T) {
testRender := func(input, expected string) {
buffer, err := markdown.RenderString(markup.NewTestRenderContext(), input)
assert.NoError(t, err)
@@ -618,4 +618,6 @@ func TestMarkdownFencedCodeBlock(t *testing.T) {
testRender("```js:app.ts\ncode\n```", jsCommon)
testRender("```js,ignore\ncode\n```", jsCommon)
testRender("```js ignore\ncode\n```", jsCommon)
testRender(" code\n", prefix+`<code>code`+nl+`</code>`+suffix)
testRender(" <script>alert(1)</script>\n", prefix+`<code>&lt;script&gt;alert(1)&lt;/script&gt;`+nl+`</code>`+suffix)
}
-9
View File
@@ -198,15 +198,6 @@ func Render(rctx *RenderContext, origInput io.Reader, output io.Writer) error {
return RenderWithRenderer(rctx, renderer, input, output)
}
// RenderString renders Markup string to HTML with all specific handling stuff and return string
func RenderString(ctx *RenderContext, content string) (string, error) {
var buf strings.Builder
if err := Render(ctx, strings.NewReader(content), &buf); err != nil {
return "", err
}
return buf.String(), nil
}
func RenderIFrame(ctx *RenderContext, opts *ExternalRendererOptions, output io.Writer) error {
ownerName, repoName := ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"]
refSubURL := ctx.RenderOptions.Metas["RefTypeNameSubURL"]
+1 -1
View File
@@ -237,7 +237,7 @@ var (
AddCoCommitterTrailers: true,
RetargetChildrenOnMerge: true,
DelayCheckForInactiveDays: 7,
DefaultTitleSource: RepoPRTitleSourceFirstCommit,
DefaultTitleSource: RepoPRTitleSourceAuto,
},
// Issue settings
+4 -7
View File
@@ -155,7 +155,6 @@ func sessionHandler(session ssh.Session) {
process.SetSysProcAttribute(cmd)
wg := &sync.WaitGroup{}
wg.Add(2)
if err = cmd.Start(); err != nil {
log.Error("SSH: Start: %v", err)
@@ -169,21 +168,19 @@ func sessionHandler(session ssh.Session) {
}
}()
go func() {
defer wg.Done()
wg.Go(func() {
defer stdout.Close()
if _, err := io.Copy(session, stdout); err != nil {
log.Error("Failed to write stdout to session. %s", err)
}
}()
})
go func() {
defer wg.Done()
wg.Go(func() {
defer stderr.Close()
if _, err := io.Copy(session.Stderr(), stderr); err != nil {
log.Error("Failed to write stderr to session. %s", err)
}
}()
})
// Ensure all the output has been written before we wait on the command
// to exit.
@@ -54,17 +54,14 @@ func TestScopedTemplateSetFuncMap(t *testing.T) {
out1 := bytes.Buffer{}
out2 := bytes.Buffer{}
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
wg.Go(func() {
err := ts.newExecutor(funcMap1).Execute(&out1, nil)
assert.NoError(t, err)
wg.Done()
}()
go func() {
})
wg.Go(func() {
err := ts.newExecutor(funcMap2).Execute(&out2, nil)
assert.NoError(t, err)
wg.Done()
}()
})
wg.Wait()
assert.Equal(t, "base1\ntest1\nbase1\ntest1", out1.String())
assert.Equal(t, "base2\ntest2\nbase2\ntest2", out2.String())
+15 -34
View File
@@ -121,23 +121,7 @@ func (ut *RenderUtils) RenderIssueSimpleTitle(text string) template.HTML {
return ret
}
func (ut *RenderUtils) RenderLabelWithLink(label *issues_model.Label, link any) template.HTML {
var attrHref template.HTML
switch link.(type) {
case template.URL, string:
attrHref = htmlutil.HTMLFormat(`href="%s"`, link)
default:
panic(fmt.Sprintf("unexpected type %T for link", link))
}
return ut.renderLabelWithTag(label, "a", attrHref)
}
func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML {
return ut.renderLabelWithTag(label, "span", "")
}
// RenderLabel renders a label
func (ut *RenderUtils) renderLabelWithTag(label *issues_model.Label, tagName, tagAttrs template.HTML) template.HTML {
locale := ut.ctx.Value(translation.ContextKey).(translation.Locale)
var extraCSSClasses string
textColor := util.ContrastColor(label.Color)
@@ -151,8 +135,8 @@ func (ut *RenderUtils) renderLabelWithTag(label *issues_model.Label, tagName, ta
if labelScope == "" {
// Regular label
return htmlutil.HTMLFormat(`<%s %s class="ui label %s" style="color: %s !important; background-color: %s !important;" data-tooltip-content title="%s"><span class="gt-ellipsis">%s</span></%s>`,
tagName, tagAttrs, extraCSSClasses, textColor, label.Color, descriptionText, ut.RenderEmoji(label.Name), tagName)
return htmlutil.HTMLFormat(`<span class="ui label %s" style="color: %s !important; background-color: %s !important;" data-tooltip-content title="%s"><span class="gt-ellipsis">%s</span></span>`,
extraCSSClasses, textColor, label.Color, descriptionText, ut.RenderEmoji(label.Name))
}
// Scoped label
@@ -187,29 +171,25 @@ func (ut *RenderUtils) renderLabelWithTag(label *issues_model.Label, tagName, ta
if label.ExclusiveOrder > 0 {
// <scope> | <label> | <order>
return htmlutil.HTMLFormat(`<%s %s class="ui label %s scope-parent" data-tooltip-content title="%s">`+
return htmlutil.HTMLFormat(`<span class="ui label %s scope-parent" data-tooltip-content title="%s">`+
`<div class="ui label scope-left" style="color: %s !important; background-color: %s !important">%s</div>`+
`<div class="ui label scope-middle" style="color: %s !important; background-color: %s !important">%s</div>`+
`<div class="ui label scope-right">%d</div>`+
`</%s>`,
tagName, tagAttrs,
`</span>`,
extraCSSClasses, descriptionText,
textColor, scopeColor, scopeHTML,
textColor, itemColor, itemHTML,
label.ExclusiveOrder,
tagName)
label.ExclusiveOrder)
}
// <scope> | <label>
return htmlutil.HTMLFormat(`<%s %s class="ui label %s scope-parent" data-tooltip-content title="%s">`+
return htmlutil.HTMLFormat(`<span class="ui label %s scope-parent" data-tooltip-content title="%s">`+
`<div class="ui label scope-left" style="color: %s !important; background-color: %s !important">%s</div>`+
`<div class="ui label scope-right" style="color: %s !important; background-color: %s !important">%s</div>`+
`</%s>`,
tagName, tagAttrs,
`</span>`,
extraCSSClasses, descriptionText,
textColor, scopeColor, scopeHTML,
textColor, itemColor, itemHTML,
tagName)
textColor, itemColor, itemHTML)
}
// RenderEmoji renders html text with emoji post processors
@@ -246,18 +226,19 @@ func (ut *RenderUtils) MarkdownToHtml(input string) template.HTML { //nolint:rev
func (ut *RenderUtils) RenderLabels(labels []*issues_model.Label, repoLink string, issue *issues_model.Issue) template.HTML {
isPullRequest := issue != nil && issue.IsPull
baseLink := fmt.Sprintf("%s/%s", repoLink, util.Iif(isPullRequest, "pulls", "issues"))
var htmlCode strings.Builder
htmlCode.WriteString(`<span class="labels-list">`)
var htmlCode htmlutil.HTMLBuilder
htmlCode.WriteHTML(`<span class="labels-list">`)
for _, label := range labels {
// Protect against nil value in labels - shouldn't happen but would cause a panic if so
if label == nil {
continue
}
link := fmt.Sprintf("%s?labels=%d", baseLink, label.ID)
htmlCode.WriteString(string(ut.RenderLabelWithLink(label, template.URL(link))))
htmlCode.WriteFormat(`<a class="item" href="%s?labels=%d">`, baseLink, label.ID)
htmlCode.WriteHTML(ut.RenderLabel(label))
htmlCode.WriteHTML("</a>")
}
htmlCode.WriteString("</span>")
return template.HTML(htmlCode.String())
htmlCode.WriteHTML("</span>")
return htmlCode.HTMLString()
}
func (ut *RenderUtils) RenderThemeItem(info *webtheme.ThemeMetaInfo, iconSize int) template.HTML {
+6 -7
View File
@@ -206,16 +206,15 @@ func TestRenderLabels(t *testing.T) {
expected = `/owner/repo/pulls?labels=123`
assert.Contains(t, ut.RenderLabels([]*issues.Label{label}, "/owner/repo", issue), expected)
expectedLabel := `<a href="&lt;&gt;" class="ui label " style="color: #fff !important; background-color: label-color !important;" data-tooltip-content title=""><span class="gt-ellipsis">label-name</span></a>`
assert.Equal(t, expectedLabel, string(ut.RenderLabelWithLink(label, "<>")))
assert.Equal(t, expectedLabel, string(ut.RenderLabelWithLink(label, template.URL("<>"))))
expectedLabel := `<span class="ui label " style="color: #fff !important; background-color: label-color !important;" data-tooltip-content title=""><span class="gt-ellipsis">label-name</span></span>`
assert.Equal(t, expectedLabel, string(ut.RenderLabel(label)))
label = &issues.Label{ID: 123, Name: "</>", Exclusive: true}
expectedLabel = `<a href="" class="ui label scope-parent" data-tooltip-content title=""><div class="ui label scope-left" style="color: #fff !important; background-color: #000000 !important">&lt;</div><div class="ui label scope-right" style="color: #fff !important; background-color: #000000 !important">&gt;</div></a>`
assert.Equal(t, expectedLabel, string(ut.RenderLabelWithLink(label, "")))
expectedLabel = `<span class="ui label scope-parent" data-tooltip-content title=""><div class="ui label scope-left" style="color: #fff !important; background-color: #000000 !important">&lt;</div><div class="ui label scope-right" style="color: #fff !important; background-color: #000000 !important">&gt;</div></span>`
assert.Equal(t, expectedLabel, string(ut.RenderLabel(label)))
label = &issues.Label{ID: 123, Name: "</>", Exclusive: true, ExclusiveOrder: 1}
expectedLabel = `<a href="" class="ui label scope-parent" data-tooltip-content title=""><div class="ui label scope-left" style="color: #fff !important; background-color: #000000 !important">&lt;</div><div class="ui label scope-middle" style="color: #fff !important; background-color: #000000 !important">&gt;</div><div class="ui label scope-right">1</div></a>`
assert.Equal(t, expectedLabel, string(ut.RenderLabelWithLink(label, "")))
expectedLabel = `<span class="ui label scope-parent" data-tooltip-content title=""><div class="ui label scope-left" style="color: #fff !important; background-color: #000000 !important">&lt;</div><div class="ui label scope-middle" style="color: #fff !important; background-color: #000000 !important">&gt;</div><div class="ui label scope-right">1</div></span>`
assert.Equal(t, expectedLabel, string(ut.RenderLabel(label)))
}
func TestUserMention(t *testing.T) {
+1
View File
@@ -105,6 +105,7 @@
"copy_error": "Copy failed",
"copy_type_unsupported": "This file type cannot be copied",
"copy_filename": "Copy filename",
"copy_output": "Copy output",
"write": "Write",
"preview": "Preview",
"loading": "Loading…",
+21 -7
View File
@@ -313,7 +313,6 @@
"install.admin_email": "Courriel",
"install.install_btn_confirm": "Installer Gitea",
"install.test_git_failed": "Le test de la commande \"git\" a échoué : %v",
"install.sqlite3_not_available": "Cette version de Gitea ne supporte pas SQLite3. Veuillez télécharger la version binaire officielle de %s (pas la version 'gobuild').",
"install.invalid_db_setting": "Les paramètres de la base de données sont invalides : %v",
"install.invalid_db_table": "La table \"%s\" de la base de données est invalide : %v",
"install.invalid_repo_path": "Le chemin racine du dépôt est invalide : %v",
@@ -1306,7 +1305,7 @@
"repo.editor.upload_file_is_locked": "Le fichier \"%s\" est verrouillé par %s.",
"repo.editor.upload_files_to_dir": "Téléverser les fichiers vers \"%s\"",
"repo.editor.cannot_commit_to_protected_branch": "Impossible de créer une révision sur la branche protégée \"%s\".",
"repo.editor.no_commit_to_branch": "Impossible d'enregistrer la révision directement sur la branche parce que :",
"repo.editor.no_commit_to_branch": "Impossible de réviser cette branche car :",
"repo.editor.user_no_push_to_branch": "L'utilisateur ne peut pas pousser vers la branche",
"repo.editor.require_signed_commit": "Cette branche nécessite une révision signée",
"repo.editor.cherry_pick": "Picorer %s vers:",
@@ -1322,7 +1321,7 @@
"repo.commits.desc": "Naviguer dans l'historique des modifications.",
"repo.commits.commits": "Révisions",
"repo.commits.no_commits": "Pas de révisions en commun. \"%s\" et \"%s\" ont des historiques entièrement différents.",
"repo.commits.nothing_to_compare": "Ces branches sont égales.",
"repo.commits.nothing_to_compare": "Ces révisions sont équivalentes.",
"repo.commits.search.tooltip": "Vous pouvez utiliser les mots-clés \"author:\", \"committer:\", \"after:\", ou \"before:\" pour filtrer votre recherche, ex.: \"revert author:Alice before:2019-01-13\".",
"repo.commits.search_branch": "Cette branche",
"repo.commits.search_all": "Toutes les branches",
@@ -1784,9 +1783,9 @@
"repo.pulls.select_commit_hold_shift_for_range": "Maintenir Maj et cliquer sur des révisions pour sélectionner un ensemble de révisions.",
"repo.pulls.review_only_possible_for_full_diff": "Une évaluation n'est possible que lorsque vous affichez le différentiel complet.",
"repo.pulls.filter_changes_by_commit": "Filtrer par révision",
"repo.pulls.nothing_to_compare": "Ces branches sont identiques. Il ny a pas besoin de créer une demande d'ajout.",
"repo.pulls.nothing_to_compare": "Ces branches ou étiquettes sont équivalentes, il nest pas nécessaire de créer une demande dajout.",
"repo.pulls.no_common_history": "Ces branches ne partagent pas de base de fusion commune. Sélectionnez une autre base ou branche de comparaison.",
"repo.pulls.nothing_to_compare_have_tag": "Les branches/étiquettes sélectionnées sont équivalentes.",
"repo.pulls.nothing_to_compare_have_tag": "Ces branches ou étiquettes sont équivalentes.",
"repo.pulls.nothing_to_compare_and_allow_empty_pr": "Ces branches sont égales. Cette demande d'ajout sera vide.",
"repo.pulls.has_pull_request": "'Il existe déjà une demande d'ajout entre ces deux branches : <a href=\"%[1]s\">%[2]s#%[3]d</a>'",
"repo.pulls.create": "Créer une demande d'ajout",
@@ -1820,6 +1819,7 @@
"repo.pulls.required_status_check_failed": "Certains contrôles requis n'ont pas réussi.",
"repo.pulls.required_status_check_missing": "Certains contrôles requis sont manquants.",
"repo.pulls.required_status_check_administrator": "En tant qu'administrateur, vous pouvez toujours fusionner cette requête de pull.",
"repo.pulls.required_status_check_bypass_allowlist": "Vous êtes autorisé à contourner les règles de protection pour cette fusion.",
"repo.pulls.blocked_by_approvals": "Cette demande dajout nest pas suffisamment approuvée. %d approbations obtenues sur %d.",
"repo.pulls.blocked_by_approvals_whitelisted": "Cette demande dajout na pas encore assez dapprobations. %d sur %d approbations de la part des utilisateurs ou équipes sur la liste autorisée.",
"repo.pulls.blocked_by_rejection": "Cette demande dajout nécessite des corrections sollicitées par un évaluateur officiel.",
@@ -2139,7 +2139,9 @@
"repo.settings.pulls_desc": "Activer les demandes de fusion",
"repo.settings.pulls.ignore_whitespace": "Ignorer les espaces lors des conflits",
"repo.settings.pulls.enable_autodetect_manual_merge": "Activer la détection automatique de la fusion manuelle (Remarque : dans certains cas particuliers, des erreurs de détection peuvent se produire)",
"repo.settings.pulls.allow_merge_update": "Activer la mise à jour dune demande dajout par rebase",
"repo.settings.pulls.allow_rebase_update": "Activer la mise à jour de demande d'ajout par rebase",
"repo.settings.pulls.default_update_style": "Style de mise à jour de la branche par défaut",
"repo.settings.pulls.default_target_branch": "Branche cible par défaut pour les nouvelles demandes dajout",
"repo.settings.pulls.default_target_branch_default": "Branche par défaut (%s)",
"repo.settings.pulls.default_delete_branch_after_merge": "Supprimer la branche après la fusion par default",
@@ -2414,6 +2416,11 @@
"repo.settings.protect_merge_whitelist_committers_desc": "Nautoriser que les utilisateurs et les équipes listés à appliquer les demandes de fusion sur cette branche.",
"repo.settings.protect_merge_whitelist_users": "Utilisateurs autorisés à fusionner :",
"repo.settings.protect_merge_whitelist_teams": "Équipes autorisées à fusionner :",
"repo.settings.protect_bypass_allowlist": "Contourner la protection de la branche",
"repo.settings.protect_enable_bypass_allowlist": "Autoriser des utilisateurs et des équipes à contourner les restrictions de branche",
"repo.settings.protect_enable_bypass_allowlist_desc": "Les utilisateurs ou équipes autorisés peuvent fusionner ou pousser des changements nonobstant les règles dapprobations, de vérifications de statut et les protections fichiers.",
"repo.settings.protect_bypass_allowlist_users": "Liste dutilisateurs autorisés à contourner les protections :",
"repo.settings.protect_bypass_allowlist_teams": "Liste d’équipes autorisées à contourner les protections :",
"repo.settings.protect_check_status_contexts": "Activer le Contrôle Qualité",
"repo.settings.protect_status_check_patterns": "Motifs de vérification des statuts :",
"repo.settings.protect_status_check_patterns_desc": "Entrez des motifs pour spécifier quelles vérifications doivent réussir avant que des branches puissent être fusionnées. Un motif par ligne. Un motif ne peut être vide.",
@@ -2455,7 +2462,7 @@
"repo.settings.block_outdated_branch": "Bloquer la fusion si la demande d'ajout est obsolète",
"repo.settings.block_outdated_branch_desc": "La fusion ne sera pas possible lorsque la branche principale est derrière la branche de base.",
"repo.settings.block_admin_merge_override": "Les administrateurs doivent respecter les règles de protection des branches",
"repo.settings.block_admin_merge_override_desc": "Les administrateurs doivent respecter les règles de protection des branches et ne peuvent pas les contourner.",
"repo.settings.block_admin_merge_override_desc": "Les administrateurs sont également soumis aux règles de protection des branches. En activant la liste dautorisation de contournement, les utilisateurs et équipes qui y figurent peuvent toujours contourner ces règles.",
"repo.settings.default_branch_desc": "Sélectionnez une branche par défaut pour les révisions.",
"repo.settings.default_target_branch_desc": "Les demandes dajout peuvent utiliser une branche cible différente, telle que définie dans la section Demandes dajouts des Paramètres avancés du dépôt.",
"repo.settings.merge_style_desc": "Styles de fusion",
@@ -3623,7 +3630,13 @@
"packages.terraform.delete.latest": "La dernière version dun état Terraform ne peut pas être supprimée.",
"packages.vagrant.install": "Pour ajouter une machine Vagrant, exécutez la commande suivante :",
"packages.settings.link": "Lier ce paquet à un dépôt",
"packages.settings.link.description": "Si vous associez un paquet à un dépôt, le paquet sera inclus dans sa liste des paquets. Seul les dépôts dun même propriétaire peuvent être associés. Laisser ce champ vide supprimera le lien.",
"packages.settings.link.description": "Si vous liez un paquet à dépôt, il sera visible dans sa liste des paquets.",
"packages.settings.link.notice1": "Seuls les dépôts du même propriétaire peuvent être liés.",
"packages.settings.link.notice2": "Lier un dépôt ne modifie pas la visibilité du paquet.",
"packages.settings.link.notice3": "Laisser le champ vide supprimera le lien.",
"packages.settings.visibility": "Visibilité du paquet",
"packages.settings.visibility.inherit": "La visibilité du paquet est héritée depuis le propriétaire et ne peut pas être modifiée ici. Pour la modifier, mettez à jour les paramètres de visibilité de lutilisateur ou de l'organisation propriétaire de ce paquet.",
"packages.settings.visibility.button": "Modifier la visibilité du propriétaire",
"packages.settings.link.select": "Sélectionner un dépôt",
"packages.settings.link.button": "Actualiser le lien du dépôt",
"packages.settings.link.success": "Le lien du dépôt a été mis à jour avec succès.",
@@ -3697,6 +3710,7 @@
"actions.status.success": "Succès",
"actions.status.failure": "Échec",
"actions.status.cancelled": "Annulé",
"actions.status.cancelling": "Annulation",
"actions.status.skipped": "Ignoré",
"actions.status.blocked": "Bloqué",
"actions.runners": "Exécuteurs",
+10 -1
View File
@@ -1819,6 +1819,7 @@
"repo.pulls.required_status_check_failed": "Níor éirigh le roinnt seiceálacha riachtanacha.",
"repo.pulls.required_status_check_missing": "Tá roinnt seiceanna riachtanacha ar iarraidh.",
"repo.pulls.required_status_check_administrator": "Mar riarthóir, féadfaidh tú an t-iarratas tarraingthe seo a chumasc fós.",
"repo.pulls.required_status_check_bypass_allowlist": "Ceadaítear duit rialacha cosanta brainse a sheachbhóthar don chumasc seo.",
"repo.pulls.blocked_by_approvals": "Níl go leor ceadaithe ag an iarraidh tarraingthe seo fós. Deonaíodh %d den fhaomhadh %d.",
"repo.pulls.blocked_by_approvals_whitelisted": "Níl go leor ceaduithe riachtanacha ag an iarratas tarraingte seo go fóill. %d de %d faomhadh tugtha ó úsáideoirí nó foirne ar an liosta ceadaithe.",
"repo.pulls.blocked_by_rejection": "Tá athruithe ag athbhreithneoir oifigiúil ag an iarratas tarraingthe seo.",
@@ -2138,7 +2139,9 @@
"repo.settings.pulls_desc": "Cumasaigh Iarratais Tarraingthe Stóras",
"repo.settings.pulls.ignore_whitespace": "Déan neamhaird de spás bán le haghaidh coinbhleachtaí",
"repo.settings.pulls.enable_autodetect_manual_merge": "Cumasaigh cumasc láimhe autodetector (Nóta: I roinnt cásanna speisialta, is féidir míbhreithiúnais tarlú)",
"repo.settings.pulls.allow_merge_update": "Cumasaigh brainse iarratais tarraingthe a nuashonrú trí chumasc",
"repo.settings.pulls.allow_rebase_update": "Cumasaigh brainse iarratais tarraingthe a nuashonrú trí athbhunú",
"repo.settings.pulls.default_update_style": "Stíl réamhshocraithe nuashonraithe brainse",
"repo.settings.pulls.default_target_branch": "Brainse sprice réamhshocraithe le haghaidh iarratais tarraingthe nua",
"repo.settings.pulls.default_target_branch_default": "Brainse réamhshocraithe (%s)",
"repo.settings.pulls.default_delete_branch_after_merge": "Scrios brainse an iarratais tarraingthe tar éis cumasc de réir réamhshocraithe",
@@ -2413,6 +2416,11 @@
"repo.settings.protect_merge_whitelist_committers_desc": "Ní lig ach d'úsáideoirí nó d'fhoirne liostaithe iarratais tarraingthe isteach sa bhrainse seo a chumasc.",
"repo.settings.protect_merge_whitelist_users": "Úsáideoirí ar an Liosta Ceadaithe le haghaidh cumasc:",
"repo.settings.protect_merge_whitelist_teams": "Foirne ar an Liosta Ceadaithe le haghaidh cumasc:",
"repo.settings.protect_bypass_allowlist": "Cosaint brainse seachbhóthair",
"repo.settings.protect_enable_bypass_allowlist": "Ceadaigh d’úsáideoirí nó dfhoirne roghnaithe cosaint brainse a sheachbhóthar",
"repo.settings.protect_enable_bypass_allowlist_desc": "Is féidir le húsáideoirí nó foirne atá ar an liosta ceadaithe cumasc nó brú fiú nuair a chuirfeadh ceaduithe riachtanacha, seiceálacha stádais nó rialacha comhad cosanta bac orthu murach sin.",
"repo.settings.protect_bypass_allowlist_users": "Úsáideoirí ar an liosta ceadanna chun cosaint a sheachbhóthar:",
"repo.settings.protect_bypass_allowlist_teams": "Foirne ar an liosta ceadaithe chun cosaint a sheachbhóthar:",
"repo.settings.protect_check_status_contexts": "Cumasaigh Seiceáil Stádas",
"repo.settings.protect_status_check_patterns": "Patrúin seiceála stádais:",
"repo.settings.protect_status_check_patterns_desc": "Iontráil patrúin chun a shonrú cé na seiceálacha stádais a chaithfidh pas a fháil sular féidir brainsí a chumasc le brainse a chomhoibríonn leis an riail seo. Sonraíonn gach líne patrún. Ní féidir patrúin a bheith folamh.",
@@ -2454,7 +2462,7 @@
"repo.settings.block_outdated_branch": "Cuir bac ar chumasc má tá an t-iarratas tarraingthe as dáta",
"repo.settings.block_outdated_branch_desc": "Ní bheidh cumasc indéanta nuair a bhíonn ceannbhrainse taobh thiar de bhronnbhrainse.",
"repo.settings.block_admin_merge_override": "Ní mór do riarthóirí rialacha cosanta brainse a leanúint",
"repo.settings.block_admin_merge_override_desc": "Ní mór do riarthóirí rialacha cosanta brainse a leanúint agus ní féidir leo iad a sheachaint.",
"repo.settings.block_admin_merge_override_desc": "Ní mór do riarthóirí rialacha cosanta brainse a leanúint agus ní féidir leo dul timpeall orthu. Is féidir le húsáideoirí nó foirne sa liosta ceadanna seachbhóthair na rialacha seo a sheachbhóthar fós má tá an liosta ceadanna seachbhóthair cumasaithe.",
"repo.settings.default_branch_desc": "Roghnaigh brainse réamhshocraithe le haghaidh tiomnuithe cóid.",
"repo.settings.default_target_branch_desc": "Is féidir le hiarratais tarraingthe brainse sprice réamhshocraithe difriúil a úsáid má tá sé socraithe sa rannán Iarratais Tarraingthe de na Socruithe Ardleibhéil Stórála.",
"repo.settings.merge_style_desc": "Stíleanna Cumaisc",
@@ -3702,6 +3710,7 @@
"actions.status.success": "Rath",
"actions.status.failure": "Teip",
"actions.status.cancelled": "Cealaíodh",
"actions.status.cancelling": "Ag cealú",
"actions.status.skipped": "Scipeáilte",
"actions.status.blocked": "Blocáilte",
"actions.runners": "Reathaitheoirí",
+2 -2
View File
@@ -51,7 +51,7 @@
"idiomorph": "0.7.4",
"jquery": "4.0.0",
"js-yaml": "4.1.1",
"katex": "0.16.45",
"katex": "0.16.46",
"mermaid": "11.15.0",
"online-3d-viewer": "0.18.0",
"pdfobject": "2.3.1",
@@ -123,6 +123,6 @@
"typescript-eslint": "8.59.3",
"updates": "17.16.11",
"vitest": "4.1.6",
"vue-tsc": "3.2.8"
"vue-tsc": "3.2.9"
}
}
+20 -20
View File
@@ -166,8 +166,8 @@ importers:
specifier: 4.1.1
version: 4.1.1
katex:
specifier: 0.16.45
version: 0.16.45
specifier: 0.16.46
version: 0.16.46
mermaid:
specifier: 11.15.0
version: 11.15.0
@@ -374,8 +374,8 @@ importers:
specifier: 4.1.6
version: 4.1.6(@types/node@25.7.0)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.12(@types/node@25.7.0)(esbuild@0.28.0)(jiti@2.7.0))
vue-tsc:
specifier: 3.2.8
version: 3.2.8(typescript@6.0.3)
specifier: 3.2.9
version: 3.2.9(typescript@6.0.3)
packages:
@@ -1753,8 +1753,8 @@ packages:
'@vue/compiler-ssr@3.5.34':
resolution: {integrity: sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==}
'@vue/language-core@3.2.8':
resolution: {integrity: sha512-9OiSPQFiAAWNVnXb0d2dcTmcKnFQamhuNES6ayyISrb/mwPWVgoGdAqSfCWqKhQpa3D5gDTcYD+w7ObiheZ81g==}
'@vue/language-core@3.2.9':
resolution: {integrity: sha512-ie0ojt/0fU/GfIogh+zgHbaYRPlt9S+cLOxcWwF7nTSFh897BVgnFKL2byT4kpp1mlqYWZ2psGwSniyE2xsxYw==}
'@vue/reactivity@3.5.34':
resolution: {integrity: sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==}
@@ -1800,8 +1800,8 @@ packages:
ajv@8.20.0:
resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
alien-signals@3.1.2:
resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==}
alien-signals@3.2.1:
resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
@@ -3101,8 +3101,8 @@ packages:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'}
katex@0.16.45:
resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==}
katex@0.16.46:
resolution: {integrity: sha512-WHy4Coo+bGZyH7NwJKHkS04YFsFcarWbAEOAC3EMndzdN6VSZqklLLIgfxzyaW9jDoeGYJX9SWbJPKpecox0Uw==}
hasBin: true
keyv@4.5.4:
@@ -4258,8 +4258,8 @@ packages:
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
vue-tsc@3.2.8:
resolution: {integrity: sha512-27vTLJ6Q2370obOd0PFYoYoKnmXJ521uUIedrs3Zhhhg/8YG10VOCMmwt+JQslatpAMTDbnWiitLnoD5VlIvog==}
vue-tsc@3.2.9:
resolution: {integrity: sha512-qm8/nbo+9eZc1SCndm9wT+gq23pM+wRIdHY0wjm83B3lIginHTwcdrLUyTrKjDWXbMVNjKegNrnymhpdqnCL3A==}
hasBin: true
peerDependencies:
typescript: '>=5.0.0'
@@ -5928,12 +5928,12 @@ snapshots:
'@vue/compiler-dom': 3.5.34
'@vue/shared': 3.5.34
'@vue/language-core@3.2.8':
'@vue/language-core@3.2.9':
dependencies:
'@volar/language-core': 2.4.28
'@vue/compiler-dom': 3.5.34
'@vue/shared': 3.5.34
alien-signals: 3.1.2
alien-signals: 3.2.1
muggle-string: 0.4.1
path-browserify: 1.0.1
picomatch: 4.0.4
@@ -5997,7 +5997,7 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
alien-signals@3.1.2: {}
alien-signals@3.2.1: {}
ansi-regex@5.0.1: {}
@@ -7360,7 +7360,7 @@ snapshots:
object.assign: '@nolyfill/object.assign@1.0.44'
object.values: '@nolyfill/object.values@1.0.44'
katex@0.16.45:
katex@0.16.46:
dependencies:
commander: 8.3.0
@@ -7558,7 +7558,7 @@ snapshots:
dayjs: 1.11.20
dompurify: 3.4.2
es-toolkit: 1.46.1
katex: 0.16.45
katex: 0.16.46
khroma: 2.1.0
marked: 16.4.2
roughjs: 4.6.6
@@ -7625,7 +7625,7 @@ snapshots:
dependencies:
'@types/katex': 0.16.8
devlop: 1.1.0
katex: 0.16.45
katex: 0.16.46
micromark-factory-space: 2.0.1
micromark-util-character: 2.1.1
micromark-util-symbol: 2.0.1
@@ -8606,10 +8606,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
vue-tsc@3.2.8(typescript@6.0.3):
vue-tsc@3.2.9(typescript@6.0.3):
dependencies:
'@volar/typescript': 2.4.28
'@vue/language-core': 3.2.8
'@vue/language-core': 3.2.9
typescript: 6.0.3
vue@3.5.34(typescript@6.0.3):
+2 -7
View File
@@ -104,6 +104,7 @@ import (
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
actions_module "code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
@@ -162,13 +163,7 @@ func ArtifactsV4Routes(prefix string) *web.Router {
}
func (r *artifactV4Routes) buildSignature(endpoint, expires, artifactName string, taskID, artifactID int64) []byte {
mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret())
mac.Write([]byte(endpoint))
mac.Write([]byte(expires))
mac.Write([]byte(artifactName))
_, _ = fmt.Fprint(mac, taskID)
_, _ = fmt.Fprint(mac, artifactID)
return mac.Sum(nil)
return actions_module.BuildSignature("v4", endpoint, expires, artifactName, strconv.FormatInt(taskID, 10), strconv.FormatInt(artifactID, 10))
}
func (r *artifactV4Routes) buildArtifactURL(ctx *ArtifactContext, endpoint, artifactName string, taskID, artifactID int64) string {
+84 -61
View File
@@ -212,6 +212,11 @@ func repoAssignment() func(ctx *context.APIContext) {
ctx.APIErrorNotFound()
return
}
if !ctx.TokenCanAccessRepo(repo) {
ctx.APIErrorNotFound()
return
}
}
}
@@ -249,51 +254,66 @@ func checkTokenPublicOnly() func(ctx *context.APIContext) {
return
}
// public Only permission check
switch {
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryRepository):
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
ctx.APIError(http.StatusForbidden, "token scope is limited to public repos")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryIssue):
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
ctx.APIError(http.StatusForbidden, "token scope is limited to public issues")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryOrganization):
if ctx.Org.Organization != nil && ctx.Org.Organization.Visibility != api.VisibleTypePublic {
ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs")
return
}
if ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryUser):
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
ctx.APIError(http.StatusForbidden, "token scope is limited to public users")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryActivityPub):
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
ctx.APIError(http.StatusForbidden, "token scope is limited to public activitypub")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryNotification):
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
ctx.APIError(http.StatusForbidden, "token scope is limited to public notifications")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryPackage):
if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
ctx.APIError(http.StatusForbidden, "token scope is limited to public packages")
return
for _, category := range requiredScopeCategories {
switch category {
case auth_model.AccessTokenScopeCategoryRepository:
if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) {
ctx.APIError(http.StatusForbidden, "token scope is limited to public repos")
return
}
case auth_model.AccessTokenScopeCategoryIssue:
if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) {
ctx.APIError(http.StatusForbidden, "token scope is limited to public issues")
return
}
case auth_model.AccessTokenScopeCategoryOrganization:
orgPrivate := ctx.Org.Organization != nil && !ctx.Org.Organization.Visibility.IsPublic()
userOrgPrivate := ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() && !ctx.ContextUser.Visibility.IsPublic()
if orgPrivate || userOrgPrivate {
ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs")
return
}
case auth_model.AccessTokenScopeCategoryUser:
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && !ctx.ContextUser.Visibility.IsPublic() {
ctx.APIError(http.StatusForbidden, "token scope is limited to public users")
return
}
case auth_model.AccessTokenScopeCategoryActivityPub:
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && !ctx.ContextUser.Visibility.IsPublic() {
ctx.APIError(http.StatusForbidden, "token scope is limited to public activitypub")
return
}
case auth_model.AccessTokenScopeCategoryNotification:
if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) {
ctx.APIError(http.StatusForbidden, "token scope is limited to public notifications")
return
}
case auth_model.AccessTokenScopeCategoryPackage:
if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
ctx.APIError(http.StatusForbidden, "token scope is limited to public packages")
return
}
}
}
}
}
func rejectPublicOnly() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !ctx.PublicOnly {
return
}
ctx.APIError(http.StatusForbidden, "this endpoint is not available for public-only tokens")
}
}
func contextAuthenticatedUser() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
ctx.ContextUser = ctx.Doer
}
}
// if a token is being used for auth, we check that it contains the required scope
// if a token is not being used, reqToken will enforce other sign in methods
func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeCategory) func(ctx *context.APIContext) {
@@ -957,6 +977,8 @@ func Routes() *web.Router {
})
// Notifications (requires 'notifications' scope)
// The notifications API is not available for public-only tokens because a user's notifications mix
// public and private repository events in the same mailbox.
m.Group("/notifications", func() {
m.Combo("").
Get(reqToken(), notify.ListNotifications).
@@ -965,7 +987,7 @@ func Routes() *web.Router {
m.Combo("/threads/{id}").
Get(reqToken(), notify.GetThread).
Patch(reqToken(), notify.ReadThread)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification))
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification), rejectPublicOnly())
// Users (requires user scope)
m.Group("/users", func() {
@@ -1013,8 +1035,9 @@ func Routes() *web.Router {
m.Group("/settings", func() {
m.Get("", user.GetUserSettings)
m.Patch("", bind(api.UserSettingsOptions{}), user.UpdateUserSettings)
}, reqToken())
m.Combo("/emails").
}, rejectPublicOnly())
// Email addresses are always private account data.
m.Combo("/emails", rejectPublicOnly()).
Get(user.ListEmails).
Post(bind(api.CreateEmailOption{}), user.AddEmail).
Delete(bind(api.DeleteEmailOption{}), user.DeleteEmail)
@@ -1046,7 +1069,7 @@ func Routes() *web.Router {
m.Get("/runs", reqToken(), user.ListWorkflowRuns)
m.Get("/jobs", reqToken(), user.ListWorkflowJobs)
})
}, rejectPublicOnly())
m.Get("/followers", user.ListMyFollowers)
m.Group("/following", func() {
@@ -1064,7 +1087,7 @@ func Routes() *web.Router {
Post(bind(api.CreateKeyOption{}), user.CreatePublicKey)
m.Combo("/{id}").Get(user.GetPublicKey).
Delete(user.DeletePublicKey)
})
}, rejectPublicOnly())
// (admin:application scope)
m.Group("/applications", func() {
@@ -1075,7 +1098,7 @@ func Routes() *web.Router {
Delete(user.DeleteOauth2Application).
Patch(bind(api.CreateOAuth2ApplicationOptions{}), user.UpdateOauth2Application).
Get(user.GetOauth2Application)
})
}, rejectPublicOnly())
// (admin:gpg_key scope)
m.Group("/gpg_keys", func() {
@@ -1083,13 +1106,13 @@ func Routes() *web.Router {
Post(bind(api.CreateGPGKeyOption{}), user.CreateGPGKey)
m.Combo("/{id}").Get(user.GetGPGKey).
Delete(user.DeleteGPGKey)
})
m.Get("/gpg_key_token", user.GetVerificationToken)
m.Post("/gpg_key_verify", bind(api.VerifyGPGKeyOption{}), user.VerifyUserGPGKey)
}, rejectPublicOnly())
m.Get("/gpg_key_token", rejectPublicOnly(), user.GetVerificationToken)
m.Post("/gpg_key_verify", rejectPublicOnly(), bind(api.VerifyGPGKeyOption{}), user.VerifyUserGPGKey)
// (repo scope)
m.Combo("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(user.ListMyRepos).
Post(bind(api.CreateRepoOption{}), repo.Create)
Post(rejectPublicOnly(), bind(api.CreateRepoOption{}), repo.Create)
// (repo scope)
m.Group("/starred", func() {
@@ -1100,22 +1123,22 @@ func Routes() *web.Router {
m.Delete("", user.Unstar)
}, repoAssignment(), checkTokenPublicOnly())
}, reqStarsEnabled(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
m.Get("/times", repo.ListMyTrackedTimes)
m.Get("/stopwatches", repo.GetStopwatches)
m.Get("/times", rejectPublicOnly(), repo.ListMyTrackedTimes)
m.Get("/stopwatches", rejectPublicOnly(), repo.GetStopwatches)
m.Get("/subscriptions", user.GetMyWatchedRepos)
m.Get("/teams", org.ListUserTeams)
m.Get("/teams", rejectPublicOnly(), org.ListUserTeams)
m.Group("/hooks", func() {
m.Combo("").Get(user.ListHooks).
Post(bind(api.CreateHookOption{}), user.CreateHook)
m.Combo("/{id}").Get(user.GetHook).
Patch(bind(api.EditHookOption{}), user.EditHook).
Delete(user.DeleteHook)
}, reqWebhooksEnabled())
}, reqWebhooksEnabled(), rejectPublicOnly())
m.Group("/avatar", func() {
m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar)
m.Delete("", user.DeleteAvatar)
})
}, rejectPublicOnly())
m.Group("/blocks", func() {
m.Get("", user.ListBlocks)
@@ -1124,13 +1147,13 @@ func Routes() *web.Router {
m.Put("", user.BlockUser)
m.Delete("", user.UnblockUser)
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
})
}, rejectPublicOnly())
m.Group("/mirror-ssh-key", func() {
m.Get("", user.GetMirrorSSHKey)
m.Post("/regenerate", user.RegenerateMirrorSSHKey)
})
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken(), contextAuthenticatedUser(), checkTokenPublicOnly())
// Repositories (requires repo scope, org scope)
m.Post("/org/{org}/repos",
@@ -1435,9 +1458,9 @@ func Routes() *web.Router {
Delete(reqToken(), repo.DeleteTopic)
}, reqAdmin())
}, reqAnyRepoReader())
m.Get("/issue_templates", context.ReferencesGitRepo(), repo.GetIssueTemplates)
m.Get("/issue_config", context.ReferencesGitRepo(), repo.GetIssueConfig)
m.Get("/issue_config/validate", context.ReferencesGitRepo(), repo.ValidateIssueConfig)
m.Get("/issue_templates", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.GetIssueTemplates)
m.Get("/issue_config", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.GetIssueConfig)
m.Get("/issue_config/validate", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.ValidateIssueConfig)
m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages)
m.Get("/licenses", reqRepoReader(unit.TypeCode), repo.GetLicenses)
m.Get("/activities/feeds", repo.ListRepoActivityFeeds)
@@ -1606,7 +1629,7 @@ func Routes() *web.Router {
}, reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead), checkTokenPublicOnly())
// Organizations
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs)
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), checkTokenPublicOnly(), org.ListMyOrgs)
m.Group("/users/{username}/orgs", func() {
m.Get("", reqToken(), org.ListUserOrgs)
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
+8 -3
View File
@@ -40,6 +40,7 @@ func listUserOrgs(ctx *context.APIContext, u *user_model.User) {
UserID: u.ID,
IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, u),
}
opts.ApplyPublicOnly(ctx.PublicOnly)
orgs, maxResults, err := db.FindAndCount[organization.Organization](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
@@ -199,7 +200,7 @@ func GetAll(ctx *context.APIContext) {
// "$ref": "#/responses/OrganizationList"
vMode := []api.VisibleType{api.VisibleTypePublic}
if ctx.IsSigned && !ctx.PublicOnly {
if ctx.IsSigned {
vMode = append(vMode, api.VisibleTypeLimited)
if ctx.Doer.IsAdmin {
vMode = append(vMode, api.VisibleTypePrivate)
@@ -208,13 +209,16 @@ func GetAll(ctx *context.APIContext) {
listOptions := utils.GetListOptions(ctx)
publicOrgs, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
searchOpts := user_model.SearchUserOptions{
Actor: ctx.Doer,
ListOptions: listOptions,
Types: []user_model.UserType{user_model.UserTypeOrganization},
OrderBy: db.SearchOrderByAlphabetically,
Visible: vMode,
})
}
searchOpts.ApplyPublicOnly(ctx.PublicOnly)
publicOrgs, maxResults, err := user_model.SearchUsers(ctx, searchOpts)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -494,6 +498,7 @@ func ListOrgActivityFeeds(ctx *context.APIContext) {
Date: ctx.FormString("date"),
ListOptions: listOptions,
}
opts.ApplyPublicOnly(ctx.PublicOnly)
feeds, count, err := feed_service.GetFeeds(ctx, opts)
if err != nil {
+2 -7
View File
@@ -6,7 +6,6 @@ package repo
import (
go_context "context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
@@ -21,9 +20,9 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
secret_model "code.gitea.io/gitea/models/secret"
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
@@ -1959,11 +1958,7 @@ func DeleteArtifact(ctx *context.APIContext) {
}
func buildSignature(endp string, expires, artifactID int64) []byte {
mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret())
mac.Write([]byte(endp))
fmt.Fprint(mac, expires)
fmt.Fprint(mac, artifactID)
return mac.Sum(nil)
return actions.BuildSignature("api", endp, strconv.FormatInt(expires, 10), strconv.FormatInt(artifactID, 10))
}
func buildDownloadRawEndpoint(repo *repo_model.Repository, artifactID int64) string {
+2 -1
View File
@@ -47,9 +47,10 @@ func buildSearchIssuesRepoIDs(ctx *context.APIContext) (repoIDs []int64, allPubl
Actor: ctx.Doer,
}
if ctx.IsSigned {
opts.Private = !ctx.PublicOnly
opts.Private = true
opts.AllLimited = true
}
opts.ApplyPublicOnly(ctx.PublicOnly)
if ctx.FormString("owner") != "" {
owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
if err != nil {
+6 -3
View File
@@ -134,9 +134,6 @@ func Search(ctx *context.APIContext) {
// "$ref": "#/responses/validationError"
private := ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private"))
if ctx.PublicOnly {
private = false
}
opts := repo_model.SearchRepoOptions{
ListOptions: utils.GetListOptions(ctx),
@@ -152,6 +149,7 @@ func Search(ctx *context.APIContext) {
StarredByID: ctx.FormInt64("starredBy"),
IncludeDescription: ctx.FormBool("includeDesc"),
}
opts.ApplyPublicOnly(ctx.PublicOnly)
if ctx.FormString("template") != "" {
opts.Template = optional.Some(ctx.FormBool("template"))
@@ -557,6 +555,10 @@ func GetByID(ctx *context.APIContext) {
}
return
}
if !ctx.TokenCanAccessRepo(repo) {
ctx.APIErrorNotFound()
return
}
permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
@@ -1309,6 +1311,7 @@ func ListRepoActivityFeeds(ctx *context.APIContext) {
Date: ctx.FormString("date"),
ListOptions: listOptions,
}
opts.ApplyPublicOnly(ctx.PublicOnly)
feeds, count, err := feed_service.GetFeeds(ctx, opts)
if err != nil {
+7 -4
View File
@@ -19,12 +19,15 @@ import (
func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
opts := utils.GetListOptions(ctx)
repos, count, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
searchOpts := repo_model.SearchRepoOptions{
Actor: u,
Private: private,
ListOptions: opts,
OrderBy: "id ASC",
})
}
searchOpts.ApplyPublicOnly(ctx.PublicOnly)
repos, count, err := repo_model.GetUserRepositories(ctx, searchOpts)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -79,8 +82,7 @@ func ListUserRepos(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
private := ctx.IsSigned
listUserRepos(ctx, ctx.ContextUser, private)
listUserRepos(ctx, ctx.ContextUser, ctx.IsSigned)
}
// ListMyRepos - list the repositories you own or have access to.
@@ -110,6 +112,7 @@ func ListMyRepos(ctx *context.APIContext) {
Private: ctx.IsSigned,
IncludeDescription: true,
}
opts.ApplyPublicOnly(ctx.PublicOnly)
repos, count, err := repo_model.SearchRepository(ctx, opts)
if err != nil {
+5 -2
View File
@@ -20,11 +20,14 @@ import (
// getStarredRepos returns the repos that the user with the specified userID has
// starred
func getStarredRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, error) {
starredRepos, err := repo_model.GetStarredRepos(ctx, &repo_model.StarredReposOptions{
opts := &repo_model.StarredReposOptions{
ListOptions: utils.GetListOptions(ctx),
StarrerID: user.ID,
IncludePrivate: private,
})
}
opts.ApplyPublicOnly(ctx.PublicOnly)
starredRepos, err := repo_model.GetStarredRepos(ctx, opts)
if err != nil {
return nil, err
}
+5 -8
View File
@@ -9,7 +9,6 @@ import (
activities_model "code.gitea.io/gitea/models/activities"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
@@ -69,19 +68,16 @@ func Search(ctx *context.APIContext) {
maxResults = 1
users = []*user_model.User{user_model.NewActionsUser()}
default:
var visible []structs.VisibleType
if ctx.PublicOnly {
visible = []structs.VisibleType{structs.VisibleTypePublic}
}
users, maxResults, err = user_model.SearchUsers(ctx, user_model.SearchUserOptions{
opts := user_model.SearchUserOptions{
Actor: ctx.Doer,
Keyword: ctx.FormTrim("q"),
UID: uid,
Types: []user_model.UserType{user_model.UserTypeIndividual},
SearchByEmail: true,
Visible: visible,
ListOptions: listOptions,
})
}
opts.ApplyPublicOnly(ctx.PublicOnly)
users, maxResults, err = user_model.SearchUsers(ctx, opts)
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]any{
"ok": false,
@@ -214,6 +210,7 @@ func ListUserActivityFeeds(ctx *context.APIContext) {
Date: ctx.FormString("date"),
ListOptions: listOptions,
}
opts.ApplyPublicOnly(ctx.PublicOnly)
feeds, count, err := feed_service.GetFeeds(ctx, opts)
if err != nil {
+5 -2
View File
@@ -18,11 +18,14 @@ import (
// getWatchedRepos returns the repos that the user with the specified userID is watching
func getWatchedRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, int64, error) {
watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, &repo_model.WatchedReposOptions{
opts := &repo_model.WatchedReposOptions{
ListOptions: utils.GetListOptions(ctx),
WatcherID: user.ID,
IncludePrivate: private,
})
}
opts.ApplyPublicOnly(ctx.PublicOnly)
watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, opts)
if err != nil {
return nil, 0, err
}
+35 -24
View File
@@ -683,6 +683,8 @@ func indexCommit(commits []*git.Commit, commitID string) *git.Commit {
// ViewPullFiles render pull request changed files list page
func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
var err error
ctx.Data["PageIsPullList"] = true
ctx.Data["PageIsPullFiles"] = true
@@ -707,44 +709,53 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
headCommitID := prCompareInfo.HeadCommitID
isSingleCommit := beforeCommitID == "" && afterCommitID != ""
ctx.Data["IsShowingOnlySingleCommit"] = isSingleCommit
// FIXME: when afterCommitID==headCommitID, isSingleCommit and isShowAllCommits can be both true, which doesn't seem right
isShowAllCommits := (beforeCommitID == "" || beforeCommitID == prCompareInfo.CompareBase) && (afterCommitID == "" || afterCommitID == headCommitID)
ctx.Data["IsShowingOnlySingleCommit"] = isSingleCommit
ctx.Data["IsShowingAllCommits"] = isShowAllCommits
if afterCommitID == "" || afterCommitID == headCommitID {
afterCommitID = headCommitID
}
// "commits list" is half-open, half-closed: (base, head]
// * base commit is not in the list
// * if the PR is empty, the list is also empty (head commit is not in the list)
afterCommitID = util.IfZero(afterCommitID, headCommitID)
afterCommit := indexCommit(prCompareInfo.Commits, afterCommitID)
if afterCommit == nil && afterCommitID == headCommitID {
afterCommit, err = gitRepo.GetCommit(afterCommitID)
if err != nil {
ctx.ServerError("GetCommit(afterCommitID)", err)
return
}
}
if afterCommit == nil {
ctx.HTTPError(http.StatusBadRequest, "after commit not found in PR commits")
ctx.NotFound(nil)
return
}
var beforeCommit *git.Commit
var err error
if !isSingleCommit {
if beforeCommitID == "" || beforeCommitID == prCompareInfo.CompareBase {
beforeCommitID = prCompareInfo.CompareBase
// merge base commit is not in the list of the pull request commits
beforeCommit, err = gitRepo.GetCommit(beforeCommitID)
if err != nil {
ctx.ServerError("GetCommit", err)
return
}
} else {
beforeCommit = indexCommit(prCompareInfo.Commits, beforeCommitID)
if beforeCommit == nil {
ctx.HTTPError(http.StatusBadRequest, "before commit not found in PR commits")
return
}
}
} else {
if isSingleCommit {
beforeCommit, err = afterCommit.Parent(0)
if err != nil {
ctx.ServerError("Parent", err)
ctx.ServerError("afterCommit.Parent", err)
return
}
beforeCommitID = beforeCommit.ID.String()
} else {
beforeCommitID = util.IfZero(beforeCommitID, prCompareInfo.CompareBase)
beforeCommit = indexCommit(prCompareInfo.Commits, beforeCommitID)
if beforeCommit == nil && beforeCommitID == prCompareInfo.CompareBase {
// base commit is not in the list of the pull request commits
beforeCommit, err = gitRepo.GetCommit(beforeCommitID)
if err != nil {
ctx.ServerError("GetCommit(beforeCommitID)", err)
return
}
}
}
if beforeCommit == nil {
ctx.NotFound(nil)
return
}
ctx.Data["CompareInfo"] = prCompareInfo
+7
View File
@@ -13,6 +13,7 @@ import (
"strconv"
"strings"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache"
@@ -47,6 +48,12 @@ type APIContext struct {
PublicOnly bool // Whether the request is for a public endpoint
}
// TokenCanAccessRepo reports whether the current API token is allowed to access the repository.
// A public-only token cannot reach a private repo; any other token is unrestricted by this check.
func (ctx *APIContext) TokenCanAccessRepo(repo *repo_model.Repository) bool {
return repo == nil || !ctx.PublicOnly || !repo.IsPrivate
}
func init() {
web.RegisterResponseStatusProvider[*APIContext](func(req *http.Request) web_types.ResponseStatusProvider {
return req.Context().Value(apiContextKey).(*APIContext)
+8 -2
View File
@@ -17,6 +17,7 @@ import (
packages_model "code.gitea.io/gitea/models/packages"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/globallock"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
@@ -78,8 +79,13 @@ func CreatePackageAndAddFile(ctx context.Context, pvci *PackageCreationInfo, pfc
}
// CreatePackageOrAddFileToExisting creates a package with a file or adds the file if the package exists already
func CreatePackageOrAddFileToExisting(ctx context.Context, pvci *PackageCreationInfo, pfci *PackageFileCreationInfo) (*packages_model.PackageVersion, *packages_model.PackageFile, error) {
return createPackageAndAddFile(ctx, pvci, pfci, true)
func CreatePackageOrAddFileToExisting(ctx context.Context, pvci *PackageCreationInfo, pfci *PackageFileCreationInfo) (pv *packages_model.PackageVersion, pf *packages_model.PackageFile, err error) {
lockKey := fmt.Sprintf("pkg-upsert-%v-%v-%v", pvci.PackageType, pvci.Name, pvci.Version)
err = globallock.LockAndDo(ctx, lockKey, func(ctx context.Context) error {
pv, pf, err = createPackageAndAddFile(ctx, pvci, pfci, true)
return err
})
return pv, pf, err
}
func createPackageAndAddFile(ctx context.Context, pvci *PackageCreationInfo, pfci *PackageFileCreationInfo, allowDuplicate bool) (*packages_model.PackageVersion, *packages_model.PackageFile, error) {
+2 -1
View File
@@ -13,6 +13,7 @@ import (
"strings"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
@@ -164,7 +165,7 @@ func sortTreeViewNodes(nodes []*TreeViewNode) {
if a != b {
return a < b
}
return nodes[i].EntryName < nodes[j].EntryName
return base.NaturalSortCompare(nodes[i].EntryName, nodes[j].EntryName) < 0
})
}
+2 -2
View File
@@ -19,13 +19,13 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly.
sharedWorkerUri: '{{AssetURI "js/eventsource.sharedworker.js"}}',
{{/* this global i18n object should only contain general texts. for specialized texts, it should be provided inside the related modules by: (1) API response (2) HTML data-attribute (3) PageData */}}
i18n: {
copy_success: {{ctx.Locale.Tr "copy_success"}},
copy_error: {{ctx.Locale.Tr "copy_error"}},
error_occurred: {{ctx.Locale.Tr "error.occurred"}},
remove_label_str: {{ctx.Locale.Tr "remove_label_str"}},
modal_confirm: {{ctx.Locale.Tr "modal.confirm"}},
modal_cancel: {{ctx.Locale.Tr "modal.cancel"}},
more_items: {{ctx.Locale.Tr "more_items"}},
copy_success: {{ctx.Locale.Tr "copy_success"}},
copy_error: {{ctx.Locale.Tr "copy_error"}},
},
};
{{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}}
@@ -36,6 +36,7 @@
data-locale-show-log-seconds="{{ctx.Locale.Tr "show_log_seconds"}}"
data-locale-show-full-screen="{{ctx.Locale.Tr "show_full_screen"}}"
data-locale-download-logs="{{ctx.Locale.Tr "download_logs"}}"
data-locale-copy-output="{{ctx.Locale.Tr "copy_output"}}"
data-locale-logs-always-auto-scroll="{{ctx.Locale.Tr "actions.logs.always_auto_scroll"}}"
data-locale-logs-always-expand-running="{{ctx.Locale.Tr "actions.logs.always_expand_running"}}"
>
+13 -13
View File
@@ -29,8 +29,8 @@
</div>
<p class="info tw-flex tw-items-center tw-my-1">{{svg "octicon-git-commit" 16 "tw-mr-1"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{ctx.RenderUtils.RenderCommitMessage .DefaultBranchBranch.DBBranch.CommitMessage .Repository}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DefaultBranchBranch.DBBranch.CommitTime}}{{if .DefaultBranchBranch.DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p>
</td>
{{/* FIXME: here and below, the tw-overflow-visible is not quite right but it is still needed the moment: to show the important buttons when the width is narrow */}}
<td class="tw-text-right tw-overflow-visible">
<td>
<div class="tw-flex tw-flex-wrap tw-justify-end">
{{if and $.IsWriter $.Repository.CanContentChange (not .IsDeleted)}}
<button class="btn interact-bg show-create-branch-modal tw-p-2"
data-modal="#create-branch-modal"
@@ -63,6 +63,7 @@
{{svg "octicon-pencil"}}
</button>
{{end}}
</div>
</td>
</tr>
</tbody>
@@ -71,9 +72,7 @@
{{end}}
<h4 class="ui top attached header">
<div class="flex-text-block">
{{ctx.Locale.Tr "repo.branches"}}
</div>
{{ctx.Locale.Tr "repo.branches"}}
</h4>
<div class="ui attached segment">
@@ -106,7 +105,7 @@
<p class="info tw-flex tw-items-center tw-my-1">{{svg "octicon-git-commit" 16 "tw-mr-1"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{ctx.RenderUtils.RenderCommitMessage .DBBranch.CommitMessage $.Repository}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DBBranch.CommitTime}}{{if .DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} &nbsp;{{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p>
{{end}}
</td>
<td class="two wide ui">
<td class="two wide">
{{if and (not .DBBranch.IsDeleted) $.DefaultBranchBranch}}
{{$tooltipDivergence := ""}}
{{if or .CommitsBehind .CommitsAhead}}
@@ -127,21 +126,21 @@
</div>
{{end}}
</td>
<td class="two wide tw-text-right">
<td class="three wide tw-text-right">
{{if not .LatestPullRequest}}
{{if .IsIncluded}}
<span class="ui orange large label" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.included_desc"}}">
{{svg "octicon-git-pull-request"}} {{ctx.Locale.Tr "repo.branch.included"}}
</span>
{{else if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}}
<a href="{{$.PullRequestCtx.MakeDefaultCompareLink .DBBranch.Name}}?expand=1">
<button id="new-pull-request" class="ui compact basic button tw-mr-0">{{if $.CanPull}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}</button>
<a class="ui compact basic button tw-m-0 tw-max-w-full" href="{{$.PullRequestCtx.MakeDefaultCompareLink .DBBranch.Name}}?expand=1">
<span class="gt-ellipsis">{{if $.CanPull}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}</span>
</a>
{{end}}
{{else if and .LatestPullRequest.HasMerged .MergeMovedOn}}
{{if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}}
<a href="{{$.PullRequestCtx.MakeDefaultCompareLink .DBBranch.Name}}?expand=1">
<button id="new-pull-request" class="ui compact basic button tw-mr-0">{{if $.CanPull}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}</button>
<a class="ui compact basic button tw-m-0 tw-max-w-full" href="{{$.PullRequestCtx.MakeDefaultCompareLink .DBBranch.Name}}?expand=1">
<span class="gt-ellipsis">{{if $.CanPull}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}</span>
</a>
{{end}}
{{else}}
@@ -155,8 +154,8 @@
{{end}}
{{end}}
</td>
{{/* FIXME: here and above, the tw-overflow-visible is not quite right */}}
<td class="three wide tw-text-right tw-overflow-visible">
<td class="three wide">
<div class="tw-flex tw-flex-wrap tw-justify-end">
{{if and $.IsWriter $.Repository.CanContentChange (not .DBBranch.IsDeleted)}}
<button class="btn interact-bg tw-p-2 show-modal show-create-branch-modal"
data-branch-from="{{.DBBranch.Name}}"
@@ -202,6 +201,7 @@
</button>
{{end}}
{{end}}
</div>
</td>
</tr>
{{end}}
+1 -1
View File
@@ -68,7 +68,7 @@
{{range $label := .Labels}}
{{$link := print $.Issue.Repo.Link "/issues"}}
{{$link = QueryBuild $link "labels" $label.ID}}
{{ctx.RenderUtils.RenderLabelWithLink $label $link}}
<a class="item" href="{{$link}}">{{ctx.RenderUtils.RenderLabel $label}}</a>
{{end}}
</div>
{{if .Assignees}}
@@ -169,7 +169,7 @@
</div>
{{else if eq .Type 7}}
{{if or .AddedLabels .RemovedLabels}}
<div class="timeline-item event with-labels-list-inline" id="{{.HashTag}}">
<div class="timeline-item event" id="{{.HashTag}}">
<span class="badge">{{svg "octicon-tag"}}</span>
{{template "shared/user/avatarlink" dict "user" .Poster}}
<span class="comment-text-line">
@@ -3,13 +3,13 @@
{{svg "octicon-kebab-horizontal"}}
</a>
<div class="menu">
{{$referenceUrl := ""}}
{{$referenceLink := ""}}
{{if .issue}}
{{$referenceUrl = printf "%s#%s" ctx.RootData.Issue.Link .item.HashTag}}
{{$referenceLink = printf "%s#%s" ctx.RootData.Issue.Link .item.HashTag}}
{{else}}
{{$referenceUrl = printf "%s/files#%s" ctx.RootData.Issue.Link .item.HashTag}}
{{$referenceLink = printf "%s/files#%s" ctx.RootData.Issue.Link .item.HashTag}}
{{end}}
<div class="item context js-aria-clickable" data-clipboard-text-type="url" data-clipboard-text="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.copy_link"}}</div>
<div class="item context js-aria-clickable" data-clipboard-text="{{ctx.AppFullLink}}{{$referenceLink}}">{{ctx.Locale.Tr "repo.issues.context.copy_link"}}</div>
<div class="item context js-aria-clickable" data-clipboard-target="#{{.item.HashTag}}-raw">{{ctx.Locale.Tr "repo.issues.context.copy_source"}}</div>
{{if ctx.RootData.IsSigned}}
{{$needDivider := false}}
@@ -17,7 +17,7 @@
{{$needDivider = true}}
<div class="item context js-aria-clickable quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{ctx.Locale.Tr "repo.issues.context.quote_reply"}}</div>
{{if not ctx.Consts.RepoUnitTypeIssues.UnitGlobalDisabled}}
<div class="item context js-aria-clickable reference-issue" data-target="{{.item.HashTag}}-raw" data-modal="#reference-issue-modal" data-poster="{{.item.Poster.GetDisplayName}}" data-poster-username="{{.item.Poster.Name}}" data-reference="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</div>
<div class="item context js-aria-clickable reference-issue" data-target="{{.item.HashTag}}-raw" data-modal="#reference-issue-modal" data-poster="{{.item.Poster.GetDisplayName}}" data-poster-username="{{.item.Poster.Name}}" data-reference="{{$referenceLink}}">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</div>
{{end}}
{{if or ctx.RootData.Permission.IsAdmin .IsCommentPoster ctx.RootData.HasIssuesOrPullsWritePermission}}
<div class="divider"></div>
+1 -1
View File
@@ -92,7 +92,7 @@
<button class="ui dropdown basic compact jump button tw-px-3" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}">
{{svg "octicon-kebab-horizontal"}}
<div class="menu">
<a class="item" data-clipboard-text="{{.Repository.Link}}/src/commit/{{.CommitID}}/{{PathEscapeSegments .TreePath}}" data-clipboard-text-type="url">
<a class="item" data-clipboard-text="{{ctx.AppFullLink}}{{.Repository.Link}}/src/commit/{{.CommitID}}/{{PathEscapeSegments .TreePath}}">
{{svg "octicon-link" 16}}{{ctx.Locale.Tr "repo.file_copy_permalink"}}
</a>
{{if and (not $.DisableDownloadSourceArchives) $.RefFullName}}
+7 -5
View File
@@ -15,17 +15,19 @@
<div class="item-main">
<div class="item-header">
<div class="item-title">
<a class="tw-no-underline issue-title" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}">{{.Title | ctx.RenderUtils.RenderIssueSimpleTitle}}</a>
<div>
<a class="issue-item-title" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}">{{.Title | ctx.RenderUtils.RenderIssueSimpleTitle}}</a>
{{if .IsPull}}
{{if (index $.CommitStatuses .PullRequest.ID)}}
<span class="tw-align-[1px]">{{/* make the "flex" children align with parent "inline" */}}
{{template "repo/commit_statuses" dict "Status" (index $.CommitLastStatus .PullRequest.ID) "Statuses" (index $.CommitStatuses .PullRequest.ID)}}
</span>
{{end}}
{{end}}
<span class="labels-list">
{{range .Labels}}
<a href="?q={{$.Keyword}}&type={{$.ViewType}}&state={{$.State}}&labels={{.ID}}{{if ne $.listType "milestone"}}&milestone={{$.MilestoneID}}{{end}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.RenderUtils.RenderLabel .}}</a>
{{end}}
{{- range .Labels -}}
<a class="item" href="?q={{$.Keyword}}&type={{$.ViewType}}&state={{$.State}}&labels={{.ID}}{{if ne $.listType "milestone"}}&milestone={{$.MilestoneID}}{{end}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.RenderUtils.RenderLabel .}}</a>
{{- end -}}
</span>
</div>
{{if .TotalTrackedTime}}
+1 -1
View File
@@ -107,7 +107,7 @@
{{else if .GetOpType.InActions "create_pull_request"}}
<span class="tw-inline-block tw-truncate issue title">{{index .GetIssueInfos 1 | ctx.RenderUtils.RenderIssueSimpleTitle}}</span>
{{else if .GetOpType.InActions "comment_issue" "approve_pull_request" "reject_pull_request" "comment_pull"}}
<a href="{{.GetCommentLink ctx}}" class="tw-inline-block tw-truncate tw-self-start issue title">{{(.GetIssueTitle ctx) | ctx.RenderUtils.RenderIssueSimpleTitle}}</a>
<a href="{{.GetCommentLink ctx}}" class="tw-inline-block tw-truncate tw-max-w-full tw-self-start issue title">{{(.GetIssueTitle ctx) | ctx.RenderUtils.RenderIssueSimpleTitle}}</a>
{{$comment := index .GetIssueInfos 1}}
{{if $comment}}
<div class="render-content markup truncated-markup">{{ctx.RenderUtils.MarkdownToHtml $comment}}</div>
@@ -8,6 +8,7 @@ import (
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
@@ -176,3 +177,19 @@ func TestAPIRepoValidateIssueConfig(t *testing.T) {
assert.NotEmpty(t, issueConfigValidation.Message)
})
}
func TestAPIRepoIssueConfigRequiresCodeUnit(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 24})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
token := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadRepository)
for _, path := range []string{
fmt.Sprintf("/api/v1/repos/%s/issue_config", repo.FullName()),
fmt.Sprintf("/api/v1/repos/%s/issue_config/validate", repo.FullName()),
} {
req := NewRequest(t, "GET", path).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
}
}
@@ -8,10 +8,12 @@ import (
"net/url"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
@@ -49,3 +51,19 @@ about: bar
assert.Equal(t, "error occurs when parsing issue template: count=2", resp.Header().Get("X-Gitea-Warning"))
})
}
func TestAPIIssueTemplateRequiresCodeUnit(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 24})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
token := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadRepository)
issueTemplatesURL := "/api/v1/repos/" + repo.FullName() + "/issue_templates"
languagesURL := "/api/v1/repos/" + repo.FullName() + "/languages"
req := NewRequest(t, "GET", issueTemplatesURL).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
req = NewRequest(t, "GET", languagesURL).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
}
+4 -7
View File
@@ -108,7 +108,7 @@ func testAPIListIssuesPublicOnly(t *testing.T) {
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopePublicOnly)
req = NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken)
MakeRequest(t, req, http.StatusForbidden)
MakeRequest(t, req, http.StatusNotFound)
}
func testAPICreateIssue(t *testing.T) {
@@ -171,9 +171,8 @@ func testAPICreateIssueParallel(t *testing.T) {
var wg sync.WaitGroup
for i := range 10 {
wg.Add(1)
go func(parentT *testing.T, i int) {
parentT.Run(fmt.Sprintf("ParallelCreateIssue_%d", i), func(t *testing.T) {
wg.Go(func() {
t.Run(fmt.Sprintf("ParallelCreateIssue_%d", i), func(t *testing.T) {
newTitle := title + strconv.Itoa(i)
newBody := body + strconv.Itoa(i)
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
@@ -192,10 +191,8 @@ func testAPICreateIssueParallel(t *testing.T) {
Content: newBody,
Title: newTitle,
})
wg.Done()
})
}(t, i)
})
}
wg.Wait()
}
@@ -209,3 +209,23 @@ func TestAPINotificationPUT(t *testing.T) {
assert.True(t, apiNL[0].Unread)
assert.False(t, apiNL[0].Pinned)
}
func TestAPINotificationPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
thread5 := unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: 5})
token := getUserToken(t, user2.Name, auth_model.AccessTokenScopeReadNotification, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/api/v1/notifications").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
req = NewRequest(t, "GET", "/api/v1/notifications/new").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications/threads/%d", thread5.ID)).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
}
@@ -766,20 +766,16 @@ func TestPackageContainer(t *testing.T) {
var wg sync.WaitGroup
for i := range 10 {
wg.Add(1)
content := []byte{byte(i)}
digest := fmt.Sprintf("sha256:%x", sha256.Sum256(content))
go func() {
defer wg.Done()
wg.Go(func() {
req := NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", url, digest), bytes.NewReader(content)).
AddTokenAuth(userToken)
resp := MakeRequest(t, req, http.StatusCreated)
assert.Equal(t, digest, resp.Header().Get("Docker-Content-Digest"))
}()
})
}
wg.Wait()
})
+2 -4
View File
@@ -321,11 +321,9 @@ func TestPackageMavenConcurrent(t *testing.T) {
var wg sync.WaitGroup
for i := range 10 {
wg.Add(1)
go func(i int) {
wg.Go(func() {
putFile(t, fmt.Sprintf("/%s/%s.jar", packageVersion, strconv.Itoa(i)), "test", http.StatusCreated)
wg.Done()
}(i)
})
}
wg.Wait()
})
+107
View File
@@ -0,0 +1,107 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestAPIUserReposPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/api/v1/user/repos").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var repos []api.Repository
DecodeJSON(t, resp, &repos)
assert.NotEmpty(t, repos)
for _, repo := range repos {
assert.False(t, repo.Private)
}
assert.NotContains(t, repoNames(repos), "user2/repo2")
req = NewRequest(t, "GET", "/api/v1/users/user2/repos").
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &repos)
assert.NotEmpty(t, repos)
for _, repo := range repos {
assert.False(t, repo.Private)
}
assert.NotContains(t, repoNames(repos), "user2/repo2")
}
func repoNames(repos []api.Repository) []string {
names := make([]string, 0, len(repos))
for _, repo := range repos {
names = append(names, repo.FullName)
}
return names
}
func TestAPIRepoByIDPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/api/v1/repositories/1").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", "/api/v1/repositories/2").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
func TestAPIActivityFeedsPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser)
req := NewRequest(t, "GET", "/api/v1/users/user2/activities/feeds").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var activities []api.Activity
DecodeJSON(t, resp, &activities)
assert.NotEmpty(t, activities)
publicToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopePublicOnly)
req = NewRequest(t, "GET", "/api/v1/users/user2/activities/feeds").
AddTokenAuth(publicToken)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &activities)
assertPublicActivitiesOnly(t, activities)
orgToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization)
req = NewRequest(t, "GET", "/api/v1/orgs/org3/activities/feeds").
AddTokenAuth(orgToken)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &activities)
assert.NotEmpty(t, activities)
publicOrgToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopePublicOnly)
req = NewRequest(t, "GET", "/api/v1/orgs/org3/activities/feeds").
AddTokenAuth(publicOrgToken)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &activities)
assertPublicActivitiesOnly(t, activities)
}
func assertPublicActivitiesOnly(t *testing.T, activities []api.Activity) {
t.Helper()
for _, activity := range activities {
assert.False(t, activity.IsPrivate)
if activity.Repo != nil {
assert.False(t, activity.Repo.Private)
}
}
}
+5 -5
View File
@@ -29,10 +29,10 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
session := loginUser(t, user1.LowerName)
// public only token should be forbidden
// public-only token cannot see a private repo
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopePublicOnly, auth_model.AccessTokenScopeWriteRepository)
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches", repo3.Name)) // a plain repo
MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken), http.StatusNotFound)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
resp := MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK)
@@ -46,7 +46,7 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
assert.Equal(t, "master", branches[1].Name)
link2, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches/test_branch", repo3.Name))
MakeRequest(t, NewRequest(t, "GET", link2.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", link2.String()).AddTokenAuth(publicOnlyToken), http.StatusNotFound)
resp = MakeRequest(t, NewRequest(t, "GET", link2.String()).AddTokenAuth(token), http.StatusOK)
bs, err = io.ReadAll(resp.Body)
@@ -55,7 +55,7 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
assert.NoError(t, json.Unmarshal(bs, &branch))
assert.Equal(t, "test_branch", branch.Name)
MakeRequest(t, NewRequest(t, "POST", link.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "POST", link.String()).AddTokenAuth(publicOnlyToken), http.StatusNotFound)
req := NewRequest(t, "POST", link.String()).AddTokenAuth(token)
req.Header.Add("Content-Type", "application/json")
@@ -81,7 +81,7 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
link3, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches/test_branch2", repo3.Name))
MakeRequest(t, NewRequest(t, "DELETE", link3.String()), http.StatusNotFound)
MakeRequest(t, NewRequest(t, "DELETE", link3.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "DELETE", link3.String()).AddTokenAuth(publicOnlyToken), http.StatusNotFound)
MakeRequest(t, NewRequest(t, "DELETE", link3.String()).AddTokenAuth(token), http.StatusNoContent)
assert.NoError(t, err)
+2 -2
View File
@@ -42,8 +42,8 @@ func TestAPIGetRawFileOrLFS(t *testing.T) {
lfs := lfsCommitAndPushTest(t, dstPath, testFileSizeSmall)[0]
reqLFS := NewRequest(t, "GET", "/api/v1/repos/user2/repo-lfs-test/media/"+lfs).AddTokenAuth(httpContext.Token)
respLFS := MakeRequestNilResponseRecorder(t, reqLFS, http.StatusOK)
assert.Equal(t, testFileSizeSmall, respLFS.Length)
respLFS := MakeRequest(t, reqLFS, http.StatusOK)
assert.Equal(t, testFileSizeSmall, respLFS.Body.Len())
})
})
}
+41
View File
@@ -154,3 +154,44 @@ func TestMyOrgs(t *testing.T) {
},
}, orgs)
}
func TestMyOrgsPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
normalUsername := "user2"
token := getUserToken(t, normalUsername, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/api/v1/user/orgs").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var orgs []*api.Organization
DecodeJSON(t, resp, &orgs)
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
org17 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org17"})
assert.Equal(t, []*api.Organization{
{
ID: 17,
Name: org17.Name,
UserName: org17.Name,
FullName: org17.FullName,
Email: org17.Email,
AvatarURL: org17.AvatarLink(t.Context()),
Description: "",
Website: "",
Location: "",
Visibility: "public",
},
{
ID: 3,
Name: org3.Name,
UserName: org3.Name,
FullName: org3.FullName,
Email: org3.Email,
AvatarURL: org3.AvatarLink(t.Context()),
Description: "",
Website: "",
Location: "",
Visibility: "public",
},
}, orgs)
}
@@ -0,0 +1,177 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/require"
)
func TestAPIPublicOnlySelfUserRoutes(t *testing.T) {
defer tests.PrepareTestEnv(t)()
privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user31"})
require.True(t, privateUser.Visibility.IsPrivate())
privateSession := loginUser(t, privateUser.Name)
privateReadUserToken := getTokenForLoggedInUser(t, privateSession,
auth_model.AccessTokenScopePublicOnly,
auth_model.AccessTokenScopeReadUser,
)
privateWriteUserToken := getTokenForLoggedInUser(t, privateSession,
auth_model.AccessTokenScopePublicOnly,
auth_model.AccessTokenScopeWriteUser,
)
t.Run("PrivateProfileForbidden", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
MakeRequest(t, NewRequest(t, "GET", "/api/v1/users/user31").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
})
t.Run("PrivateSensitiveSelfRoutesForbidden", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/settings").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
hideEmail := true
settingsReq := NewRequestWithJSON(t, "PATCH", "/api/v1/user/settings", &api.UserSettingsOptions{
HideEmail: &hideEmail,
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, settingsReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/emails").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
emailReq := NewRequestWithJSON(t, "POST", "/api/v1/user/emails", &api.CreateEmailOption{
Emails: []string{"user31-public-only@example.com"},
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, emailReq, http.StatusForbidden)
keyReq := NewRequestWithJSON(t, "POST", "/api/v1/user/keys", api.CreateKeyOption{
Title: "public-only-private-key",
Key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, keyReq, http.StatusForbidden)
oauthReq := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &api.CreateOAuth2ApplicationOptions{
Name: "public-only-private-oauth-app",
RedirectURIs: []string{"https://example.com/callback"},
ConfidentialClient: true,
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, oauthReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/gpg_keys").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
gpgKeyReq := NewRequestWithJSON(t, "POST", "/api/v1/user/gpg_keys", &api.CreateGPGKeyOption{
ArmoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\ncomment\n-----END PGP PUBLIC KEY BLOCK-----",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, gpgKeyReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/gpg_key_token").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
gpgVerifyReq := NewRequestWithJSON(t, "POST", "/api/v1/user/gpg_key_verify", &api.VerifyGPGKeyOption{
KeyID: "deadbeef",
Signature: "invalid-signature",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, gpgVerifyReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/actions/variables").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "DELETE", "/api/v1/user/actions/secrets/PRIVATE_SECRET").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
variableReq := NewRequestWithJSON(t, "POST", "/api/v1/user/actions/variables/PRIVATE_VAR", api.CreateVariableOption{
Value: "private-value",
Description: "must stay private",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, variableReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "POST", "/api/v1/user/actions/runners/registration-token").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/hooks").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
hookReq := NewRequestWithJSON(t, "POST", "/api/v1/user/hooks", api.CreateHookOption{
Type: "gitea",
Config: api.CreateHookOptionConfig{
"content_type": "json",
"url": "http://example.com/",
},
Name: "public-only-private-hook",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, hookReq, http.StatusForbidden)
avatarReq := NewRequestWithJSON(t, "POST", "/api/v1/user/avatar", &api.UpdateUserAvatarOption{
Image: "aGVsbG8=",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, avatarReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "DELETE", "/api/v1/user/avatar").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/times").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/stopwatches").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/subscriptions").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/teams").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/blocks").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "PUT", "/api/v1/user/blocks/user2").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "PUT", "/api/v1/user/following/user2").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "DELETE", "/api/v1/user/following/user2").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
})
t.Run("PublicRepoRoutesFilterAndRejectMutations", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
publicSession := loginUser(t, "user2")
fullWriteRepoToken := getTokenForLoggedInUser(t, publicSession,
auth_model.AccessTokenScopeWriteUser,
auth_model.AccessTokenScopeWriteRepository,
)
publicOnlyReadRepoToken := getTokenForLoggedInUser(t, publicSession,
auth_model.AccessTokenScopePublicOnly,
auth_model.AccessTokenScopeReadUser,
auth_model.AccessTokenScopeReadRepository,
)
publicOnlyWriteRepoToken := getTokenForLoggedInUser(t, publicSession,
auth_model.AccessTokenScopePublicOnly,
auth_model.AccessTokenScopeWriteUser,
auth_model.AccessTokenScopeWriteRepository,
)
publicRepoName := "public-only-visible-self-repo"
privateRepoName := "public-only-hidden-self-repo"
resp := MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
Name: publicRepoName,
Private: false,
}).AddTokenAuth(fullWriteRepoToken), http.StatusCreated)
publicRepo := DecodeJSON(t, resp, &api.Repository{})
require.Equal(t, "user2/"+publicRepoName, publicRepo.FullName)
resp = MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
Name: privateRepoName,
Private: true,
}).AddTokenAuth(fullWriteRepoToken), http.StatusCreated)
privateRepo := DecodeJSON(t, resp, &api.Repository{})
require.Equal(t, "user2/"+privateRepoName, privateRepo.FullName)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/repos/user2/"+privateRepoName).AddTokenAuth(publicOnlyReadRepoToken), http.StatusNotFound)
resp = MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/repos").AddTokenAuth(publicOnlyReadRepoToken), http.StatusOK)
repos := DecodeJSON(t, resp, []api.Repository{})
foundPublicRepo := false
for _, repo := range repos {
require.NotEqual(t, privateRepo.FullName, repo.FullName)
if repo.FullName == publicRepo.FullName {
foundPublicRepo = true
}
}
require.True(t, foundPublicRepo)
MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
Name: "public-only-rejected-self-repo",
Private: false,
}).AddTokenAuth(publicOnlyWriteRepoToken), http.StatusForbidden)
})
}
+22
View File
@@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPIStar(t *testing.T) {
@@ -153,3 +154,24 @@ func TestAPIStarDisabled(t *testing.T) {
MakeRequest(t, req, http.StatusForbidden)
})
}
func TestAPIStarPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/api/v1/user/starred").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
repos := DecodeJSON(t, resp, []api.Repository{})
if assert.Len(t, repos, 1) {
assert.Equal(t, "user5/repo4", repos[0].FullName)
}
req = NewRequest(t, "GET", "/api/v1/users/user2/starred").
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
repos = DecodeJSON(t, resp, []api.Repository{})
require.Len(t, repos, 1)
assert.Equal(t, "user5/repo4", repos[0].FullName)
}
+25
View File
@@ -92,3 +92,28 @@ func TestAPIWatch(t *testing.T) {
MakeRequest(t, req, http.StatusNoContent)
})
}
func TestAPIWatchPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user1")
writeRepoToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeReadUser)
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopePublicOnly, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository)
MakeRequest(t, NewRequest(t, "PUT", "/api/v1/repos/user2/repo1/subscription").AddTokenAuth(writeRepoToken), http.StatusOK)
MakeRequest(t, NewRequest(t, "PUT", "/api/v1/repos/user2/repo2/subscription").AddTokenAuth(writeRepoToken), http.StatusOK)
resp := MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/subscriptions").AddTokenAuth(publicOnlyToken), http.StatusOK)
repos := DecodeJSON(t, resp, []api.Repository{})
for _, r := range repos {
assert.False(t, r.Private, "private repo %s leaked via /user/subscriptions", r.FullName)
}
assert.NotContains(t, repoNames(repos), "user2/repo2")
resp = MakeRequest(t, NewRequest(t, "GET", "/api/v1/users/user1/subscriptions").AddTokenAuth(publicOnlyToken), http.StatusOK)
repos = DecodeJSON(t, resp, []api.Repository{})
for _, r := range repos {
assert.False(t, r.Private, "private repo %s leaked via /users/{username}/subscriptions", r.FullName)
}
assert.NotContains(t, repoNames(repos), "user2/repo2")
}
+22 -28
View File
@@ -5,7 +5,6 @@ package integration
import (
"context"
"encoding/hex"
"fmt"
"io"
mathRand "math/rand/v2"
@@ -246,8 +245,8 @@ func rawTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS s
// Request raw paths
req := NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", little))
resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
assert.Equal(t, testFileSizeSmall, resp.Length)
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, testFileSizeSmall, resp.Body.Len())
if setting.LFS.StartServer {
req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", littleLFS))
@@ -261,8 +260,8 @@ func rawTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS s
if !testing.Short() {
req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", big))
resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
assert.Equal(t, testFileSizeLarge, resp.Length)
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, testFileSizeLarge, resp.Body.Len())
if setting.LFS.StartServer {
req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", bigLFS))
@@ -287,22 +286,22 @@ func mediaTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS
// Request media paths
req := NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", little))
resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
assert.Equal(t, testFileSizeSmall, resp.Length)
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, testFileSizeSmall, resp.Body.Len())
req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", littleLFS))
resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
assert.Equal(t, testFileSizeSmall, resp.Length)
resp = session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, testFileSizeSmall, resp.Body.Len())
if !testing.Short() {
req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", big))
resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
assert.Equal(t, testFileSizeLarge, resp.Length)
resp = session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, testFileSizeLarge, resp.Body.Len())
if setting.LFS.StartServer {
req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", bigLFS))
resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
assert.Equal(t, testFileSizeLarge, resp.Length)
resp = session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, testFileSizeLarge, resp.Body.Len())
}
}
})
@@ -560,13 +559,11 @@ func doMergeFork(ctx, baseCtx APITestContext, baseBranch, headBranch string) fun
t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr))
// Then get the diff string
var diffHash string
var diffLength int
var diffContent string
t.Run("GetDiff", func(t *testing.T) {
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d.diff", url.PathEscape(baseCtx.Username), url.PathEscape(baseCtx.Reponame), pr.Index))
resp := ctx.Session.MakeRequestNilResponseHashSumRecorder(t, req, http.StatusOK)
diffHash = string(resp.Hash.Sum(nil))
diffLength = resp.Length
resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
diffContent = resp.Body.String()
})
// Now: Merge the PR & make sure that doesn't break the PR page or change its diff
@@ -578,17 +575,17 @@ func doMergeFork(ctx, baseCtx APITestContext, baseBranch, headBranch string) fun
assert.NoError(t, err)
assert.Equal(t, oldMergeBase, pr2.MergeBase)
})
t.Run("EnsurDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength))
t.Run("EnsurDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffContent))
// Then: Delete the head branch & make sure that doesn't break the PR page or change its diff
t.Run("DeleteHeadBranch", doBranchDelete(baseCtx, baseCtx.Username, baseCtx.Reponame, headBranch))
t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr))
t.Run("EnsureDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength))
t.Run("EnsureDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffContent))
// Delete the head repository & make sure that doesn't break the PR page or change its diff
t.Run("DeleteHeadRepository", doAPIDeleteRepository(ctx))
t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr))
t.Run("EnsureDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength))
t.Run("EnsureDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffContent))
}
}
@@ -632,15 +629,12 @@ func doEnsureCanSeePull(ctx APITestContext, pr api.PullRequest) func(t *testing.
}
}
func doEnsureDiffNoChange(ctx APITestContext, pr api.PullRequest, diffHash string, diffLength int) func(t *testing.T) {
func doEnsureDiffNoChange(ctx APITestContext, pr api.PullRequest, diffContent string) func(t *testing.T) {
return func(t *testing.T) {
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d.diff", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index))
resp := ctx.Session.MakeRequestNilResponseHashSumRecorder(t, req, http.StatusOK)
actual := string(resp.Hash.Sum(nil))
actualLength := resp.Length
equal := diffHash == actual
assert.True(t, equal, "Unexpected change in the diff string: expected hash: %s size: %d but was actually: %s size: %d", hex.EncodeToString([]byte(diffHash)), diffLength, hex.EncodeToString([]byte(actual)), actualLength)
resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
actual := resp.Body.String()
assert.Equal(t, diffContent, actual)
}
}
+4 -7
View File
@@ -63,17 +63,14 @@ func TestDataAsyncDoubleRead_Issue29101(t *testing.T) {
var data1, data2 []byte
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
wg.Go(func() {
data1, _ = io.ReadAll(r1)
assert.NoError(t, err)
wg.Done()
}()
go func() {
})
wg.Go(func() {
data2, _ = io.ReadAll(r2)
assert.NoError(t, err)
wg.Done()
}()
})
wg.Wait()
assert.Equal(t, testContent, data1)
assert.Equal(t, testContent, data2)
+10 -128
View File
@@ -9,8 +9,6 @@ import (
"encoding/base64"
"flag"
"fmt"
"hash"
"hash/fnv"
"io"
"net/http"
"net/http/cookiejar"
@@ -38,49 +36,10 @@ import (
"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/xeipuuv/gojsonschema"
)
var testWebRoutes *web.Router
type NilResponseRecorder struct {
httptest.ResponseRecorder
Length int
}
func (n *NilResponseRecorder) Write(b []byte) (int, error) {
n.Length += len(b)
return len(b), nil
}
// NewRecorder returns an initialized ResponseRecorder.
func NewNilResponseRecorder() *NilResponseRecorder {
return &NilResponseRecorder{
ResponseRecorder: *httptest.NewRecorder(),
}
}
type NilResponseHashSumRecorder struct {
httptest.ResponseRecorder
Hash hash.Hash
Length int
}
func (n *NilResponseHashSumRecorder) Write(b []byte) (int, error) {
_, _ = n.Hash.Write(b)
n.Length += len(b)
return len(b), nil
}
// NewRecorder returns an initialized ResponseRecorder.
func NewNilResponseHashSumRecorder() *NilResponseHashSumRecorder {
return &NilResponseHashSumRecorder{
Hash: fnv.New32(),
ResponseRecorder: *httptest.NewRecorder(),
}
}
func testMain(m *testing.M) int {
defer log.GetManager().Close()
@@ -177,42 +136,6 @@ func (s *TestSession) MakeRequest(t testing.TB, rw *RequestWrapper, expectedStat
return resp
}
func (s *TestSession) MakeRequestNilResponseRecorder(t testing.TB, rw *RequestWrapper, expectedStatus int) *NilResponseRecorder {
t.Helper()
req := rw.Request
baseURL, err := url.Parse(setting.AppURL)
assert.NoError(t, err)
for _, c := range s.jar.Cookies(baseURL) {
req.AddCookie(c)
}
resp := MakeRequestNilResponseRecorder(t, rw, expectedStatus)
ch := http.Header{}
ch.Add("Cookie", strings.Join(resp.Header()["Set-Cookie"], ";"))
cr := http.Request{Header: ch}
s.jar.SetCookies(baseURL, cr.Cookies())
return resp
}
func (s *TestSession) MakeRequestNilResponseHashSumRecorder(t testing.TB, rw *RequestWrapper, expectedStatus int) *NilResponseHashSumRecorder {
t.Helper()
req := rw.Request
baseURL, err := url.Parse(setting.AppURL)
assert.NoError(t, err)
for _, c := range s.jar.Cookies(baseURL) {
req.AddCookie(c)
}
resp := MakeRequestNilResponseHashSumRecorder(t, rw, expectedStatus)
ch := http.Header{}
ch.Add("Cookie", strings.Join(resp.Header()["Set-Cookie"], ";"))
cr := http.Request{Header: ch}
s.jar.SetCookies(baseURL, cr.Cookies())
return resp
}
const userPassword = "password"
func emptyTestSession(t testing.TB) *TestSession {
@@ -268,7 +191,11 @@ func getTokenForLoggedInUser(t testing.TB, session *TestSession, scopes ...auth.
req := NewRequestWithURLValues(t, "POST", "/user/settings/applications", urlValues)
session.MakeRequest(t, req, http.StatusSeeOther)
flashes := session.GetCookieFlashMessage()
return flashes.InfoMsg
assert.NotNil(t, flashes)
if flashes != nil {
return flashes.InfoMsg
}
return ""
}
type RequestWrapper struct {
@@ -336,7 +263,7 @@ func NewRequestWithBody(t testing.TB, method, urlStr string, body io.Reader) *Re
t.Fatalf("invalid url str: %s", urlStr)
}
req, err := http.NewRequest(method, urlStr, body)
require.NoError(t, err)
assert.NoError(t, err)
if req.URL.User != nil {
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(req.URL.User.String())))
}
@@ -364,35 +291,8 @@ func MakeRequest(t testing.TB, rw *RequestWrapper, expectedStatus int) *httptest
if expectedStatus != NoExpectedStatus {
if expectedStatus != recorder.Code {
logUnexpectedResponse(t, recorder)
require.Equal(t, expectedStatus, recorder.Code, "Request: %s %s", req.Method, req.URL.String())
}
}
return recorder
}
func MakeRequestNilResponseRecorder(t testing.TB, rw *RequestWrapper, expectedStatus int) *NilResponseRecorder {
t.Helper()
req := rw.Request
recorder := NewNilResponseRecorder()
testWebRoutes.ServeHTTP(recorder, req)
if expectedStatus != NoExpectedStatus {
if !assert.Equal(t, expectedStatus, recorder.Code,
"Request: %s %s", req.Method, req.URL.String()) {
logUnexpectedResponse(t, &recorder.ResponseRecorder)
}
}
return recorder
}
func MakeRequestNilResponseHashSumRecorder(t testing.TB, rw *RequestWrapper, expectedStatus int) *NilResponseHashSumRecorder {
t.Helper()
req := rw.Request
recorder := NewNilResponseHashSumRecorder()
testWebRoutes.ServeHTTP(recorder, req)
if expectedStatus != NoExpectedStatus {
if !assert.Equal(t, expectedStatus, recorder.Code,
"Request: %s %s", req.Method, req.URL.String()) {
logUnexpectedResponse(t, &recorder.ResponseRecorder)
// don't use "require" which exits the test case and makes "wait group" wait forever
assert.Equal(t, expectedStatus, recorder.Code, "Request: %s %s", req.Method, req.URL.String())
}
}
return recorder
@@ -428,25 +328,7 @@ func DecodeJSON[T any](t testing.TB, resp *httptest.ResponseRecorder, v T) (ret
// FIXME: JSON-KEY-CASE: for testing purpose only, because many structs don't provide `json` tags, they just use capitalized field names
decoder := json.NewDecoderCaseInsensitive(resp.Body)
require.NoError(t, decoder.Decode(&v))
// don't use "require" which exits the test case and makes "wait group" wait forever
assert.NoError(t, decoder.Decode(&v))
return v
}
func VerifyJSONSchema(t testing.TB, resp *httptest.ResponseRecorder, schemaFile string) {
t.Helper()
schemaFilePath := filepath.Join(filepath.Dir(setting.AppPath), "tests", "integration", "schemas", schemaFile)
_, schemaFileErr := os.Stat(schemaFilePath)
assert.NoError(t, schemaFileErr)
schema, schemaFileReadErr := os.ReadFile(schemaFilePath)
assert.NoError(t, schemaFileReadErr)
assert.NotEmpty(t, schema)
nodeinfoSchema := gojsonschema.NewStringLoader(string(schema))
nodeinfoString := gojsonschema.NewStringLoader(resp.Body.String())
result, schemaValidationErr := gojsonschema.Validate(nodeinfoSchema, nodeinfoString)
assert.NoError(t, schemaValidationErr)
assert.Empty(t, result.Errors())
assert.True(t, result.Valid())
}
+1 -1
View File
@@ -34,7 +34,7 @@ import (
func getIssuesSelection(t testing.TB, htmlDoc *HTMLDoc) *goquery.Selection {
issueList := htmlDoc.doc.Find("#issue-list")
assert.Equal(t, 1, issueList.Length())
return issueList.Find(".item").Find(".issue-title")
return issueList.Find(".item").Find(".issue-item-title")
}
func getIssue(t *testing.T, repoID int64, issueSelection *goquery.Selection) *issues_model.Issue {
+20 -11
View File
@@ -140,20 +140,29 @@ func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) {
func TestPullCreate_EmptyChangesWithSameCommits(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
session := loginUser(t, "user1")
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
testCreateBranch(t, session, "user1", "repo1", "branch/master", "status1", http.StatusSeeOther)
req := NewRequestWithValues(t, "POST", "/user1/repo1/compare/master...status1",
map[string]string{
"title": "pull request from status1",
},
)
session.MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", "/user1/repo1/pulls/1")
resp := session.MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body)
testCreateBranch(t, session, "user2", "repo1", "branch/master", "empty-pr-branch", http.StatusSeeOther)
resp := testPullCreateDirectly(t, session, createPullRequestOptions{
BaseRepoOwner: "user2",
BaseRepoName: "repo1",
BaseBranch: "master",
HeadBranch: "empty-pr-branch",
Title: "empty pr test",
})
prURL := test.RedirectURL(resp)
// check the "merge box" text
req := NewRequest(t, "GET", prURL)
resp = session.MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body)
text := strings.TrimSpace(doc.doc.Find(".merge-section").Text())
assert.Contains(t, text, "This branch is already included in the target branch. There is nothing to merge.")
// check the "files" tab content
req = NewRequest(t, "GET", prURL+"/files")
resp = session.MakeRequest(t, req, http.StatusOK)
doc = NewHTMLParser(t, resp.Body)
assert.Equal(t, "Diff Content Not Available", strings.TrimSpace(doc.Find("#diff-container").Text()))
})
}
+3 -5
View File
@@ -180,14 +180,12 @@ func TestRepoCommitsStatusParallel(t *testing.T) {
var wg sync.WaitGroup
for i := range 10 {
wg.Add(1)
go func(parentT *testing.T, i int) {
parentT.Run(fmt.Sprintf("ParallelCreateStatus_%d", i), func(t *testing.T) {
wg.Go(func() {
t.Run(fmt.Sprintf("ParallelCreateStatus_%d", i), func(t *testing.T) {
ctx := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository)
doAPICreateCommitStatusTest(ctx, path.Base(commitURL), commitstatus.CommitStatusPending, "testci")(t)
wg.Done()
})
}(t, i)
})
}
wg.Wait()
}
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
set -euo pipefail
# Run a compiled *.test binary. When TEST_SHARD is set, enumerate top-level
# tests via -test.list and run only the shard's slice; TestMain skips
# environment setup in -test.list mode. Without TEST_SHARD, runs the binary
# directly.
BINARY=${1:?usage: $0 BINARY}
if [ -z "${TEST_SHARD:-}" ]; then
exec "$BINARY"
fi
if ! [[ "${TEST_TOTAL_SHARDS:-}" =~ ^[1-9][0-9]*$ ]]; then
echo "TEST_TOTAL_SHARDS must be a positive integer, got: ${TEST_TOTAL_SHARDS:-}" >&2
exit 2
fi
if ! [[ "$TEST_SHARD" =~ ^[1-9][0-9]*$ ]] || [ "$TEST_SHARD" -gt "$TEST_TOTAL_SHARDS" ]; then
echo "TEST_SHARD must be in [1, $TEST_TOTAL_SHARDS], got: $TEST_SHARD" >&2
exit 2
fi
NAMES=$("$BINARY" -test.list='^Test' | LC_ALL=C sort -u | awk -v r=$((TEST_SHARD - 1)) -v t="$TEST_TOTAL_SHARDS" '(NR - 1) % t == r')
if [ -z "$NAMES" ]; then
echo "shard $TEST_SHARD/$TEST_TOTAL_SHARDS has no tests assigned" >&2
exit 1
fi
PATTERN=$(echo "$NAMES" | paste -sd '|' -)
echo "Running shard $TEST_SHARD/$TEST_TOTAL_SHARDS ($(echo "$NAMES" | wc -l | tr -d ' ') tests)"
exec "$BINARY" -test.run "^($PATTERN)\$"
+9
View File
@@ -305,6 +305,15 @@ a.suppressed:hover {
color: inherit;
}
/* keyboard focus ring for interactive elements */
a:focus-visible,
button:focus-visible,
.ui.button:focus-visible,
[role="button"]:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: -2px;
}
a.silenced:hover {
text-decoration-line: none;
}
+2 -1
View File
@@ -13,6 +13,7 @@
}
.btn.is-loading > *,
.btn-octicon.is-loading > *,
.button.is-loading > * {
opacity: 0;
}
@@ -23,7 +24,7 @@
display: block;
left: 50%;
top: 50%;
height: min(4em, 66.6%);
height: var(--loading-size, min(4em, 66.6%));
width: fit-content; /* compat: safari - https://bugs.webkit.org/show_bug.cgi?id=267625 */
aspect-ratio: 1;
transform: translate(-50%, -50%);
-4
View File
@@ -21,10 +21,6 @@
white-space: nowrap;
}
.ui.button:focus-visible {
box-shadow: inset 0 0 0 2px currentcolor;
}
@media (max-width: 767.98px) {
.ui.button {
white-space: normal;
+17 -10
View File
@@ -258,30 +258,37 @@ a.ui.ui.ui.grey.label:hover {
font-size: 1rem;
}
/* To let labels break up and wrap across multiple lines (issue title, comment event), use "display: contents here" to apply parent layout.
If the labels-list itself needs some layouts, use extra classes or "tw" helpers. */
/* labels-list layout: span.labels-list > a.item > span.ui.label,
by default, the labels list is rendered as "display: inline",
it can also be rendered as flex by adding flex-related classes (the general ".item" selector also applies) */
.labels-list {
display: contents;
font-size: var(--font-size-label); /* it must match the label font size, otherwise the height mismatches */
max-width: 100%; /* for ellipsis */
font-size: var(--font-size-label); /* must match the label font size, otherwise the height mismatches */
}
.labels-list a {
display: inline-flex;
.labels-list > .item {
max-width: 100%; /* for ellipsis */
}
.labels-list a:hover {
text-decoration: none;
}
.labels-list .ui.label {
min-height: 20px;
padding-top: 0;
padding-bottom: 0;
}
.with-labels-list-inline .labels-list .ui.label + .ui.label {
margin-left: var(--gap-inline);
/* to align with different font sizes, these styles need to be written separately */
#issue-list .labels-list > .item {
margin-right: 4px; /* margin-right (not margin-left) keeps wrapped labels left-aligned */
vertical-align: 2.5px; /* compensates inline-flex baseline so labels center with text */
}
.with-labels-list-inline .labels-list .ui.label {
line-height: var(--line-height-default);
.timeline-item .labels-list > .item {
margin-right: 4px;
vertical-align: 1.5px;
}
/* Scoped labels with different colors on left and right */
+1 -8
View File
@@ -516,10 +516,8 @@ td .commit-summary {
}
.repository.view.issue .comment-list .timeline-item .comment-text-line {
/* TODO: this "line-height" is not ideal (actually it is abused), many layouts depend on this magic value,
for example: alignment of the header arrow and the avatar, view PR commit list left icon layout, dismiss review with reason, etc */
line-height: 32px;
color: var(--color-text-light);
flex: 1;
min-width: 0;
}
@@ -646,11 +644,6 @@ td .commit-summary {
align-items: center;
}
.repository.view.issue .comment-list .event > .comment-text-line {
flex: 1;
min-width: 0;
}
.repository.view.issue .comment-list .event > .detail {
margin-left: 15px;
width: 100%;
+7
View File
@@ -34,6 +34,13 @@
}
}
#issue-list .issue-item-title {
font-size: 16px;
font-weight: var(--font-weight-semibold);
color: var(--color-text);
text-decoration: none;
}
#issue-list .branches {
display: inline-flex;
}
+2 -2
View File
@@ -5,12 +5,12 @@
}
.timeline-item .conversation-holder .bottom-reactions {
margin: 1em 0 0 36px;
margin: 1em 0 0 24px;
padding-bottom: 8px;
}
.bottom-reactions .ui.label {
padding: 5px 8px;
padding: 2px 6px;
font-weight: var(--font-weight-normal);
}
-2
View File
@@ -74,8 +74,6 @@
display: inline-flex;
flex-wrap: wrap;
align-items: center;
/* labels are under effect of this gap here because they are display:contents. Ideally we should make wrapping
of labels work without display: contents and set this to a static value again. */
gap: var(--gap-inline);
max-width: 100%;
color: var(--color-text);
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../../node_modules/swagger-ui-dist/swagger-ui.css";
@import "swagger-ui-dist/swagger-ui.css";
html,
html body,
+80 -22
View File
@@ -3,8 +3,9 @@ import {nextTick, onBeforeUnmount, onMounted, ref, toRefs, watch} from 'vue';
import {SvgIcon} from '../svg.ts';
import ActionStatusIcon from './ActionStatusIcon.vue';
import {addDelegatedEventListener, createElementFromAttrs, toggleElem} from '../utils/dom.ts';
import {formatDatetime} from '../utils/time.ts';
import {formatDatetime, formatDatetimeISO} from '../utils/time.ts';
import {POST} from '../modules/fetch.ts';
import {copyToClipboardWithFeedback} from '../modules/clipboard.ts';
import type {IntervalId} from '../types.ts';
import {toggleFullScreen} from '../utils.ts';
import {localUserSettings} from '../modules/user-settings.ts';
@@ -201,6 +202,22 @@ function endLogGroup(stepIndex: number) {
el._stepLogsActiveContainer = undefined;
}
async function copyStepOutput(event: MouseEvent, stepIndex: number) {
await copyToClipboardWithFeedback(event.currentTarget as HTMLElement, async () => {
const data = await fetchJobData([{step: stepIndex, cursor: null, expanded: true}]);
const stepLog = data.logs.stepsLog?.find((s) => s.step === stepIndex);
const lines: string[] = [];
for (const line of stepLog?.lines ?? []) {
const cmd = parseLogLineCommand(line);
if (cmd?.name === 'hidden' || cmd?.name === 'endgroup') continue;
const ts = formatDatetimeISO(line.timestamp);
const msg = createLogLineMessage(line, cmd).textContent ?? '';
lines.push(`${ts} ${msg}`);
}
return lines.join('\n');
});
}
// show/hide the step logs for a step
function toggleStepLogs(idx: number) {
currentJobStepsStates.value[idx].expanded = !currentJobStepsStates.value[idx].expanded;
@@ -216,7 +233,7 @@ function createLogLine(stepIndex: number, startTime: number, line: LogLine, cmd:
String(line.index),
);
const logTimeStamp = createElementFromAttrs('span', {class: 'log-time-stamp'},
formatDatetime(new Date(line.timestamp * 1000)), // for "Show timestamps"
formatDatetime(line.timestamp * 1000), // for "Show timestamps"
);
const logMsg = createLogLineMessage(line, cmd);
const seconds = Math.floor(line.timestamp - startTime);
@@ -261,17 +278,14 @@ function appendLogs(stepIndex: number, startTime: number, logLines: LogLine[]) {
}
}
async function fetchJobData(abortController: AbortController): Promise<JobData> {
const logCursors = currentJobStepsStates.value.map((it, idx) => {
// cursor is used to indicate the last position of the logs
// it's only used by backend, frontend just reads it and passes it back, it can be any type.
// for example: make cursor=null means the first time to fetch logs, cursor=eof means no more logs, etc
return {step: idx, cursor: it.cursor, expanded: it.expanded};
});
const resp = await POST(props.actionsViewUrl, {
signal: abortController.signal,
data: {logCursors},
});
// "cursor" is used to indicate the last position of the logs.
// It's only used by backend, frontend just reads it and passes it back, it can be any type.
// Frontend knows nothing about its type, never uses its value.
// For example: backend can make cursor=null means the first time to fetch logs, cursor=1234 for a position, cursor=eof for no more logs, etc.
type LogCursor = {step: number, cursor: any, expanded: boolean};
async function fetchJobData(logCursors: LogCursor[], signal?: AbortSignal): Promise<JobData> {
const resp = await POST(props.actionsViewUrl, {signal, data: {logCursors}});
return await resp.json();
}
@@ -286,7 +300,8 @@ async function loadJob() {
const abortController = new AbortController();
loadingAbortController = abortController;
try {
const runJobResp = await fetchJobData(abortController);
const logCursors = currentJobStepsStates.value.map((it, idx) => ({step: idx, cursor: it.cursor, expanded: it.expanded}));
const runJobResp = await fetchJobData(logCursors, abortController.signal);
if (loadingAbortController !== abortController) return;
// FIXME: this logic is quite hacky and dirty, it should be refactored in a better way in the future
@@ -411,7 +426,7 @@ async function hashChangeListener() {
</div>
<div class="job-info-header-right">
<div class="ui top right pointing dropdown custom jump item" @click.stop="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible">
<button class="ui button tw-px-3">
<button class="btn interact-bg tw-p-2">
<SvgIcon name="octicon-gear" :size="18"/>
</button>
<div class="menu transition action-job-menu" :class="{visible: menuVisible}" v-if="menuVisible" v-cloak>
@@ -459,15 +474,25 @@ async function hashChangeListener() {
<SvgIcon
v-if="isDone(run.status) && currentJobStepsStates[stepIdx].expanded && currentJobStepsStates[stepIdx].cursor === null"
name="gitea-running"
class="tw-mr-2 rotate-clockwise"
class="rotate-clockwise"
/>
<SvgIcon
v-else
:name="currentJobStepsStates[stepIdx].expanded ? 'octicon-chevron-down' : 'octicon-chevron-right'"
:class="['tw-mr-2', !isExpandable(jobStep.status) && 'tw-invisible']"
name="octicon-chevron-right"
class="tw-mr-2 step-summary-chevron"
:class="{'tw-invisible': !isExpandable(jobStep.status)}"
/>
<ActionStatusIcon :status="jobStep.status" icon-variant="circle-fill" class="tw-mr-2"/>
<ActionStatusIcon :status="jobStep.status" icon-variant="circle-fill"/>
<span class="step-summary-msg gt-ellipsis">{{ jobStep.summary }}</span>
<button
v-if="isExpandable(jobStep.status)"
class="btn interact-fg step-copy-btn"
:aria-label="locale.copyOutput"
:data-tooltip-content="locale.copyOutput"
@click.stop="copyStepOutput($event, stepIdx)"
>
<SvgIcon name="octicon-copy" :size="14"/>
</button>
<span class="step-summary-duration">{{ jobStep.duration }}</span>
</div>
<!-- the log elements could be a lot, do not use v-if to destroy/reconstruct the DOM,
@@ -552,6 +577,7 @@ async function hashChangeListener() {
padding: 5px 10px;
display: flex;
align-items: center;
gap: 8px;
border-radius: var(--border-radius);
}
@@ -564,12 +590,32 @@ async function hashChangeListener() {
background: var(--color-console-hover-bg);
}
.job-step-container .job-step-summary .step-summary-chevron {
transition: transform 0.1s ease;
}
.job-step-container .job-step-summary.selected .step-summary-chevron {
transform: rotate(90deg);
}
.job-step-container .job-step-summary .step-summary-msg {
flex: 1;
}
.job-step-container .job-step-summary .step-summary-duration {
margin-left: 16px;
.job-step-container .job-step-summary .step-copy-btn {
visibility: hidden;
margin: 0 4px;
}
.job-step-container .job-step-summary:hover .step-copy-btn,
.job-step-container .job-step-summary.selected .step-copy-btn {
visibility: visible;
}
@media (hover: none) {
.job-step-container .job-step-summary:focus-within .step-copy-btn {
visibility: visible;
}
}
.job-step-container .job-step-summary.selected {
@@ -707,13 +753,25 @@ async function hashChangeListener() {
}
.job-log-group-summary {
cursor: pointer;
position: relative;
display: list-item;
list-style: disclosure-closed inside;
padding-left: 58px; /* line-num gutter (48px) + log-msg margin (10px), so the marker sits in the content column */
}
.job-log-group[open] > .job-log-group-summary {
list-style-type: disclosure-open;
}
.job-log-group-summary > .job-log-line {
position: absolute;
inset: 0;
z-index: -1; /* to avoid hiding the triangle of the "details" element */
z-index: -1; /* sit behind the disclosure marker */
overflow: hidden;
}
.job-log-group-summary > .job-log-line .log-msg {
margin-left: 21px;
}
</style>
+48 -32
View File
@@ -142,35 +142,36 @@ async function deleteArtifact(name: string) {
<div class="action-view-body">
<div class="action-view-left">
<!-- summary -->
<a class="job-brief-item silenced" :href="run.viewLink" :class="!props.jobId ? 'selected' : ''">
<SvgIcon name="octicon-home"/>
<span class="gt-ellipsis">{{ locale.summary }}</span>
</a>
<div class="flex-items-block action-view-sidebar-list">
<a class="item silenced" :href="run.viewLink" :class="!props.jobId ? 'selected' : ''">
<SvgIcon name="octicon-home"/>
<span class="gt-ellipsis">{{ locale.summary }}</span>
</a>
</div>
<!-- jobs list -->
<div class="ui divider"/>
<div class="left-list-header">{{ locale.allJobs }}</div>
<!-- unlike other lists, the items have paddings already -->
<ul class="ui relaxed list flex-items-block tw-p-0">
<li class="item job-brief-item" v-for="job in run.jobs" :key="job.id" :class="props.jobId === job.id ? 'selected' : ''">
<a class="tw-contents silenced" :href="job.link">
<div class="flex-items-block action-view-sidebar-list">
<div class="item" v-for="job in run.jobs" :key="job.id" :class="props.jobId === job.id ? 'selected' : ''">
<a class="flex-text-block tw-flex-1 silenced" :href="job.link">
<ActionStatusIcon :locale-status="locale.status[job.status]" :status="job.status" icon-variant="circle-fill"/>
<span class="tw-flex-1 gt-ellipsis">{{ job.name }}</span>
<SvgIcon name="octicon-sync" role="button" :data-tooltip-content="locale.rerun" class="tw-cursor-pointer link-action interact-fg" :data-url="`${run.link}/jobs/${job.id}/rerun`" v-if="job.canRerun"/>
<span>{{ job.duration }}</span>
<SvgIcon name="octicon-sync" role="button" :data-tooltip-content="locale.rerun" class="job-rerun-button tw-cursor-pointer link-action interact-fg" :data-url="`${run.link}/jobs/${job.id}/rerun`" v-if="job.canRerun"/>
<span class="job-duration">{{ job.duration }}</span>
</a>
</li>
</ul>
</div>
</div>
<!-- artifacts list -->
<template v-if="artifacts.length > 0">
<div class="ui divider"/>
<div class="left-list-header">{{ locale.artifactsTitle }} ({{ artifacts.length }})</div>
<ul class="ui relaxed list flex-items-block">
<li class="item" v-for="artifact in artifacts" :key="artifact.name">
<div class="flex-items-block action-view-sidebar-list">
<div class="item" v-for="artifact in artifacts" :key="artifact.name">
<template v-if="artifact.status !== 'expired'">
<a
class="tw-flex-1 flex-text-block muted" target="_blank"
class="tw-flex-1 tw-min-w-0 flex-text-block silenced" target="_blank"
:href="buildArtifactLink(artifact.name)"
:data-tooltip-content="buildArtifactTooltipHtml(artifact, locale.artifactExpiresAt)"
data-tooltip-render="html"
@@ -179,7 +180,7 @@ async function deleteArtifact(name: string) {
<SvgIcon name="octicon-file" class="tw-text-text-light"/>
<span class="tw-flex-1 gt-ellipsis">{{ artifact.name }}</span>
</a>
<a v-if="run.canDeleteArtifact" class="muted" @click="deleteArtifact(artifact.name)">
<a v-if="run.canDeleteArtifact" class="silenced" @click="deleteArtifact(artifact.name)">
<SvgIcon name="octicon-trash"/>
</a>
</template>
@@ -188,21 +189,21 @@ async function deleteArtifact(name: string) {
<span class="tw-flex-1 gt-ellipsis">{{ artifact.name }}</span>
<span class="ui label tw-flex-shrink-0">{{ locale.artifactExpired }}</span>
</span>
</li>
</ul>
</div>
</div>
</template>
<!-- run details -->
<div class="ui divider"/>
<div class="left-list-header">{{ locale.runDetails }}</div>
<ul class="ui relaxed list">
<li class="item">
<a class="flex-text-block" :href="`${run.link}/workflow`">
<div class="flex-items-block action-view-sidebar-list">
<div class="item">
<a class="flex-text-block silenced" :href="`${run.link}/workflow`">
<SvgIcon name="octicon-file-code" class="tw-text-text"/>
<span class="gt-ellipsis">{{ locale.workflowFile }}</span>
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="action-view-right">
@@ -309,29 +310,44 @@ async function deleteArtifact(name: string) {
color: var(--color-text-light-2);
}
.action-view-left .ui.relaxed.list {
.action-view-sidebar-list {
margin: var(--gap-block) 0;
padding-left: 10px;
}
.job-brief-item {
.action-view-sidebar-list:first-child {
margin-top: 0;
}
.action-view-sidebar-list > .item {
padding: 6px 10px;
border-radius: var(--border-radius);
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: var(--gap-block);
}
.job-brief-item:hover {
.action-view-sidebar-list > .item:hover {
background-color: var(--color-hover);
}
.job-brief-item.selected {
.action-view-sidebar-list > .item.selected {
font-weight: var(--font-weight-bold);
background-color: var(--color-active);
}
/* the re-run button replaces the duration on hover/focus */
.action-view-sidebar-list > .item .job-rerun-button {
display: none;
}
.action-view-sidebar-list > .item:hover .job-rerun-button,
.action-view-sidebar-list > .item:focus-within .job-rerun-button {
display: inline-flex;
}
/* only swap out the duration when a re-run button exists to take its place */
.action-view-sidebar-list > .item:hover .job-rerun-button ~ .job-duration,
.action-view-sidebar-list > .item:focus-within .job-rerun-button ~ .job-duration {
display: none;
}
/* ================ */
/* action view right */
-40
View File
@@ -1,40 +0,0 @@
import {showTemporaryTooltip} from '../modules/tippy.ts';
import {toAbsoluteUrl} from '../utils.ts';
import {clippie} from 'clippie';
const {copy_success, copy_error} = window.config.i18n;
// Enable clipboard copy from HTML attributes. These properties are supported:
// - data-clipboard-text: Direct text to copy
// - data-clipboard-target: Holds a selector for an element. "value" of <input> or <textarea>, or "textContent" of <div> will be copied
// - data-clipboard-text-type: When set to 'url' will convert relative to absolute urls
export function initGlobalCopyToClipboardListener() {
document.addEventListener('click', async (e) => {
const target = (e.target as HTMLElement).closest('[data-clipboard-text], [data-clipboard-target]');
if (!target) return;
e.preventDefault();
let text = target.getAttribute('data-clipboard-text');
if (text === null) {
const textSelector = target.getAttribute('data-clipboard-target')!;
const textTarget = document.querySelector(textSelector)!;
if (textTarget.nodeName === 'INPUT' || textTarget.nodeName === 'TEXTAREA') {
text = (textTarget as HTMLInputElement | HTMLTextAreaElement).value;
} else if (textTarget.nodeName === 'DIV') {
text = textTarget.textContent;
} else {
throw new Error(`Unsupported element for clipboard target: ${textSelector}`);
}
}
if (text && target.getAttribute('data-clipboard-text-type') === 'url') {
text = toAbsoluteUrl(text);
}
if (text) {
const success = await clippie(text);
showTemporaryTooltip(target, success ? copy_success : copy_error);
}
});
}

Some files were not shown because too many files have changed in this diff Show More