Compare commits

...

104 Commits

Author SHA1 Message Date
Lunny Xiao
2c749ce548 update changelog for 1.26.2 (#37797)
Signed-off-by: Nicolas <bircni@icloud.com>
Co-authored-by: Nicolas <bircni@icloud.com>
2026-05-20 19:57:25 +02:00
Lunny Xiao
f540f57354 update changelog for 1.26.2 (#37577) 2026-05-20 17:24:06 +00:00
Nicolas
1c2d5e9b03 fix(actions): make artifact signature payloads unambiguous (#37707) (#37795)
This PR hardens artifact URL signing by encoding signature inputs in an
unambiguous binary payload before computing the HMAC.

What it changes:

- replace direct concatenation-style signing inputs with explicit
payload builders
- encode string fields with a length prefix before appending their bytes
- encode integer fields as fixed-width binary values instead of decimal
text
- apply the same hardening to both:
  - Actions Artifact V4 signing in `routers/api/actions/artifactsv4.go`
  - artifact download signing in `routers/api/v1/repo/action.go`
- add regression tests that verify distinct field combinations produce
distinct payloads and signatures

Why:

The previous signing logic built HMAC inputs by appending multiple
fields without a strongly structured representation. That kind of
construction can create ambiguity at field boundaries, where different
parameter combinations may serialize into the same byte stream for
signing.

This change removes that ambiguity by constructing a deterministic
payload format with explicit boundaries between fields.

Backport #37707

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
2026-05-20 17:16:21 +00:00
Nicolas
a859221a62 fix(pull): handle empty pull request files view to allow reviews (#37783) (#37785)
Backport #37783

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-05-19 18:45:18 +00:00
wxiaoguang
d37f7b44a9 fix(markup): make RenderString never fail (#37779) (#37780)
Backport #37779
2026-05-19 18:08:11 +00:00
Lunny Xiao
a34eac5ef4 fix: Unify public-only token filtering in API queries and repo access checks (#37118) (#37773)
backport #37118 

This PR closes remaining `public-only` token gaps in the API by making
the restriction apply consistently across repository, organization,
activity, notification, and authenticated `/api/v1/user/...` routes.

Previously, `public-only` tokens were still able to:
- receive private results from some list/search/self endpoints,
- access repository data through ID-based lookups,
- and reach several authenticated self routes that should remain
unavailable for public-only access.

This change treats `public-only` as a cross-cutting visibility boundary:
- list/search endpoints now filter private resources consistently,
- repository lookups enforce the same restriction even when addressed
indirectly,
- and self routes that inherently expose or mutate private account state
now reject `public-only` tokens.

---
Generated by a coding agent with Codex 5.2

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
Co-authored-by: Nicolas <bircni@icloud.com>
2026-05-19 15:38:51 +00:00
Giteabot
6d2b02dac1 fix(permissions): Fix reading permission (#37769) (#37781) 2026-05-19 17:06:09 +02:00
Giteabot
1b70a4451a fix: add natural sort to sortTreeViewNodes (#37772) (#37777)
Backport #37772

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Lavamini Inc <jianwangqau@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Nicolas <bircni@icloud.com>
2026-05-19 09:53:45 +00:00
Giteabot
bc29cd0d3d fix: package creation unique conflict (#37774) (#37776)
Backport #37774

fix #30973

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-05-19 08:58:09 +00:00
wxiaoguang
edfba678ec fix!: add DEFAULT_TITLE_SOURCE setting for pull request title default behavior (#37465) (#37766)
Backport #37465

Make DEFAULT_TITLE_SOURCE default to "auto" like GitHub

---------

Co-authored-by: 0xGREG <28388707+0xGREG@users.noreply.github.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Nicolas <bircni@icloud.com>
2026-05-18 11:01:09 -07:00
Giteabot
9c0ad8291b fix: Add missed token scope checking (#37735) (#37757)
Backport #37735 by @lunny

Follow #37698

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-05-18 06:59:33 +00:00
Giteabot
58597cc30a fix: Allow direct commits for unprotected files with push restrictions (#37657) (#37756)
Backport #37657 by @bircni

Fixes an issue where users could not commit changes on a file which is
unprotected.

Fixes #37655

Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 06:11:19 +00:00
Lunny Xiao
86cc3e8783 fix(oauth): bind token exchanges to the original client request (#37704) (#37740)
Backport #37704 

This PR hardens OAuth token exchange validation by binding exchanged
credentials to the client and redirect URI that originally obtained
them.

What it changes:

- reject refresh token exchanges when the refresh token belongs to a
different OAuth application
- reject authorization code exchanges when the `redirect_uri` in the
token request differs from the `redirect_uri` stored with the
authorization code
- add integration coverage for:
  - authorization code exchange with a mismatched redirect URI
- refresh token reuse across two different dynamically created OAuth
applications

Why:

OAuth authorization codes and refresh tokens must remain bound to the
client context that originally received them. Without those checks:
- a valid authorization code can be redeemed against a different
registered redirect URI of the same client
- a refresh token can be replayed by a different OAuth client

---------

Co-authored-by: Nicolas <bircni@icloud.com>
2026-05-17 22:17:33 +02:00
Lunny Xiao
5038561235 fix(oauth): strengthen PKCE validation and refresh token replay protection (#37706) (#37738)
Backport #37706 

This PR tightens several OAuth validation paths related to PKCE
handling, redirect URI normalization, and refresh-token replay safety.

What it changes:

- switch redirect URI comparison to ASCII-only normalization for
exact-match checks, avoiding Unicode case-folding surprises
- harden PKCE verification by:
  - allowing PKCE omission only when no challenge data was stored
  - rejecting exchanges with a missing verifier when PKCE was used
- rejecting malformed challenge state where a challenge exists without a
valid method
  - comparing derived challenges with constant-time string matching
- make refresh-token invalidation counter updates conditional on the
previously observed counter value, so stale refresh state cannot be
accepted after the grant changes

Why:

These checks close gaps where:
- redirect URI comparisons could rely on broader Unicode normalization
than intended
- malformed or incomplete PKCE state could be treated too permissively
- concurrent or stale refresh-token use could advance the same grant
more than once

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
Co-authored-by: Nicolas <bircni@icloud.com>
2026-05-17 09:04:58 +00:00
Zettat123
1d7b84922f fix(actions): wrong assumption that run id always >= job id (#37737) (#37742)
Backport #37737

Fix #37734

Follow up #37008

The `jobNum >= runNum` check is useless. Removed it to support `job_id <
run_id`
2026-05-17 08:42:20 +02:00
Giteabot
2965b0c08a fix(web): enforce token scopes on raw, media, and attachment downloads (#37698) (#37733) 2026-05-16 18:18:44 +02:00
Giteabot
ab0d52b4c7 fix(auth): set User-Agent on avatar fetch and sync avatar on link-account register (#37564) (#37588) (#37726)
Backport #37588 by @pandareen

## Summary

Fixes
[go-gitea/gitea#37564](https://github.com/go-gitea/gitea/issues/37564):
when an OIDC provider returns a `picture` claim, Gitea is supposed to
download that image as the user's avatar (if `[oauth2_client]
UPDATE_AVATAR = true`). Two latent bugs prevented this from working
consistently:

1. **Default Go User-Agent rejected by some image hosts.**
`oauth2UpdateAvatarIfNeed` used `http.Get`, which sends `User-Agent:
Go-http-client/1.1`. Hosts like `upload.wikimedia.org` reject that UA
with `403`, and every error path silently returned, so the user was left
with an identicon and **no log line** to diagnose the issue.
2. **Link-account *register* path skipped avatar sync.** First-time OIDC
sign-ins where auto-registration is disabled (or required a
username/password retype) go through `LinkAccountPostRegister`, which
created the user but never called `oauth2SignInSync`. So the avatar /
full name / SSH keys from the IdP were dropped on the floor for those
users, even though the existing-account-link path (`oauth2LinkAccount`)
and the auto-register path (`handleOAuth2SignIn`) both already did the
sync.

## Changes

- `routers/web/auth/oauth.go` — `oauth2UpdateAvatarIfNeed` now uses
`http.NewRequest` + `http.DefaultClient.Do`, sets `User-Agent: Gitea
<version>`, and logs every failure path at `Warn` (invalid URL, fetch
error, non-200, body read error, oversize body, upload error). No silent
failures.
- `routers/web/auth/linkaccount.go` — `LinkAccountPostRegister` now
calls `oauth2SignInSync` after a successful user creation, mirroring the
auto-register and link-existing-account flows.
- `tests/integration/oauth_avatar_test.go` — new
`TestOAuth2AvatarFromPicture` integration test with five sub-cases:
- `AutoRegister_FetchesAvatarFromPictureWithGiteaUA` — happy path,
asserts `use_custom_avatar=true`, an avatar hash is set, exactly one
HTTP request was made, and the request carried a `Gitea ` UA. The mock
server enforces the UA prefix to mirror real-world hosts that reject
Go's default UA.
- `AutoRegister_NonOK_DoesNotUpdateAvatar` — server returns 403; user's
avatar must remain unset.
- `AutoRegister_EmptyPicture_NoFetch` — empty `picture` claim must not
trigger any HTTP request.
- `AutoRegister_UpdateAvatarFalse_NoFetch` — `UPDATE_AVATAR=false` must
not trigger any HTTP request.
- `LinkAccountRegister_FetchesAvatarFromPicture` — guards the
`linkaccount.go` fix; without the new `oauth2SignInSync` call this
assertion fails.

## Test plan

- [x] `go test -tags 'sqlite sqlite_unlock_notify' -run
'^TestOAuth2AvatarFromPicture$' ./tests/integration/ -v` — 5/5 sub-tests
pass.
- [x] Manual: log in as a Keycloak user with `picture` claim pointing at
`https://avatars.githubusercontent.com/u/9919?v=4` — Gitea avatar is
replaced with the GitHub picture.
- [x] Manual: same flow with `https://upload.wikimedia.org/...` —
request now succeeds (or returns a clearly logged `Warn` line if
rate-limited with `429`); previously it silently 403'd.
- [x] Manual: `UPDATE_AVATAR=false` — user keeps the identicon, no
outbound request in container logs.
- [ ] Reviewer: please double-check that no other call sites of
`oauth2UpdateAvatarIfNeed` rely on the old `http.Get` behaviour.

## Related

- Upstream issue: go-gitea/gitea#37564
--------------------------------------------


AI Editor was used in this PR

---------

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: pandareen <7270563+pandareen@users.noreply.github.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Nicolas <bircni@icloud.com>
2026-05-16 14:15:53 +00:00
Giteabot
519b8d6d88 fix(security): enforce wiki git writes and LFS token access at request time (#37695) (#37714)
Backport #37695 by @lunny

This PR fixes two permission-checking gaps in Git and LFS request
handling.

## What it changes

- keep wiki Git HTTP pushes on the normal write-permission path, even
when proc-receive support is enabled
- revalidate LFS bearer token requests against the current user state
and current repository permissions before allowing access
- add regression coverage for unauthorized wiki HTTP pushes
- add LFS tests for blocked users, revoked repository access, read-only
upload attempts, and valid write access

## Why

- wiki repositories should not inherit the relaxed refs/for handling
used for normal code repositories
- LFS authorization tokens should not remain usable after a user is
disabled or loses repository access

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-05-16 06:58:28 +00:00
Zettat123
7b82ded82a fix(actions): deadlock between PrepareRunAndInsert and UpdateTaskByState (#37692) (#37718) 2026-05-16 07:02:14 +02:00
wxiaoguang
1d5163133b fix(repo): /generate must sync the branch table for the new repo (#37693) (#37712)
Backport #37693
2026-05-16 01:54:48 +08:00
Giteabot
0e53c41694 feat(api): encrypt AWS creds (#37679) (#37713)
Backport #37679 by @Exgene

## Description

As mentioned in #37654 `AWSAccessKeyID` and `AWSSecretAccessKey` are not
encrypted and stored as is.

## Update

Follow the existing `AuthToken` flow of setting the `Encrypted` fields,
`Decrypting` them later and `Clearing` them at the end.

Closes #37654

Signed-off-by: Kausthubh J Rao <105716675+Exgene@users.noreply.github.com>
Co-authored-by: Kausthubh J Rao <105716675+Exgene@users.noreply.github.com>
Co-authored-by: Lauris B <lauris@nix.lv>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
2026-05-15 15:58:19 +02:00
Lunny Xiao
c7af094b0a build: Fix snap build (1.26) (#37686)
---------

Signed-off-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-13 17:58:43 +00:00
Giteabot
28729ef7e3 fix(actions): run TransferLogs on UpdateLog{Rows:[], NoMore:true} (#37631) (#37687)
Backport #37631 by @silverwind

`UpdateLog` short-circuits on `len(Rows)==0` before honoring `NoMore`,
so a final empty `UpdateLog{NoMore:true}` never runs `TransferLogs`. The
task's `dbfs_data` rows are then never moved to log storage and never
deleted.

The bug has been latent since the original Actions implementation,
`act_runner` versions after
[runner#819](https://gitea.com/gitea/runner/pulls/819) trip it
deterministically.

Fix: let `NoMore=true` with no new rows fall through to `TransferLogs`.
Bail when the runner has outrun the server (`Index > ack`) even with
`NoMore`, since archiving a log with a gap is worse than retrying.
Always call `WriteLogs` so `offset==0` bootstraps an empty DBFS file in
the no-output case (otherwise `TransferLogs` would fail at `dbfs.Open`).

Fixes: https://github.com/go-gitea/gitea/issues/37623
Ref: [runner#952](https://gitea.com/gitea/runner/pulls/952)
Ref: [runner#950](https://gitea.com/gitea/runner/pulls/950)

---
This PR was written with the help of Claude Opus 4.7

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
2026-05-13 06:58:28 +00:00
silverwind
57dd9f5bab fix(deps): update dependency mermaid to v11.15.0 [security], add e2e test (#37665)
Backport of #37662.

---
This PR was written with the help of Claude Opus 4.7

---------

Co-authored-by: Giteabot <teabot@gitea.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
2026-05-12 08:37:33 +02:00
Nicolas
5829522019 fix show correct mergebase (#37656)
## Summary

When comparing branches with **no common merge base** (e.g. unrelated
histories or orphan branches), `PageIsComparePull` is false and
`CommitCount` is zero. The compare template still showed
`repo.commits.nothing_to_compare`, which in German reads like the
branches are identical—even though the flash already explains there is
no merge base.

## Changes

- **`templates/repo/diff/compare.tmpl`**: Only render the grey “nothing
to compare” segment when `CompareInfo.CompareBase` is set.

<img width="1962" height="564"
src="https://github.com/user-attachments/assets/adc3b4a0-6f03-45da-b297-e15e5ad0aa79"
/>

---

Backport of https://github.com/go-gitea/gitea/pull/37651

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-05-11 18:07:23 +00:00
Giteabot
5eaa0bc603 fix(packages): Add label for private and internal package and fix composor package source permission check (#37610) (#37643)
Backport #37610 by @lunny

- Add permission checks for Composer package source links

- Add private/internal visibility labels for packages, similar to
repository visibility labels

<img width="969" height="571" alt="image"
src="https://github.com/user-attachments/assets/8a8ec3a0-bfbd-4dd6-b45b-58eda5db1a2d"
/>

- Add a link to change package visibility

<img width="1309" height="208" alt="image"
src="https://github.com/user-attachments/assets/3fa82b23-4c63-4a5e-b3f0-d37a103231ee"
/>

- Update link package descriptions

<img width="1308" height="265" alt="image"
src="https://github.com/user-attachments/assets/2c80b50e-5ffe-4d96-aedd-aa15964c4e05"
/>

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-05-11 10:36:07 -07:00
Giteabot
fb159eae8f fix: "run as root" check (#37622) (#37625)
Backport #37622

Remove the hacky and fragile `sed os.Getuid()` patch.

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-05-09 17:02:21 +00:00
Giteabot
631a9b5d16 fix: make clone URL respect public URL detection setting (#37615) (#37617)
Backport #37615 by @wxiaoguang

Fix #37614

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-05-08 23:04:04 +02:00
techknowlogick
5636219dbc chore(deps): bump go-git/go-git/v5 to 5.19.0 (#37608) (#37609)
Co-authored-by: Nicolas <bircni@icloud.com>
2026-05-08 20:16:22 +00:00
Giteabot
439984474c chore(deps): update dependency go to v1.26.3 (#37601) (#37613) 2026-05-08 19:40:49 +00:00
Nicolas
a55be951e3 Compare dropdown fails when selecting branch with no common merge-base (#37470) (#37472)
## Summary

- handle compare requests where base and head refs have no common merge
base without returning 500
- keep the compare branch selectors usable and show a clear warning
message
- add regression coverage for unrelated-history compare selection and
merge-base error detection

Fixes #37469 



Manuel Backport of: https://github.com/go-gitea/gitea/pull/37470

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-05-08 19:08:36 +00:00
Giteabot
65f3feaa84 fix: treat email addresses case-insensitively (#37600) (#37611) 2026-05-08 18:32:25 +00:00
Giteabot
b28c4f2b08 fix(actions): fix blank lines after ::endgroup:: (#37597) (#37612)
Backport #37597 by @silverwind

`endLogGroup` was incorrectly appending empty `<div>`s, producing a
useless blank line after every group. Before and after:

<img width="250" alt="Screenshot 2026-05-07 at 22 40 40"
src="https://github.com/user-attachments/assets/8baf0fd0-99c8-4648-bf3f-edc6c4b197ec"
/> <img width="250" alt="Screenshot 2026-05-07 at 22 37 12"
src="https://github.com/user-attachments/assets/c45f28ae-1bbf-4b25-9d7b-281c19421f63"
/>

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
2026-05-08 17:30:33 +00:00
Giteabot
677ab982bf fix(git): Fix smart http request scope bug (#37583) (#37605)
Backport #37583 by @lunny

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-05-08 09:14:41 -07:00
Giteabot
e10da87ebe fix(actions): report individual step status in workflow job API response (#37592) (#37598)
Backport #37592 by @bircni

When a workflow job failed, the API response reported all steps as
failed — even steps that had completed successfully before the failing
step. `ToActionWorkflowJob` was calling `ToActionsStatus(job.Status)`
for every step instead of `ToActionsStatus(step.Status)`, so the job's
overall conclusion was propagated to each step.

Each `ActionTaskStep` has its own `Status` field that tracks the actual
outcome of that step independently of the job result.

Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-05-08 00:14:45 +02:00
wxiaoguang
3004c45607 fix: Invalid UTF-8 commit messages in JSON API responses (#37542) (#37585)
Backport #37542

Co-authored-by: Nicolas <bircni@icloud.com>

---------

Co-authored-by: Nicolas <bircni@icloud.com>
2026-05-07 16:22:09 +00:00
Giteabot
7d77631881 fix: use consistent GetUser family functions (#37553) (#37589)
Backport #37553

fixes adding collaborative owners in Actions settings when the user or
organization name contains capital letters.

Fixes #37548

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-05-07 15:43:45 +00:00
Giteabot
2bafa41554 fix(api): return 409 message instead of empty JSON for wrong commit id (#37572) (#37584)
Backport #37572 by @Exgene

## Issue
Closes #37217 

The error string was getting lost while returning due to `ctx.JSON()`
which cannot serialize the `error` object.

## Fix

Use `ctx.APIError()` to return proper error messages back to the client.

Co-authored-by: Kausthubh J Rao <105716675+Exgene@users.noreply.github.com>
2026-05-07 11:36:52 +02:00
Giteabot
b586d80f97 fix(actions): prevent panic when workflow contains null jobs (#37570) (#37576)
Backport #37570 by @Exgene

## The issue

Closes #37568. Basically due to empty fields being present in the
actions file, the jobs would be produced as `nil` inside `jobparser.go`
. Because of this when we call `Parse` on the `jobparser` module.

```go
Needs:   job.Needs(),
```

would propagate the `nil` job down the chain. 

## The fix

For now i decide to fix it by guarding with an `if job == nil` check.

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Kausthubh J Rao <105716675+Exgene@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-05-07 02:31:49 +00:00
Giteabot
58a66cae3c Make ServeSetHeaders default to download attachment if filename exists (#37552) (#37555)
Backport #37552

Fix #37550

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-05-05 18:21:07 +00:00
Giteabot
356a119f30 fix(actions): validate workflow param to prevent 500 error (#37546) (#37554)
Backport #37546 by @KalashThakare

This PR fixes issue #37523:

1. Prevents a 500 error on the Actions page when disabling workflows
with an empty workflow parameter
2. Uses a single **ctx.JSONError** in the handler to return 400 Bad
Request with the message “workflow is required” for empty input

Co-authored-by: Kalash Thakare ☯︎ <kalashthakare898@gmail.com>
2026-05-05 19:49:17 +02:00
Giteabot
b79529015e Don't unblock run-level-concurrency-blocked runs in the resolver (#37461) (#37538)
Backport #37461 by @silverwind

Fixes #37446.

The job-status resolver in `checkJobsOfCurrentRunAttempt` only
considered `needs` and job-level concurrency when transitioning jobs out
of `Blocked`. When something drove the resolver against a run blocked
solely by workflow-level concurrency — for example, a sibling run in the
same group entering the queue and triggering `EmitJobsIfReadyByRun` —
the run's job silently became `Waiting` while another run still held the
concurrency group, and the runner could pick it up, defeating the
concurrency guarantee.

The fix bails out of the resolver when the run's latest attempt is still
blocked by run-level concurrency. `checkRunConcurrency` re-evaluates
when the holding run finishes.

Covered by a unit test
(`Test_checkJobsOfCurrentRunAttempt_RunLevelConcurrencyKeepsJobsBlocked`
in `services/actions/job_emitter_test.go`) that sets up a Running holder
attempt and a Blocked sibling attempt in the same concurrency group
directly in the DB, calls `checkJobsOfCurrentRunAttempt`, and asserts
the blocked job stays `Blocked`. Fails on master, passes with the fix.

---
This PR was written with the help of Claude Opus 4.7

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
2026-05-04 13:37:20 -07:00
Giteabot
eeb4d8ffa2 fix(packages): use file names for generic web downloads (#37514) (#37520)
Backport #37514 

Fixes #37511.

Signed-off-by: cyphercodes <cyphercodes@users.noreply.github.com>
Co-authored-by: Rayan Salhab <r.salhab@aiyexpertsolutions.com>
Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-05-03 11:24:02 -07:00
Giteabot
dd78d87dcd fix: merge autodetect can't close other PRs but only the last one when multiple PRs are pushed at once (#37512) (#37516)
Backport #37512

Fixes #37510.

Co-authored-by: Jason Learst <jason@jasonlearst.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-05-03 01:15:56 -07:00
Giteabot
e2b211f291 Fix update branch protection order (#37508) (#37513)
Backport #37508 
Regression of changed behavior or Golang JSON v2 package

Fix #37506

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-05-02 19:10:50 +00:00
Giteabot
8a49e9d346 Fix mCaptcha broken after Vite migration (#37492) (#37509) 2026-05-02 18:20:52 +02:00
Nicolas
b88bad2a01 Fix basic auth bug (#37503)
Backport for #37486
2026-05-02 10:58:40 +00:00
Giteabot
5632abff9e Fix review submission from single-commit PR view (#37475) (#37485)
Backport #37475 by @cyphercodes

Fixes #37415.

Pin the review submission form action to the canonical PR files route

Co-authored-by: Rayan Salhab <r.salhab@aiyexpertsolutions.com>
Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
Co-authored-by: Hermes Agent (OpenAI GPT-5.5) <noreply@nousresearch.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-29 21:34:37 +02:00
Giteabot
74e515623b Fix allow maintainer edit permission check (#37479) (#37484)
Backport #37479 by wxiaoguang

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-29 17:07:09 +00:00
Giteabot
4ee74d7699 FIX: URL sanitization to handle schemeless credentials (#37440) (#37471)
Backport #37440 by @bircni

Fixes #37435

Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-28 21:35:18 +00:00
Giteabot
c4a1ff7d16 Fix scheduled action panic with null event payload (#37459) (#37466)
Backport #37459 by cyphercodes

This fixes the scheduled action panic when an event payload is JSON
`null` by initializing the payload map before adding `schedule`. It also
adds regression coverage for the null-payload case.

Fixes #37447.

Co-authored-by: Rayan Salhab <r.salhab@aiyexpertsolutions.com>
Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
Co-authored-by: Hermes Agent (GPT-5.5) <hermes-agent@users.noreply.github.com>
Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
2026-04-28 05:07:26 +00:00
Giteabot
78899832eb Fix attachment Content-Security-Policy (#37455) (#37464)
Backport #37455 by wxiaoguang

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-28 12:08:43 +08:00
wxiaoguang
fb3c1b031d Add CurrentURL template variable back (#37444) (#37449)
Backport #37444
2026-04-27 21:05:24 +08:00
wxiaoguang
cff6eb5661 Make GetPossibleUserByID can handle deleted user (#37430) (#37431)
Backport #37430
2026-04-27 00:33:09 +08:00
Giteabot
2a61284ba5 remove excessive quote from terraform instructions (#37424) (#37426)
Backport #37424 by @TheFox0x7

fixes: https://github.com/go-gitea/gitea/issues/37423

Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
2026-04-25 21:59:29 -07:00
Giteabot
11f77efea5 Fix color regressions, add priority color (#37417) (#37421)
Backport #37417 by @silverwind

- fix markup attention block regressions on 2 colors
- added new color "priority" color for important severity in markup
- all message-box style, and error form elements use monochrome text
- tweaked and improved action logs colors

<img width="722" height="637" alt="Screenshot 2026-04-25 at 17 02 49"
src="https://github.com/user-attachments/assets/e8316fd8-3889-4f67-bdc5-39429b5a7eef"
/>
<img width="885" height="123" alt="image"
src="https://github.com/user-attachments/assets/4a761834-e69a-4f5e-a39d-8e49b75fc39d"
/>

<img width="608" height="554" alt="Screenshot 2026-04-25 at 17 03 16"
src="https://github.com/user-attachments/assets/86694726-817a-42b9-91dc-005bc03720cd"
/>

<img width="319" height="279" alt="image"
src="https://github.com/user-attachments/assets/db2801e9-8963-448c-b1b8-3029a69d5cf3"
/>

<img width="396" height="345" alt="image"
src="https://github.com/user-attachments/assets/8195c20d-e034-442c-b0db-4a8455792d0c"
/>


Fixes: #37416

---
This PR was written with the help of Claude Opus 4.7

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
2026-04-25 19:26:25 +00:00
Lunny Xiao
afdbd9b7c5 change log for 1.26.1 (#37357)
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-24 12:40:36 -07:00
silverwind
64d12024d6 Stabilize e2e logout propagation test (#37408)
Backport of #37403 to `release/v1.26`.

The `events › logout propagation` e2e test was racing the SSE connection
setup: if page2's SharedWorker had not finished registering its
messenger by the time page1 triggered logout, the event was silently
dropped and page2 stayed on the authenticated page.

Wait 500ms after verifying page2 is signed in, before triggering the
logout from page1, so the SharedWorker has time to register. Comment
points at a cleaner future fix (expose a ready attribute on the page)
that will also work for the planned WebSocket SharedWorker.

---
This PR was written with the help of Claude Opus 4.7

Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
2026-04-25 00:20:09 +08:00
Giteabot
6cc1ee9424 fix: dump with default zip type produces uncompressed zip (#37401) (#37402)
Backport #37401

Fix #37393

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: wxiaoguang <2114189+wxiaoguang@users.noreply.github.com>
2026-04-24 17:45:10 +08:00
Giteabot
5d7768f34c Fix repo init README EOL (#37388) (#37399)
Backport #37388 by @wxiaoguang

Fix #27120

By the way, refactor ReserveLineBreakForTextarea to NormalizeStringEOL

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-23 23:33:25 +00:00
Giteabot
55a6cfe79b Fix org team assignee/reviewer lookups for team member permissions (#37365) (#37391)
Backport #37365 by @pisarz77

Fix team members missing from assignee list when `team_unit.access_mode`
is 0 but the doer is owner.

Fix  #34871

1. Use `GetTeamUserIDsWithAccessToAnyRepoUnit` for repo assignee list
2. Load assignee list for project issues directly
3. Use `GetTeamUserIDsWithAccessToAnyRepoUnit` for repo reviewer list

Signed-off-by: Jakub Pisarczyk <pisarz77@gmail.com>
Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: pisarz77 <pisarz77@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
2026-04-23 21:15:53 +02:00
Giteabot
1f643072c1 fix: commit status reporting (#37372) (#37386)
Backport #37372 by @bircni

Fixes the issue that status report always shows waiting to run, when
already running

https://github.com/go-gitea/gitea/issues/36906#issuecomment-4294545813

Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
2026-04-23 16:43:32 +02:00
Giteabot
0280455356 Fix button layout shift when collapsing file tree in editor (#37363) (#37375)
Backport #37363 by @bytedream

---
old:


https://github.com/user-attachments/assets/136a9ce8-f229-4583-bf19-75258d085513

new:


https://github.com/user-attachments/assets/21b7c885-00f4-4295-9191-07b66ca58b64

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: bytedream <me@bytedream.dev>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-22 21:52:48 +02:00
Giteabot
a8e465e893 Add URL to Learn more about blocking a user. (#37355) (#37367)
Backport #37355 by @PineBale

Closes #29992

<img width="1308" height="828" alt="1"
src="https://github.com/user-attachments/assets/552c2e0f-8da6-4f71-8660-8e3f5a78ace5"
/>

Co-authored-by: PineBale <272794187+PineBale@users.noreply.github.com>
2026-04-22 18:38:05 +00:00
Giteabot
fc9dfe0e56 fix: use TriggerEvent instead of Event in workflow runs API response for scheduled runs (#37288) (#37360)
Backport #37288 by @KalashThakare

## Summary

Fixes #37252

The `/api/v1/repos/{owner}/{repo}/actions/runs` endpoint was returning
`event: "push"` for workflow runs triggered by `schedule:` (cron),
instead
of `event: "schedule"`.

## Root Cause

`ActionRun` has two separate fields:
- `Event` — the workflow registration event (e.g. `push`, set when the
workflow file was first pushed)
- `TriggerEvent` — the actual event that triggered the run (e.g.
`schedule`)

`ToActionWorkflowRun` in `services/convert/action.go` was serializing
`run.Event` into the API response instead of `run.TriggerEvent`, causing
scheduled runs to be indistinguishable from push events via the API.

This was already asymmetric — the tasks/jobs API correctly used
`TriggerEvent`.

## Fix

Changed `ToActionWorkflowRun` to use `run.TriggerEvent` for the `event`
field in the API response, consistent with how the jobs API works.

## Before

`event: "push"` returned for all scheduled runs:

<img width="1112" height="191" alt="Screenshot 2026-04-19 115642"
src="https://github.com/user-attachments/assets/c0a169f5-bbd9-4f5d-9474-e4c3795110e4"
/>

## After

`event: "schedule"` correctly returned for scheduled runs:

<img width="890" height="166" alt="Screenshot 2026-04-19 121723"
src="https://github.com/user-attachments/assets/860e99ac-0935-4a43-86a1-7b60f8113480"
/>


## Testing

- Added unit test `TestToActionWorkflowRun_UsesTriggerEvent` in
  `services/convert/action_test.go` that explicitly verifies the API
  returns `TriggerEvent` and not `Event` for a scheduled run.
- Manually verified via the API against a live Gitea instance with a
  `cron: "* * * * *"` workflow.

Co-authored-by: Kalash Thakare ☯︎ <kalashthakare898@gmail.com>
Co-authored-by: Nicolas <bircni@icloud.com>
2026-04-22 18:39:10 +02:00
Giteabot
0916039c2a Add event.schedule context for schedule actions task (#37320) (#37348)
Backport #37320 by @lunny

Fix #35452

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
2026-04-21 21:45:12 +00:00
Giteabot
291f6cbd3a Fix an issue where changing an organization’s visibility caused problems when users had forked its repositories. (#37324) (#37344)
Backport #37324 by @lunny

A quick fix #37317

---

The current behavior for forks when an organization or repository is
changed to private differs from GitHub.

On GitHub, when a parent repository becomes private, the fork
relationship is removed, which keeps the behavior simple and avoids
visibility conflicts.

I think we need a similar solution to handle cases where the parent
repository becomes private while a fork remains public and the fork
relationship is still preserved.

Signed-off-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-21 19:22:35 +00:00
Giteabot
f536bcd508 Use modern "git update-index --cacheinfo" syntax to support more file names (#37338) (#37343)
Backport #37338 by @wxiaoguang

Modern syntax was added in git 2.0

And add more tests

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-21 18:41:40 +00:00
Giteabot
fc4296a21a Fix URL related escaping for oauth2 (#37334) (#37340)
Backport #37334 by wxiaoguang

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-21 17:11:19 +00:00
Giteabot
657ea10cf1 When the requested arch rpm is missing fall back to noarch (#37236) (#37339)
Backport #37236 by chethenry

This fixes: https://github.com/go-gitea/gitea/issues/37235

It uses the same changeset alpine packages got in:
https://github.com/go-gitea/gitea/issues/26691

Co-authored-by: chethenry <henry@visionlink.org>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-21 16:27:01 +00:00
Giteabot
ef096b0f90 fix(oauth): Error on auth sources with spaces (#37327) (#37332)
Backport #37327 by @prettysunflower

Nyallo~

In pull request #36901, a change is made so that the link to
authentication sources is now escaped with the QueryEscape filter.
https://github.com/go-gitea/gitea/pull/36901/changes#diff-34c39c9736a8b62e293c0c0b24c4b5b8c1c792790018c5809f9ff2cbc12b16b1R4

The problem is that [QueryEscape replace spaces with the `+`
character](https://cs.opensource.google/go/go/+/refs/tags/go1.26.2:src/net/url/url.go;l=234;drc=917949cc1d16c652cb09ba369718f45e5d814d8f),
and this is not unescaped when a user tries to log in with an
authentication source that contains a space, which throws an error.

This commit fixes that by unescaping the provider name in the URL.

---

Example of the error, on my instance, when I try to log in with
`prettysunflower's auth`
```
2026/04/21 00:11:41 routers/web/auth/oauth.go:42:SignInOAuth() [E] SignIn: oauth2 source not found, name: "prettysunflower's+auth"
	/go/src/code.gitea.io/gitea/routers/web/auth/oauth.go:42 (0x2cfa5c5)
	/usr/local/go/src/reflect/value.go:586 (0x51e245)
	/usr/local/go/src/reflect/value.go:369 (0x51d0f8)
	/go/src/code.gitea.io/gitea/modules/web/handler.go:181 (0x1a6aaf6)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/src/code.gitea.io/gitea/modules/web/handler.go:188 (0x1a6ab65)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/src/code.gitea.io/gitea/modules/web/handler.go:188 (0x1a6ab65)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/src/code.gitea.io/gitea/modules/web/handler.go:188 (0x1a6ab65)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/src/code.gitea.io/gitea/services/context/context.go:217 (0x2df1b23)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/src/code.gitea.io/gitea/modules/web/handler.go:145 (0x1a6afb5)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/pkg/mod/gitea.com/go-chi/session@v0.0.0-20251124165456-68e0254e989e/session.go:258 (0x197eb82)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/src/code.gitea.io/gitea/modules/web/handler.go:145 (0x1a6afb5)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/pkg/mod/github.com/go-chi/chi/v5@v5.2.5/chain.go:31 (0x1a61d05)
	/go/pkg/mod/github.com/go-chi/chi/v5@v5.2.5/mux.go:479 (0x1a64fae)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/pkg/mod/github.com/go-chi/chi/v5@v5.2.5/mux.go:73 (0x1a628c2)
	/go/pkg/mod/github.com/go-chi/chi/v5@v5.2.5/mux.go:321 (0x1a6421a)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/pkg/mod/github.com/go-chi/chi/v5@v5.2.5/chain.go:31 (0x1a61d05)
	/go/pkg/mod/github.com/go-chi/chi/v5@v5.2.5/mux.go:479 (0x1a64fae)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/pkg/mod/github.com/go-chi/chi/v5@v5.2.5/middleware/get_head.go:37 (0x2c33a67)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/src/code.gitea.io/gitea/modules/web/handler.go:145 (0x1a6afb5)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/pkg/mod/github.com/go-chi/chi/v5@v5.2.5/mux.go:73 (0x1a628c2)
	/go/pkg/mod/github.com/go-chi/chi/v5@v5.2.5/mux.go:321 (0x1a6421a)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/src/code.gitea.io/gitea/routers/common/maintenancemode.go:50 (0x2b752da)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/src/code.gitea.io/gitea/modules/web/handler.go:145 (0x1a6afb5)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/pkg/mod/github.com/go-chi/chi/v5@v5.2.5/chain.go:31 (0x1a61d05)
	/go/pkg/mod/github.com/go-chi/chi/v5@v5.2.5/mux.go:479 (0x1a64fae)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/src/code.gitea.io/gitea/modules/web/routing/logger_manager.go:124 (0x127d1ec)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/src/code.gitea.io/gitea/modules/web/handler.go:145 (0x1a6afb5)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/pkg/mod/github.com/chi-middleware/proxy@v1.1.1/middleware.go:37 (0x2b76acf)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/src/code.gitea.io/gitea/modules/web/handler.go:145 (0x1a6afb5)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/src/code.gitea.io/gitea/routers/common/middleware.go:89 (0x2b78cd6)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/src/code.gitea.io/gitea/modules/web/handler.go:145 (0x1a6afb5)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/src/code.gitea.io/gitea/routers/common/middleware.go:104 (0x2b7890f)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/src/code.gitea.io/gitea/modules/web/handler.go:145 (0x1a6afb5)
	/usr/local/go/src/net/http/server.go:2286 (0x94dc88)
	/go/pkg/mod/github.com/go-chi/chi/v5@v5.2.5/mux.go:90 (0x1a62881)
	/go/src/code.gitea.io/gitea/modules/web/router.go:286 (0x1a6d2a2)
	/go/src/code.gitea.io/gitea/modules/web/router.go:221 (0x1a6cbc6)
	/usr/local/go/src/net/http/server.go:3311 (0x96e36d)
	/usr/local/go/src/net/http/server.go:2073 (0x94bd6f)
	/usr/local/go/src/runtime/asm_amd64.s:1771 (0x49af20)
```

Signed-off-by: prettysunflower <me@prettysunflower.moe>
Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: prettysunflower <me@prettysunflower.moe>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-04-21 08:33:46 +00:00
Giteabot
7bd55deab3 Fix actions concurrency groups cross-branch leak (#37311) (#37331)
Backport #37311 by @silverwind

## Problem

Workflow-level concurrency groups were evaluated — and jobs were parsed
— before the run was persisted, so `run.ID` was `0` and `github.run_id`
in the expression context resolved to an empty string. Expressions like:

```yaml
concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true
```

collapsed to `<workflow>-` on every push event (`head_ref` is empty on
push), so `cancel-in-progress` cancelled in-progress runs across
**unrelated branches**, not just the current one.

Reproduced on a 1.26 instance:
- push to `master` → `ci` run starts
- push to `feature-branch` → the `master` run gets cancelled

GitHub Actions' documented semantic: on push events `github.run_id` is
unique per run, so the group is unique → no cancellation; on PR events
`github.head_ref` is the source branch → cancellation is per-PR.

## Fix

Insert the run **before** parsing jobs or evaluating workflow-level
concurrency, so `run.ID` is populated in time for every expression that
reads `github.run_id` — not just the concurrency group, but also
`run-name`, job names, and `runs-on`.

`jobparser.Parse` now runs inside the `InsertRun` transaction, after
`db.Insert(ctx, run)`. Workflow-level concurrency evaluation runs next
and only mutates `run` in memory. All concurrency-derived fields
(`raw_concurrency`, `concurrency_group`, `concurrency_cancel`) plus
`status` and `title` are persisted in a single final `UpdateRun` at
end-of-transaction — one `INSERT` + one `UPDATE` per run in both the
concurrency and non-concurrency paths (matches pre-branch parity, one
fewer `UpdateRepoRunsNumbers` `COUNT` than the interim state).

`GenerateGiteaContext` now sets `run_id` from `run.ID` unconditionally;
every caller passes a persisted run.

**Verification**: tested end-to-end on a 1.26 deployment. Before the
patch, two successive `ci` pushes (one to master, one to a feature
branch) cross-cancelled each other. After the patch, the same pushes —
in both orders (master→branch, branch→master) — run to completion
simultaneously across 15+ runs with zero cancellations.

**Regression tests** in `services/actions/context_test.go`:
- `TestEvaluateRunConcurrency_RunIDFallback` — unit check that
`EvaluateRunConcurrencyFillModel` resolves `github.run_id` from
`run.ID`.
- `TestPrepareRunAndInsert_ExpressionsSeeRunID` — full-flow check: calls
`PrepareRunAndInsert` with `${{ github.run_id }}` in both `run-name` and
the concurrency group, then asserts the persisted `Title`,
`ConcurrencyGroup`, and `RawConcurrency` contain / survive the run's ID.
Re-ordering `db.Insert` relative to either parse or concurrency eval
fails this test.

## Relation to #37119

[#37119](https://github.com/go-gitea/gitea/pull/37119) also moves
concurrency evaluation into `InsertRun` but keeps it **before**
`db.Insert`, then tries to populate `run_id` only when `run.ID > 0` —
which is still `0` at that call site, so the cross-branch leak would
survive that PR as written. This PR fixes the ordering so that `run.ID`
is actually populated at eval time, and broadens it to cover parse-time
expression interpolation too.

---
This PR was written with the help of Claude Opus 4.7

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
2026-04-21 10:04:01 +02:00
Giteabot
e4b7120bc2 Fix bug when accessing user badges (#37321) (#37329)
Backport #37321 by @lunny

Fix #37302

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-04-20 20:11:56 -07:00
Giteabot
f0fd185f14 Fix AppFullLink (#37325) (#37328)
Backport #37325 by @lunny

Fix a bug the checkout command line hint become `git fetch -u
https://gitea.combircni/tea`

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-04-21 02:04:01 +00:00
Giteabot
adfa535dc2 Fix vite manifest update masking build errors (#37279) (#37310)
Backport #37279 by @silverwind

Moves the manifest patching from `closeBundle` to `writeBundle`. Thrown
errors in `writeBundle` work correctly and exit the build.

Signed-off-by: silverwind <me@silverwind.io>
Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-20 09:26:47 +02:00
wxiaoguang
e6691b0e8d Fix Mermaid diagrams failing when node labels contain line breaks (#37296) (#37299)
Backport #37296

Co-authored-by: Nicolas <bircni@icloud.com>
2026-04-19 23:48:33 +08:00
Giteabot
82613a40a0 Fix container auth for public instance (#37290) (#37294)
Backport #37290 by wxiaoguang

Fix #37289

Don't tell container client that the instance needs basic auth if the
public access is available.

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-19 13:23:12 +00:00
Giteabot
ba5117e4e4 Enhance GetActionWorkflow to support fallback references (#37189) (#37283)
Backport #37189 by @bircni

If a workflow is not in default branch the hooks could not be detected

Fixes #37169

Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 21:13:54 +00:00
Lunny Xiao
9b9d1e31aa Changelog for 1.26.0 (#37266)
---------

Signed-off-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-04-18 12:42:39 -07:00
silverwind
eb43da41f5 Upgrade go-git to v5.18.0 (#37269)
Backport of go-git upgrade to v5.18.0 for the v1.26 release branch.

Fixes GHSA-3xc5-wrhm-f963 (credential exposure on HTTP redirects).

---
This PR was written with the help of Claude Opus 4.6

Co-authored-by: Claude (Opus 4.6) <noreply@anthropic.com>
2026-04-18 08:53:47 +00:00
wxiaoguang
1412009d0a Frontend iframe renderer framework: 3D models, OpenAPI (#37233) (#37273)
Backport

* #37233
* #37272

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-04-18 16:02:18 +08:00
Giteabot
26a618ac1a pull: Fix CODEOWNERS absolute path matching. (#37244) (#37264)
Backport #37244 by @JoeGruffins

Patterns starting with "/" (e.g. /docs/.*\.md) never matched because git
returns relative paths without a leading slash. Strip the leading "/"
before compiling the regex since the ^...$ anchoring already provides
root-relative semantics.

closes #28107

Co-authored-by: JoeGruffins <34998433+JoeGruffins@users.noreply.github.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-17 23:02:28 +00:00
wxiaoguang
145898b358 Swift registry metadata: preserve more JSON fields and accept empty metadata (#37254) (#37261)
Backport #37254

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2026-04-17 23:36:48 +02:00
Giteabot
b191cf7e77 Fix user ssh key exporting and tests (#37256) (#37258)
Backport #37256 by wxiaoguang

1. Make sure OmitEmail won't panic
2. SSH principal keys are not for signing or authentication

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-18 03:13:41 +08:00
wxiaoguang
4adee80f58 Fix team member avatar size and add tooltip (#37253)
1. Make team member avatar size=32
2. Add tooltip to the avatar
2026-04-17 16:03:55 +00:00
Giteabot
4de12baf9b Fix commit title rendering in action run and blame (#37243) (#37251)
Backport #37243 by @silverwind

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
2026-04-17 15:25:42 +02:00
wxiaoguang
5d852d2d0a Add test for "fetch redirect", add CSS value validation for external render (#37207) (#37216)
Backport #37207
2026-04-14 18:25:57 +00:00
Giteabot
2aca966c5f Fix incorrect concurrency check (#37205) (#37215)
Backport #37205 by @Zettat123

This bug was identified in
https://github.com/go-gitea/gitea/pull/37119/changes#diff-37655a02d5a44d5c0e3e19c75fb58adb47a8e7835cbd619345d5b556292935a7L180

Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-04-14 17:58:31 +00:00
wxiaoguang
3b253e06a3 Fix corrupted JSON caused by goccy library (#37214) (#37220)
Backport #37214

The only conflict is go.mod

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-04-14 17:24:39 +00:00
Giteabot
df0ad4e8c1 Fix UI regression (#37218) (#37219)
Backport #37218 by wxiaoguang

Fix  #37213

Also fix the misaligned tags, remove unused classes, etc.

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-14 23:59:31 +08:00
Giteabot
68f5e40e46 fix(api): handle missing base branch in PR commits API (#37193) (#37203)
Backport #37193 by @Mohit25022005

fix(api): handle missing base branch in PR commits API

Closes #36366

Previously, the PR commits API returned a 500 Internal Server Error
when the base branch was missing due to an unhandled git "bad revision"
error.

This change:
- Checks for base branch existence before performing git operations
- Returns 404 when the base branch does not exist
- Prevents git errors from surfacing as 500 responses

This improves API robustness and aligns behavior with expected error
handling.

Tested locally by:
- Creating a pull request
- Deleting the base branch
- Verifying that the API returns 404 instead of 500

Co-authored-by: Mohit Swarnkar <mohitswarnkar13@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-04-13 23:42:11 +02:00
Giteabot
d1be5c3612 Fix encoding for Matrix Webhooks (#37190) (#37201)
Backport #37190 by @bircni

`url.PathEscape` unnecessarily encodes ! to %21, causing Matrix
homeservers to reject the request with 401. Replace %21 back to ! after
escaping.

Fixes #36012

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-13 22:26:32 +02:00
Giteabot
8687faaf3a Always show owner/repo name in compare page dropdowns (#37172) (#37200)
Backport #37172 by @xingxing21

Fixes: https://github.com/go-gitea/gitea/issues/36677

The fix is a template-only change in
[templates/repo/diff/compare.tmpl:16-25](vscode-webview://1ca9j6f1e3qtaf59o0cr4ind65ulf8mevvbbbq88int1gg2lncar/templates/repo/diff/compare.tmpl#L16-L25).

Before, the display names were built with conditional logic:

All four variables defaulted to just the owner name (Examples)
$HeadCompareName was only upgraded to owner/repo format in two narrow
cases:
Same org, different repos
A root repo exists with the same owner as the head repo
All other cases (same-repo PRs, cross-org PRs) got owner-only labels
After, all four variables are unconditionally set to owner/repo format
using printf "%s/%s":

```
- {{$BaseCompareName := printf "%s/%s" $.BaseName $.Repository.Name -}}
- {{- $HeadCompareName := printf "%s/%s" $.HeadRepo.OwnerName $.HeadRepo.Name -}}
- {{- $OwnForkCompareName := "" -}}
- {{- if .OwnForkRepo -}}
-	{{- $OwnForkCompareName = printf "%s/%s" .OwnForkRepo.OwnerName .OwnForkRepo.Name -}}
- {{- end -}}
- {{- $RootRepoCompareName := "" -}}
- {{- if .RootRepo -}}
-	{{- $RootRepoCompareName = printf "%s/%s" .RootRepo.OwnerName .RootRepo.Name -}}
- {{- end -}}

+ {{$BaseCompareName := $.Repository.FullName -}}
+ {{$HeadCompareName := $.HeadRepo.FullName -}}
+ {{$OwnForkCompareName := "" -}}
+ {{if $.OwnForkRepo -}}
+	{{$OwnForkCompareName = $.OwnForkRepo.FullName -}}
+ {{end -}}
+ {{$RootRepoCompareName := "" -}}
+ {{if $.RootRepo -}}
+	{{$RootRepoCompareName = $.RootRepo.FullName -}}
+ {{end -}}
```

These variables drive the labels in the base and head branch selector
buttons and their dropdown items. No backend changes were needed —
$.BaseName, $.Repository.Name, $.HeadRepo.OwnerName, and $.HeadRepo.Name
were already available in the template context.

Co-authored-by: Xing Hong <39619359+xingxing21@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-13 18:42:56 +00:00
Giteabot
789a3d3a4d fix(api): handle fork-only commits in compare API (#37185) (#37199)
Backport #37185 by @Mohit25022005

Fix 500 error when comparing branches across fork repositories

## Problem

The compare API returns a 500 Internal Server Error when comparing
branches where the head commit exists only in the fork repository.

## Cause

The API was using the base repository's GitRepo and repository context
when converting commits. This fails when the commit does not exist in
the base repository, resulting in a "fatal: bad object" error.

## Solution

Use the head repository and HeadGitRepo when available to ensure commits
are resolved in the correct repository context.

## Result

* Fixes "fatal: bad object" error
* Enables proper comparison between base and fork repositories
* Prevents 500 Internal Server Error

Fixes #37168

Co-authored-by: Mohit Swarnkar <mohitswarnkar13@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-13 18:01:44 +00:00
Giteabot
b37e098ff0 Indicate form field readonly via background, fix RunUser config (#37175, #37180) (#37184)
Backport #37175 by @silverwind

The `Run As Username` field on the install page was a `readonly` input
that looked editable but wasn't, confusing users. Style `readonly`
inputs with a subtle background, matching other frameworks.

<img width="735" height="131" alt="image"
src="https://github.com/user-attachments/assets/cb76ce71-faab-4300-811e-e4c503b59f9a"
/>

Backport #37180

The comment "so just use current one if config says default" is not
right anymore: "git" isn't the "default" value of RunUser (Comment out
app.example.ini #15807). The RunUser's value is from current session's
username.

Fixes #37174

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.6) <noreply@anthropic.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-12 18:53:09 -07:00
Giteabot
f9b808a8d2 Remove dead CSS rules (#37173) (#37177)
Backport #37173 by @silverwind

Remove CSS rules whose HTML classes/IDs are no longer referenced in any
template, Go source, or JavaScript/TypeScript file:

- `.archived-icon`: removed from templates in c85bb62635
- `.bottom-line`: removed from blame rendering in 9c6aeb47f7
- `.commit-status-link`: removed from templates in f3c4baa84b
- `.instruct-toggle`: removed from templates in 75e85c25c1
- `.runner-new-text`, `#runner-new`: never referenced outside CSS
- `.ap-terminal`: stale, asciinema-player uses `.ap-term`, still not
needed
- `.scrolling.dimmable.dimmed`: dimmer stand-in never adds this class
- `.markup span.align-center/align-right/float-left/float-right`: never
produced by any renderer, sanitizer strips class attributes
- `.markup ul.no-list`, `.markup ol.no-list`: same as above

---
This PR was written with the help of Claude Opus 4.6

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.6) <noreply@anthropic.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-11 11:16:20 +00:00
Giteabot
0112ec9b34 Fix flaky TestCatFileBatch/QueryTerminated test (#37159) (#37178)
Backport #37159 by @silverwind

`TestCatFileBatch/QueryTerminated` relied on timing to distinguish
`os.ErrClosed` vs `io.EOF` error paths. Replace `time.Sleep`-based
synchronization with a channel-based hook on pipe close, making both
error paths fully deterministic regardless of CI runner speed.

Ref:
https://github.com/go-gitea/gitea/actions/runs/24193070536/job/70615366804

---
This PR was written with the help of Claude Opus 4.6

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.6) <noreply@anthropic.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-11 13:14:35 +02:00
Giteabot
7a7376dfc8 Implement logout redirection for reverse proxy auth setups (#36085) (#37171)
Backport #36085 by @eliroca

When authentication is handled externally by a reverse proxy SSO
provider, users can be redirected to an external logout URL or relative
path defined on the reverse proxy.

Co-authored-by: Elisei Roca <eroca@suse.de>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-10 16:49:55 +00:00
Giteabot
fc5e0ec877 Add missing //nolint:depguard (#37162) (#37170)
Backport #37162 by @silverwind

When running `golangci-lint` without `GOEXPERIMENT=jsonv2`, a lint error
`import 'encoding/json' is not allowed` is seen.

All other files in the module that import `encodings/json` have
`//nolint` already, so add it.

---
This PR was written with the help of Claude Opus 4.6

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.6) <noreply@anthropic.com>
2026-04-10 16:18:18 +02:00
silverwind
d0a39bc3a4 Replace rollup-plugin-license with rolldown-license-plugin (#37130) (#37158)
Backport #37130. Only one merge conflict in lockfile.

---
This PR was written with the help of Claude Opus 4.6

Co-authored-by: Claude (Opus 4.6) <noreply@anthropic.com>
2026-04-10 10:47:11 +00:00
Giteabot
4eca71d6d4 Report structurally invalid workflows to users (#37116) (#37164)
Backport #37116 by @bircni

`model.ReadWorkflow` succeeds for YAML that is syntactically valid but
fails deeper parsing in `jobparser.Parse` (e.g. blank lines inside `run:
|` blocks cause a SetJob round-trip error). Add
`ValidateWorkflowContent` which runs the full `jobparser.Parse` to catch
these cases, and use it in the file view, the actions workflow list, and
the workflow detection loop so users see the error instead of silently
getting a 500 or a dropped workflow.

Fixes #37115

Signed-off-by: Nicolas <bircni@icloud.com>
Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-09 20:57:04 +00:00
Giteabot
a2283a0c03 Clean up and improve non-gitea js error filter (#37148) (#37155)
Backport #37148 by @silverwind

1. Filter out errors that contain `chrome-extension://` etc protocols
2. Extract filtering into its own function and test it
3. Fix the `window.config.assetUrlPrefix` mock, guaranteed to end with
`/assets`
4. Remove useless `??` and `?.` for properties that always exist

---
This PR was written with the help of Claude Opus 4.6

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.6) <noreply@anthropic.com>
2026-04-09 14:35:07 +02:00
Giteabot
3e6b9e5312 Bump min go version to 1.26.2 (#37139) (#37143)
Backport #37139 by @silverwind

Update Go from 1.26.1 to 1.26.2 to fix 6 stdlib vulnerabilities:
- GO-2026-4947: `crypto/x509` chain building
- GO-2026-4946: `crypto/x509` policy validation
- GO-2026-4870: `crypto/tls` KeyUpdate DoS
- GO-2026-4869: `archive/tar` unbounded allocation
- GO-2026-4866: `crypto/x509` name constraints bypass
- GO-2026-4865: `html/template` XSS

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.6) <noreply@anthropic.com>
2026-04-08 16:27:32 +00:00
Lunny Xiao
1ad9e996be Changelog for 1.26.0-rc0 (#37134) 2026-04-07 20:33:38 -07:00
317 changed files with 7404 additions and 2697 deletions

View File

@@ -4,6 +4,490 @@ 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
* Add event.schedule context for schedule actions task (#37320) (#37348)
* Fix an issue where changing an organization's visibility caused problems when users had forked its repositories. (#37324) (#37344)
* Use modern "git update-index --cacheinfo" syntax to support more file names (#37338) (#37343)
* Fix URL related escaping for oauth2 (#37334) (#37340)
* When the requested arch rpm is missing fall back to noarch (#37236) (#37339)
* Fix actions concurrency groups cross-branch leak (#37311) (#37331)
* Fix bug when accessing user badges (#37321) (#37329)
* Fix AppFullLink (#37325) (#37328)
* Fix container auth for public instance (#37290) (#37294)
* Enhance GetActionWorkflow to support fallback references (#37189) (#37283)
* Fix vite manifest update masking build errors (#37279) (#37310)
* Fix Mermaid diagrams failing when node labels contain line breaks (#37296) (#37299)
* Use TriggerEvent instead of Event in workflow runs API response for scheduled runs (#37288) #37360
* Add URL to Learn more about blocking a user. (#37355) #37367
* Fix button layout shift when collapsing file tree in editor (#37363) #37375
* Fix org team assignee/reviewer lookups for team member permissions (#37365) #37391
* Fix repo init README EOL (#37388) #37399
* Fix: dump with default zip type produces uncompressed zip (#37401)#37402
## [1.26.0](https://github.com/go-gitea/gitea/releases/tag/v1.26.0) - 2026-04-17
* BREAKING
* Correct swagger annotations for enums, status codes, and notification state (#37030)
* Remove GET API registration-token (#36801)
* Support Actions `concurrency` syntax (#32751)
* Make PUBLIC_URL_DETECTION default to "auto" (#36955)
* SECURITY
* Bound PageSize in `ListUnadoptedRepositories` (#36884)
* FEATURES
* Support Actions `concurrency` syntax (#32751)
* Add terraform state registry (#36710)
* Instance-wide (global) info banner and maintenance mode (#36571)
* Support rendering OpenAPI spec (#36449)
* Add keyboard shortcuts for repository file and code search (#36416)
* Add support for archive-upload rpc (#36391)
* Add ability to download subpath archive (#36371)
* Add workflow dependencies visualization (#26062) (#36248) & Restyle Workflow Graph (#36912)
* Automatic generation of release notes (#35977)
* Add "Go to file", "Delete Directory" to repo file list page (#35911)
* Introduce "config edit-ini" sub command to help maintaining INI config file (#35735)
* Add button to re-run failed jobs in Actions (#36924)
* Support actions and reusable workflows from private repos (#32562)
* Add summary to action runs view (#36883)
* Add user badges (#36752)
* Add configurable permissions for Actions automatic tokens (#36173)
* Add per-runner "Disable/Pause" (#36776)
* Feature non-zipped actions artifacts (action v7 / nodejs / npm v6.2.0) (#36786)
* PERFORMANCE
* WorkflowDispatch API optionally return runid (#36706)
* Add render cache for SVG icons (#36863)
* Load `mentionValues` asynchronously (#36739)
* Lazy-load some Vue components, fix heatmap chunk loading on every page (#36719)
* Load heatmap data asynchronously (#36622)
* Use prev/next pagination for user profile activities page to speed up (#36642)
* Refactor cat-file batch operations and support `--batch-command` approach (#35775)
* Use merge tree to detect conflicts when possible (#36400)
* ENHANCEMENTS
* Implement logout redirection for reverse proxy auth setups (#36085) (#37171)
* Adds option to force update new branch in contents routes (#35592)
* Add viewer controller for mermaid (zoom, drag) (#36557)
* Add code editor setting dropdowns (#36534)
* Add `elk` layout support to mermaid (#36486)
* Add resolve/unresolve review comment API endpoints (#36441)
* Allow configuring default PR base branch (fixes #36412) (#36425)
* Add support for RPM Errata (updateinfo.xml) (#37125)
* Require additional user confirmation for making repo private (#36959)
* Add `actions.WORKFLOW_DIRS` setting (#36619)
* Avoid opening new tab when downloading actions logs (#36740)
* Implements OIDC RP-Initiated Logout (#36724)
* Show workflow link (#37070)
* Desaturate dark theme background colors (#37056)
* Refactor "org teams" page and help new users to "add member" to an org (#37051)
* Add webhook name field to improve webhook identification (#37025) (#37040)
* Make task list checkboxes clickable in the preview tab (#37010)
* Improve severity labels in Actions logs and tweak colors (#36993)
* Linkify URLs in Actions workflow logs (#36986)
* Allow text selection on checkbox labels (#36970)
* Support dark/light theme images in markdown (#36922)
* Enable native dark mode for swagger-ui (#36899)
* Rework checkbox styling, remove `input` border hover effect (#36870)
* Refactor storage content-type handling of ServeDirectURL (#36804)
* Use "Enable Gravatar" but not "Disable" (#36771)
* Use case-insensitive matching for Git error "Not a valid object name" (#36728)
* Add "Copy Source" to markup comment menu (#36726)
* Change image transparency grid to CSS (#36711)
* Add "Run" prefix for unnamed action steps (#36624)
* Persist actions log time display settings in `localStorage` (#36623)
* Use first commit title for multi-commit PRs and fix auto-focus title field (#36606)
* Improve BuildCaseInsensitiveLike with lowercase (#36598)
* Improve diff highlighting (#36583)
* Exclude cancelled runs from failure-only email notifications (#36569)
* Use full-file highlighting for diff sections (#36561)
* Color command/error logs in Actions log (#36538)
* Add paging headers (#36521)
* Improve timeline entries for WIP prefix changes in pull requests (#36518)
* Add FOLDER_ICON_THEME configuration option (#36496)
* Normalize guessed languages for code highlighting (#36450)
* Add chunked transfer encoding support for LFS uploads (#36380)
* Indicate when only optional checks failed (#36367)
* Add 'allow_maintainer_edit' API option for creating a pull request (#36283)
* Support closing keywords with URL references (#36221)
* Improve diff file headers (#36215)
* Fix and enhance comment editor monospace toggle (#36181)
* Add git.DIFF_RENAME_SIMILARITY_THRESHOLD option (#36164)
* Add matching pair insertion to markdown textarea (#36121)
* Add sorting/filtering to admin user search API endpoint (#36112)
* Allow action user have read permission in public repo like other user (#36095)
* Disable matchBrackets in monaco (#36089)
* Use GitHub-style commit message for squash merge (#35987)
* Make composer registry support tar.gz and tar.bz2 and fix bugs (#35958)
* Add GITEA_PR_INDEX env variable to githooks (#35938)
* Add proper error message if session provider can not be created (#35520)
* Add button to copy file name in PR files (#35509)
* Move `X_FRAME_OPTIONS` setting from `cors` to `security` section (#30256)
* Add placeholder content for empty content page (#37114)
* Add `DEFAULT_DELETE_BRANCH_AFTER_MERGE` setting (#36917)
* Redirect to the only OAuth2 provider when no other login methods and fix various problems (#36901)
* Add admin badge to navbar avatar (#36790)
* Add `never` option to `PUBLIC_URL_DETECTION` configuration (#36785)
* Add background and run count to actions list page (#36707)
* Add icon to buttons "Close with Comment", "Close Pull Request", "Close Issue" (#36654)
* Add support for in_progress event in workflow_run webhook (#36979)
* Report commit status for pull_request_review events (#36589)
* Render merged pull request title as such in dashboard feed (#36479)
* Feature to be able to filter project boards by milestones (#36321)
* Use user id in noreply emails (#36550)
* Enable pagination on GiteaDownloader.getIssueReactions() (#36549)
* Remove striped tables in UI (#36509)
* Improve control char rendering and escape button styling (#37094)
* Support legacy run/job index-based URLs and refactor migration 326 (#37008)
* Add date to "No Contributions" tooltip (#36190)
* Show edit page confirmation dialog on tree view file change (#36130)
* Mention proc-receive in text for dashboard.resync_all_hooks func (#35991)
* Reuse selectable style for wiki (#35990)
* Support blue yellow colorblind theme (#35910)
* Support selecting theme on the footer (#35741)
* Improve online runner check (#35722)
* Add quick approve button on PR page (#35678)
* Enable commenting on expanded lines in PR diffs (#35662)
* Print PR-Title into tooltip for actions (#35579)
* Use explicit, stronger defaults for newly generated repo signing keys for Debian (#36236)
* Improve the compare page (#36261)
* Unify repo names in system notices (#36491)
* Move package settings to package instead of being tied to version (#37026)
* Add Actions API rerun endpoints for runs and jobs (#36768)
* Add branch_count to repository API (#35351) (#36743)
* Add created_by filter to SearchIssues (#36670)
* Allow admins to rename non-local users (#35970)
* Support updating branch via API (#35951)
* Add an option to automatically verify SSH keys from LDAP (#35927)
* Make "update file" API can create a new file when SHA is not set (#35738)
* Update issue.go with labels documentation (labels content, not ids) (#35522)
* Expose content_version for optimistic locking on issue and PR edits (#37035)
* Pass ServeHeaderOptions by value instead of pointer, fine tune httplib tests (#36982)
* BUGFIXES
* Frontend iframe renderer framework: 3D models, OpenAPI (#37233) (#37273)
* Fix CODEOWNERS absolute path matching. (#37244) (#37264)
* Swift registry metadata: preserve more JSON fields and accept empty metadata (#37254) (#37261)
* Fix user ssh key exporting and tests (#37256) (#37258)
* Fix team member avatar size and add tooltip (#37253)
* Fix commit title rendering in action run and blame (#37243) (#37251)
* Fix corrupted JSON caused by goccy library (#37214) (#37220)
* Add test for "fetch redirect", add CSS value validation for external render (#37207) (#37216)
* Fix incorrect concurrency check (#37205) (#37215)
* Fix handle missing base branch in PR commits API (#37193) (#37203)
* Fix encoding for Matrix Webhooks (#37190) (#37201)
* Fix handle fork-only commits in compare API (#37185) (#37199)
* Indicate form field readonly via background, fix RunUser config (#37175, #37180) (#37178)
* Report structurally invalid workflows to users (#37116) (#37164)
* Fix API not persisting pull request unit config when has_pull_requests is not set (#36718)
* Rename CSS variables and improve colorblind themes (#36353)
* Hide `add-matcher` and `remove-matcher` from actions job logs (#36520)
* Prevent navigation keys from triggering actions during IME composition (#36540)
* Fix vertical alignment of `.commit-sign-badge` children (#36570)
* Fix duplicate startup warnings in admin panel (#36641)
* Fix CODEOWNERS review request attribution using comment metadata (#36348)
* Fix HTML tags appearing in wiki table of contents (#36284)
* Fix various bugs (#37096)
* Fix various legacy problems (#37092)
* Fix RPM Registry 404 when package name contains 'package' (#37087)
* Merge some standalone Vite entries into index.js (#37085)
* Fix various problems (#37077)
* Fix issue label deletion with Actions tokens (#37013)
* Hide delete branch or tag buttons in mirror or archived repositories. (#37006)
* Fix org contact email not clearable once set (#36975)
* Fix a bug when forking a repository in an organization (#36950)
* Preserve sort order of exclusive labels from template repo (#36931)
* Make container registry support Apple Container (basic auth) (#36920)
* Fix the wrong push commits in the pull request when force push (#36914)
* Add class "list-header-filters" to the div for projects (#36889)
* Fix dbfs error handling (#36844)
* Fix incorrect viewed files counter if reverted change was viewed (#36819)
* Refactor avatar package, support default avatar fallback (#36788)
* Fix README symlink resolution in subdirectories like .github (#36775)
* Fix CSS stacking context issue in actions log (#36749)
* Add gpg signing for merge rebase and update by rebase (#36701)
* Delete non-exist branch should return 404 (#36694)
* Fix `TestActionsCollaborativeOwner` (#36657)
* Fix multi-arch Docker build SIGILL by splitting frontend stage (#36646)
* Fix linguist-detectable attribute being ignored for configuration files (#36640)
* Fix state desync in ComboMarkdownEditor (#36625)
* Unify DEFAULT_SHOW_FULL_NAME output in templates and dropdown (#36597)
* Pull Request Pusher should be the author of the merge (#36581)
* Fix various version parsing problems (#36553)
* Fix highlight diff result (#36539)
* Fix mirror sync parser and fix mirror messages (#36504)
* Fix bug when list pull request commits (#36485)
* Fix various bugs (#36446)
* Fix issue filter menu layout (#36426)
* Restrict branch naming when new change matches with protection rules (#36405)
* Fix link/origin referrer and login redirect (#36279)
* Generate IDs for HTML headings without id attribute (#36233)
* Use a migration test instead of a wrong test which populated the meta test repositories and fix a migration bug (#36160)
* Fix issue close timeline icon (#36138)
* Fix diff blob excerpt expansion (#35922)
* Fix external render (#35727)
* Fix review request webhook bug (#35339) (#35723)
* Fix shutdown waitgroup panic (#35676)
* Cleanup ActionRun creation (#35624)
* Fix possible bug when migrating issues/pull requests (#33487)
* Various fixes (#36697)
* Apply notify/register mail flags during install load (#37120)
* Repair duration display for bad stopped timestamps (#37121)
* Fix(upgrade.sh): use HTTPS for GPG key import and restore SELinux context after upgrade (#36930)
* Fix various trivial problems (#36921)
* Fix various trivial problems (#36953)
* Fix NuGet package upload error handling (#37074)
* Fix CodeQL code scanning alerts (#36858)
* Refactor issue sidebar and fix various problems (#37045)
* Fix various problems (#37029)
* Fix relative-time RangeError (#37021)
* Fix chroma lexer mapping (#36629)
* Fix typos and grammar in English locale (#36751)
* Fix milestone/project text overflow in issue sidebar (#36741)
* Fix `no-content` message not rendering after comment edit (#36733)
* Fix theme loading in development (#36605)
* Fix workflow run jobs API returning null steps (#36603)
* Fix timeline event layout overflow with long content (#36595)
* Fix minor UI issues in runner edit page (#36590)
* Fix incorrect vendored detections (#36508)
* Fix editorconfig not respected in PR Conversation view (#36492)
* Don't create self-references in merged PRs (#36490)
* Fix potential incorrect runID in run status update (#36437)
* Fix file-tree ui error when adding files to repo without commits (#36312)
* Improve image captcha contrast for dark mode (#36265)
* Fix panic in blame view when a file has only a single commit (#36230)
* Fix spelling error in migrate-storage cmd utility (#36226)
* Fix code highlighting on blame page (#36157)
* Fix nilnil in onedev downloader (#36154)
* Fix actions lint (#36029)
* Fix oauth2 session gob register (#36017)
* Fix Arch repo pacman.conf snippet (#35825)
* Fix a number of `strictNullChecks`-related issues (#35795)
* Fix URLJoin, markup render link reoslving, sign-in/up/linkaccount page common data (#36861)
* Hide delete directory button for mirror or archive repository and disable the menu item if user have no permission (#36384)
* Update message severity colors, fix navbar double border (#37019)
* Inline and lazy-load EasyMDE CSS, fix border colors (#36714)
* Closed milestones with no issues now show as 100% completed (#36220)
* Add test for ExtendCommentTreePathLength migration and fix bugs (#35791)
* Only turn links to current instance into hash links (#36237)
* Fix typos in code comments: doesnt, dont, wont (#36890)
* REFACTOR
* Clean up and improve non-gitea js error filter (#37148) (#37155)
* Always show owner/repo name in compare page dropdowns (#37172) (#37200)
* Remove dead CSS rules (#37173) (#37177)
* Replace Monaco with CodeMirror (#36764)
* Replace CSRF cookie with `CrossOriginProtection` (#36183)
* Replace index with id in actions routes (#36842)
* Remove unnecessary function parameter (#35765)
* Move jobparser from act repository to Gitea (#36699)
* Refactor compare router param parse (#36105)
* Optimize 'refreshAccesses' to perform update without removing then adding (#35702)
* Clean up checkbox cursor styles (#37016)
* Remove undocumented support of signing key in the repository git configuration file (#36143)
* Switch `cmd/` to use constructor functions. (#36962)
* Use `relative-time` to render absolute dates (#36238)
* Some refactors about GetMergeBase (#36186)
* Some small refactors (#36163)
* Use gitRepo as parameter instead of repopath when invoking sign functions (#36162)
* Move blame to gitrepo (#36161)
* Move some functions to gitrepo package to reduce RepoPath reference directly (#36126)
* Use gitrepo's clone and push when possible (#36093)
* Remove mermaid margin workaround (#35732)
* Move some functions to gitrepo package (#35543)
* Move GetDiverging functions to gitrepo (#35524)
* Use global lock instead of status pool for cron lock (#35507)
* Use explicit mux instead of DefaultServeMux (#36276)
* Use gitrepo's push function (#36245)
* Pass request context to generateAdditionalHeadersForIssue (#36274)
* Move assign project when creating pull request to the same database transaction (#36244)
* Move catfile batch to a sub package of git module (#36232)
* Use gitrepo.Repository instead of wikipath (#35398)
* Use experimental go json v2 library (#35392)
* Refactor template render (#36438)
* Refactor GetRepoRawDiffForFile to avoid unnecessary pipe or goroutine (#36434)
* Refactor text utility classes to Tailwind CSS (#36703)
* Refactor git command stdio pipe (#36422)
* Refactor git command context & pipeline (#36406)
* Refactor git command stdio pipe (#36393)
* Remove unused functions (#36672)
* Refactor Actions Token Access (#35688)
* Move commit related functions to gitrepo package (#35600)
* Move archive function to repo_model and gitrepo (#35514)
* Move some functions to gitrepo package (#35503)
* Use git model to detect whether branch exist instead of gitrepo method (#35459)
* Some refactor for repo path (#36251)
* Extract helper functions from SearchIssues (#36158)
* Refactor merge conan and container auth preserve actions taskID (#36560)
* Refactor Nuget Auth to reuse Basic Auth Token Validation (#36558)
* Refactor ActionsTaskID (#36503)
* Refactor auth middleware (#36848)
* Refactor code render and render control chars (#37078)
* Clean up AppURL, remove legacy origin-url webcomponent (#37090)
* Remove `util.URLJoin` and replace all callers with direct path concatenation (#36867)
* Replace legacy tw-flex utility classes with flex-text-block/inline (#36778)
* Mark unused&immature activitypub as "not implemented" (#36789)
* TESTING
* Add e2e tests for server push events (#36879)
* Rework e2e tests (#36634)
* Add e2e reaction test, improve accessibility, enable parallel testing (#37081)
* Increase e2e test timeouts on CI to fix flaky tests (#37053)
* BUILD
* Upgrade go-git to v5.18.0 (#37269)
* Replace rollup-plugin-license with rolldown-license-plugin (#37130) (#37158)
* Bump min go version to 1.26.2 (#37139) (#37143)
* Convert locale files from ini to json format (#35489)
* Bump golangci-lint to 2.7.2, enable modernize stringsbuilder (#36180)
* Port away from `flake-utils` (#35675)
* Remove nolint (#36252)
* Update the Unlicense copy to latest version (#36636)
* Update to go 1.26.0 and golangci-lint 2.9.0 (#36588)
* Replace `google/go-licenses` with custom generation (#36575)
* Update go dependencies (#36548)
* Bump appleboy/git-push-action from 1.0.0 to 1.2.0 (#36306)
* Remove fomantic form module (#36222)
* Bump setup-node to v6, re-enable cache (#36207)
* Bump crowdin/github-action from 1 to 2 (#36204)
* Revert "Bump alpine to 3.23 (#36185)" (#36202)
* Update chroma to v2.21.1 (#36201)
* Bump astral-sh/setup-uv from 6 to 7 (#36198)
* Bump docker/build-push-action from 5 to 6 (#36197)
* Bump aws-actions/configure-aws-credentials from 4 to 5 (#36196)
* Bump dev-hanz-ops/install-gh-cli-action from 0.1.0 to 0.2.1 (#36195)
* Add JSON linting (#36192)
* Enable dependabot for actions (#36191)
* Bump alpine to 3.23 (#36185)
* Update chroma to v2.21.0 (#36171)
* Update JS deps and eslint enhancements (#36147)
* Update JS deps (#36091)
* update golangci-lint to v2.7.0 (#36079)
* Update JS deps, fix deprecations (#36040)
* Update JS deps (#35978)
* Add toolchain directive to go.mod (#35901)
* Move `gitea-vet` to use `go tool` (#35878)
* Update to go 1.25.4 (#35877)
* Enable TypeScript `strictNullChecks` (#35843)
* Enable `vue/require-typed-ref` eslint rule (#35764)
* Update JS dependencies (#35759)
* Move `codeformat` folder to tools (#35758)
* Update dependencies (#35733)
* Bump happy-dom from 20.0.0 to 20.0.2 (#35677)
* Bump setup-go to v6 (#35660)
* Update JS deps, misc tweaks (#35643)
* Bump happy-dom from 19.0.2 to 20.0.0 (#35625)
* Use bundled version of spectral (#35573)
* Update JS and PY deps (#35565)
* Bump github.com/wneessen/go-mail from 0.6.2 to 0.7.1 (#35557)
* Migrate from webpack to vite (#37002)
* Update JS dependencies and misc tweaks (#37064)
* Update to eslint 10 (#36925)
* Optimize Docker build with dependency layer caching (#36864)
* Update JS deps (#36850)
* Update tool dependencies and fix new lint issues (#36702)
* Remove redundant linter rules (#36658)
* Move Fomantic dropdown CSS to custom module (#36530)
* Remove and forbid `@ts-expect-error` (#36513)
* Refactor git command stderr handling (#36402)
* Enable gocheckcompilerdirectives linter (#36156)
* Replace `lint-go-gopls` with additional `govet` linters (#36028)
* Update golangci-lint to v2.6.0 (#35801)
* Misc tool tweaks (#35734)
* Add cache to container build (#35697)
* Upgrade vite (#37126)
* Update `setup-uv` to v8.0.0 (#37101)
* Upgrade `go-git` to v5.17.2 and related dependencies (#37060)
* Raise minimum Node.js version to 22.18.0 (#37058)
* Upgrade `golang.org/x/image` to v0.38.0 (#37054)
* Update minimum go version to 1.26.1, golangci-lint to 2.11.2, fix test style (#36876)
* Enable eslint concurrency (#36878)
* Vendor relative-time-element as local web component (#36853)
* Update material-icon-theme v5.32.0 (#36832)
* Update Go dependencies (#36781)
* Upgrade minimatch (#36760)
* Remove i18n backport tool at the moment because of translation format changed (#36643)
* Update emoji data for Unicode 16 (#36596)
* Update JS dependencies, adjust webpack config, misc fixes (#36431)
* Update material-icon-theme to v5.31.0 (#36427)
* Update JS and PY deps (#36383)
* Bump alpine to 3.23, add platforms to `docker-dryrun` (#36379)
* Update JS deps (#36354)
* Update goldmark to v1.7.16 (#36343)
* Update chroma to v2.22.0 (#36342)
* DOCS
* Update AI Contribution Policy (#37022)
* Update AGENTS.md with additional guidelines (#37018)
* Add missing cron tasks to example ini (#37012)
* Add AI Contribution Policy to CONTRIBUTING.md (#36651)
* Minor punctuation improvement in CONTRIBUTING.md (#36291)
* Add documentation for markdown anchor post-processing (#36443)
* MISC
* Correct spelling (#36783)
* Update Nix flake (#37110)
* Update Nix flake (#37024)
* Add valid github scopes (#36977)
* Update Nix flake (#36943)
* Update Nix flake (#36902)
* Update Nix flake (#36857)
* Update Nix flake (#36787)
## [1.25.5](https://github.com/go-gitea/gitea/releases/tag/v1.25.5) - 2026-03-10
* SECURITY

View File

@@ -19,6 +19,7 @@ import (
// regexp is based on go-license, excluding README and NOTICE
// https://github.com/google/go-licenses/blob/master/licenses/find.go
// also defined in vite.config.ts
var licenseRe = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING).*$`)
// primaryLicenseRe matches exact primary license filenames without suffixes.

View File

@@ -41,10 +41,10 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; App name that shows in every page title
APP_NAME = ; Gitea: Git with a cup of tea
;APP_NAME = Gitea: Git with a cup of tea
;;
;; RUN_USER will automatically detect the current user - but you can set it here change it if you run locally
RUN_USER = ; git
;RUN_USER =
;;
;; Application run mode, affects performance and debugging: "dev" or "prod", default is "prod"
;; Mode "dev" makes Gitea easier to develop and debug, values other than "dev" are treated as "prod" which is for production use.
@@ -461,6 +461,11 @@ INTERNAL_TOKEN =
;; Name of cookie used to store authentication information.
;COOKIE_REMEMBER_NAME = gitea_incredible
;;
;; URL or path that Gitea should redirect users to *after* performing its own logout.
;; Use this, if needed, when authentication is handled by a reverse proxy or SSO.
;; For example: "/my-sso/logout?return=/my-sso/home"
;REVERSE_PROXY_LOGOUT_REDIRECT =
;;
;; Reverse proxy authentication header name of user name, email, and full name
;REVERSE_PROXY_AUTHENTICATION_USER = X-WEBAUTH-USER
;REVERSE_PROXY_AUTHENTICATION_EMAIL = X-WEBAUTH-EMAIL
@@ -1156,6 +1161,11 @@ LEVEL = Info
;; Retarget child pull requests to the parent pull request branch target on merge of parent pull request. It only works on merged PRs where the head and base branch target the same repo.
;RETARGET_CHILDREN_ON_MERGE = true
;;
;; 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 = 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.
;DELAY_CHECK_FOR_INACTIVE_DAYS = 7

View File

@@ -926,6 +926,7 @@ export default defineConfig([
{
...playwright.configs['flat/recommended'],
files: ['tests/e2e/**/*.test.ts'],
languageOptions: {globals: {...globals.nodeBuiltin, ...globals.browser}},
rules: {
...playwright.configs['flat/recommended'].rules,
'playwright/expect-expect': [0],

25
go.mod
View File

@@ -1,6 +1,6 @@
module code.gitea.io/gitea
go 1.26.1
go 1.26.3
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
// But some CAs use negative serial number, just relax the check. related:
@@ -12,7 +12,7 @@ require (
code.gitea.io/sdk/gitea v0.24.1
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
connectrpc.com/connect v1.19.1
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed
gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a
gitea.com/go-chi/cache v0.2.1
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e
@@ -51,13 +51,12 @@ require (
github.com/go-chi/cors v1.2.2
github.com/go-co-op/gocron/v2 v2.19.1
github.com/go-enry/go-enry/v2 v2.9.5
github.com/go-git/go-billy/v5 v5.8.0
github.com/go-git/go-git/v5 v5.17.2
github.com/go-git/go-billy/v5 v5.9.0
github.com/go-git/go-git/v5 v5.19.0
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.9.3
github.com/go-webauthn/webauthn v0.16.1
github.com/goccy/go-json v0.10.6
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
github.com/golang-jwt/jwt/v5 v5.3.1
@@ -111,13 +110,13 @@ require (
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
gitlab.com/gitlab-org/api/client-go v1.46.0
go.yaml.in/yaml/v4 v4.0.0-rc.3
golang.org/x/crypto v0.49.0
golang.org/x/crypto v0.50.0
golang.org/x/image v0.38.0
golang.org/x/net v0.52.0
golang.org/x/net v0.53.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.20.0
golang.org/x/sys v0.42.0
golang.org/x/text v0.35.0
golang.org/x/sys v0.44.0
golang.org/x/text v0.36.0
google.golang.org/grpc v1.79.3
google.golang.org/protobuf v1.36.11
gopkg.in/ini.v1 v1.67.1
@@ -196,6 +195,7 @@ require (
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/go-webauthn/x v0.2.2 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
@@ -244,7 +244,7 @@ require (
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.26 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
@@ -276,10 +276,9 @@ require (
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.43.0 // indirect
golang.org/x/tools v0.44.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401020348-3a24fdc17823 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

48
go.sum
View File

@@ -18,8 +18,8 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
gitea.com/gitea/act v0.261.10 h1:ndwbtuMXXz1dpYF2iwY1/PkgKNETo4jmPXfinTZt8cs=
gitea.com/gitea/act v0.261.10/go.mod h1:oIkqQHvU0lfuIWwcpqa4FmU+t3prA89tgkuHUTsrI2c=
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso=
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw=
gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a h1:JHoBrfuTSF9Ke9aNfSYj1XRPBHjKPgCApVprnt2Am0M=
gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a/go.mod h1:FOsLJIMdpiHzBp3Vby6Wfkdw2ppGscrjgU1IC7E4/zQ=
gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g=
gitea.com/go-chi/cache v0.2.1/go.mod h1:Qic0HZ8hOHW62ETGbonpwz8WYypj9NieU9659wFUJ8Q=
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo=
@@ -300,12 +300,12 @@ github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e h1:oRq/fiirun5Hql
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=
github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
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.17.2 h1:B+nkdlxdYrvyFK4GPXVU8w1U+YkbsgciIR7f2sZJ104=
github.com/go-git/go-git/v5 v5.17.2/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
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-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.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
@@ -598,8 +598,8 @@ github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -785,10 +785,10 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
@@ -800,8 +800,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -819,8 +819,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -868,8 +868,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -880,8 +880,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -892,8 +892,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -906,8 +906,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -7,7 +7,6 @@ import (
"context"
"errors"
"fmt"
"slices"
"strings"
"time"
@@ -115,7 +114,7 @@ func (run *ActionRun) RefTooltip() string {
}
// LoadAttributes load Repo TriggerUser if not loaded
func (run *ActionRun) LoadAttributes(ctx context.Context) error {
func (run *ActionRun) LoadAttributes(ctx context.Context) (err error) {
if run == nil {
return nil
}
@@ -129,11 +128,10 @@ func (run *ActionRun) LoadAttributes(ctx context.Context) error {
}
if run.TriggerUser == nil {
u, err := user_model.GetPossibleUserByID(ctx, run.TriggerUserID)
run.TriggerUserID, run.TriggerUser, err = user_model.GetPossibleUserByID(ctx, run.TriggerUserID)
if err != nil {
return err
}
run.TriggerUser = u
}
return nil
@@ -198,30 +196,34 @@ func (run *ActionRun) IsSchedule() bool {
}
// UpdateRepoRunsNumbers updates the number of runs and closed runs of a repository.
func UpdateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error {
_, err := db.GetEngine(ctx).ID(repo.ID).
NoAutoTime().
Cols("num_action_runs", "num_closed_action_runs").
SetExpr("num_action_runs",
builder.Select("count(*)").From("action_run").
Where(builder.Eq{"repo_id": repo.ID}),
).
SetExpr("num_closed_action_runs",
builder.Select("count(*)").From("action_run").
Where(builder.Eq{
"repo_id": repo.ID,
}.And(
builder.In("status",
StatusSuccess,
StatusFailure,
StatusCancelled,
StatusSkipped,
),
),
),
).
Update(repo)
return err
// Callers MUST invoke this from outside any transaction that has X-locked action_run rows for the same repo, otherwise, transaction deadlock
func UpdateRepoRunsNumbers(ctx context.Context, repoID int64) {
if db.InTransaction(ctx) {
setting.PanicInDevOrTesting("UpdateRepoRunsNumbers must not be called inside a transaction")
}
e := db.GetEngine(ctx)
numActionRuns, err := e.Where("repo_id = ?", repoID).Count(new(ActionRun))
if err != nil {
log.Error("UpdateRepoRunsNumbers count num_action_runs for repo %d: %v", repoID, err)
return
}
numClosedActionRuns, err := e.Where("repo_id = ?", repoID).
In("status", StatusSuccess, StatusFailure, StatusCancelled, StatusSkipped).
Count(new(ActionRun))
if err != nil {
log.Error("UpdateRepoRunsNumbers count num_closed_action_runs for repo %d: %v", repoID, err)
return
}
if _, err := e.ID(repoID).Cols("num_action_runs", "num_closed_action_runs").NoAutoTime().Update(&repo_model.Repository{
NumActionRuns: int(numActionRuns),
NumClosedActionRuns: int(numClosedActionRuns),
}); err != nil {
log.Error("UpdateRepoRunsNumbers update repo %d: %v", repoID, err)
}
}
// CancelPreviousJobs cancels all previous jobs of the same repository, reference, workflow, and event.
@@ -389,18 +391,6 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
// It's impossible that the run is not found, since Gitea never deletes runs.
}
if run.Status != 0 || slices.Contains(cols, "status") {
if run.RepoID == 0 {
setting.PanicInDevOrTesting("RepoID should not be 0")
}
if err = run.LoadRepo(ctx); err != nil {
return err
}
if err := UpdateRepoRunsNumbers(ctx, run.Repo); err != nil {
return err
}
}
return nil
}

View File

@@ -29,8 +29,7 @@ func TestUpdateRepoRunsNumbers(t *testing.T) {
assert.Equal(t, 2, repo.NumClosedActionRuns)
// now update will correct them, only num_actionr_runs and num_closed_action_runs should be updated
err = UpdateRepoRunsNumbers(t.Context(), repo)
assert.NoError(t, err)
UpdateRepoRunsNumbers(t.Context(), repo.ID)
repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
assert.Equal(t, 5, repo.NumActionRuns)
assert.Equal(t, 3, repo.NumClosedActionRuns)

View File

@@ -186,15 +186,7 @@ func (a *Action) LoadActUser(ctx context.Context) {
if a.ActUser != nil {
return
}
var err error
a.ActUser, err = user_model.GetPossibleUserByID(ctx, a.ActUserID)
if err == nil {
return
} else if user_model.IsErrUserNotExist(err) {
a.ActUser = user_model.NewGhostUser()
} else {
log.Error("GetUserByID(%d): %v", a.ActUserID, err)
}
a.ActUserID, a.ActUser, _ = user_model.GetPossibleUserByID(ctx, a.ActUserID)
}
func (a *Action) LoadRepo(ctx context.Context) error {
@@ -444,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 ||

View File

@@ -137,6 +137,11 @@ func (task *Task) MigrateConfig() (*migration.MigrateOptions, error) {
log.Error("Unable to decrypt AuthToken, maybe SECRET_KEY is wrong: %v", err)
}
}
if opts.AWSSecretAccessKeyEncrypted != "" {
if opts.AWSSecretAccessKey, err = secret.DecryptSecret(setting.SecretKey, opts.AWSSecretAccessKeyEncrypted); err != nil {
log.Error("Unable to decrypt AWSSecretAccessKey, maybe SECRET_KEY is wrong: %v", err)
}
}
return &opts, nil
}
@@ -201,6 +206,8 @@ func FinishMigrateTask(ctx context.Context, task *Task) error {
conf.AuthPasswordEncrypted = ""
conf.AuthTokenEncrypted = ""
conf.CloneAddrEncrypted = ""
conf.AWSSecretAccessKey = ""
conf.AWSSecretAccessKeyEncrypted = ""
confBytes, err := json.Marshal(conf)
if err != nil {
return err

View File

@@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/models/perm"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
@@ -64,7 +65,12 @@ func (key *PublicKey) AfterLoad() {
// OmitEmail returns content of public key without email address.
func (key *PublicKey) OmitEmail() string {
return strings.Join(strings.Split(key.Content, " ")[:2], " ")
fields := strings.Split(key.Content, " ") // format: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... comment
if len(fields) < 2 {
setting.PanicInDevOrTesting("invalid public key %d content: %s", key.ID, key.Content)
return "" // not a valid public key, it shouldn't really happen, the value is managed internally
}
return strings.Join(fields[:2], " ")
}
func addKey(ctx context.Context, key *PublicKey) (err error) {

View File

@@ -40,7 +40,7 @@ func CheckPrincipalKeyString(ctx context.Context, user *user_model.User, content
if !email.IsActivated {
continue
}
if content == email.Email {
if strings.EqualFold(content, email.LowerEmail) {
return content, nil
}
}

View File

@@ -5,9 +5,8 @@ package auth
import (
"context"
"crypto/sha256"
"crypto/subtle"
"encoding/base32"
"encoding/base64"
"errors"
"fmt"
"net"
@@ -24,6 +23,7 @@ import (
uuid "github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"golang.org/x/oauth2"
"xorm.io/builder"
"xorm.io/xorm"
)
@@ -31,7 +31,10 @@ import (
// Authorization codes should expire within 10 minutes per https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2
const oauth2AuthorizationCodeValidity = 10 * time.Minute
var ErrOAuth2AuthorizationCodeInvalidated = errors.New("oauth2 authorization code already invalidated")
var (
ErrOAuth2AuthorizationCodeInvalidated = errors.New("oauth2 authorization code already invalidated")
ErrOAuth2GrantStaleCounter = errors.New("oauth2 grant state changed during token refresh")
)
// OAuth2Application represents an OAuth2 client (RFC 6749)
type OAuth2Application struct {
@@ -151,30 +154,40 @@ func (app *OAuth2Application) ContainsRedirectURI(redirectURI string) bool {
// https://www.rfc-editor.org/rfc/rfc6819#section-5.2.3.3
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-12#section-3.1
contains := func(s string) bool {
s = strings.TrimSuffix(strings.ToLower(s), "/")
for _, u := range app.RedirectURIs {
if strings.TrimSuffix(strings.ToLower(u), "/") == s {
redirectCandidates := []string{redirectURI}
if !app.ConfidentialClient {
loopbackRedirect, ok := normalizePublicClientRedirectURI(redirectURI)
if ok {
redirectCandidates = append(redirectCandidates, loopbackRedirect)
}
}
for _, candidate := range redirectCandidates {
normalizedCandidate := normalizeRedirectURIForComparison(candidate)
for _, registeredURI := range app.RedirectURIs {
if normalizeRedirectURIForComparison(registeredURI) == normalizedCandidate {
return true
}
}
return false
}
if !app.ConfidentialClient {
uri, err := url.Parse(redirectURI)
// ignore port for http loopback uris following https://datatracker.ietf.org/doc/html/rfc8252#section-7.3
if err == nil && uri.Scheme == "http" && uri.Port() != "" {
ip := net.ParseIP(uri.Hostname())
if ip != nil && ip.IsLoopback() {
// strip port
uri.Host = uri.Hostname()
if contains(uri.String()) {
return true
}
}
}
return false
}
func normalizeRedirectURIForComparison(redirectURI string) string {
return strings.TrimSuffix(util.ToLowerASCII(redirectURI), "/")
}
func normalizePublicClientRedirectURI(redirectURI string) (string, bool) {
parsedURI, err := url.Parse(redirectURI)
if err != nil || parsedURI.Scheme != "http" || parsedURI.Port() == "" {
return "", false
}
return contains(redirectURI)
if ip := net.ParseIP(parsedURI.Hostname()); ip == nil || !ip.IsLoopback() {
return "", false
}
parsedURI.Host = parsedURI.Hostname()
return parsedURI.String(), true
}
// Base32 characters, but lowercased.
@@ -427,22 +440,34 @@ func (code *OAuth2AuthorizationCode) Invalidate(ctx context.Context) error {
return nil
}
func (code *OAuth2AuthorizationCode) requiresCodeVerifier() bool {
return code.CodeChallengeMethod != "" || code.CodeChallenge != ""
}
func deriveCodeChallenge(method, verifier string) (string, bool) {
switch method {
case "S256":
return oauth2.S256ChallengeFromVerifier(verifier), true
case "plain":
return verifier, true
default:
return "", false
}
}
// ValidateCodeChallenge validates the given verifier against the saved code challenge. This is part of the PKCE implementation.
func (code *OAuth2AuthorizationCode) ValidateCodeChallenge(verifier string) bool {
switch code.CodeChallengeMethod {
case "S256":
// base64url(SHA256(verifier)) see https://tools.ietf.org/html/rfc7636#section-4.6
h := sha256.Sum256([]byte(verifier))
hashedVerifier := base64.RawURLEncoding.EncodeToString(h[:])
return hashedVerifier == code.CodeChallenge
case "plain":
return verifier == code.CodeChallenge
case "":
if !code.requiresCodeVerifier() {
return true
default:
// unsupported method -> return false
}
if verifier == "" || code.CodeChallengeMethod == "" {
return false
}
expectedChallenge, ok := deriveCodeChallenge(code.CodeChallengeMethod, verifier)
if !ok {
return false
}
return subtle.ConstantTimeCompare([]byte(expectedChallenge), []byte(code.CodeChallenge)) == 1
}
// GetOAuth2AuthorizationByCode returns an authorization by its code
@@ -510,15 +535,18 @@ func (grant *OAuth2Grant) GenerateNewAuthorizationCode(ctx context.Context, redi
// IncreaseCounter increases the counter and updates the grant
func (grant *OAuth2Grant) IncreaseCounter(ctx context.Context) error {
_, err := db.GetEngine(ctx).ID(grant.ID).Incr("counter").Update(new(OAuth2Grant))
affected, err := db.GetEngine(ctx).
Where("id = ?", grant.ID).
And("counter = ?", grant.Counter).
Incr("counter").
Update(new(OAuth2Grant))
if err != nil {
return err
}
updatedGrant, err := GetOAuth2GrantByID(ctx, grant.ID)
if err != nil {
return err
if affected == 0 {
return ErrOAuth2GrantStaleCounter
}
grant.Counter = updatedGrant.Counter
grant.Counter++
return nil
}
@@ -633,7 +661,7 @@ func GetActiveOAuth2SourceByAuthName(ctx context.Context, name string) (*Source,
}
if !has {
return nil, fmt.Errorf("oauth2 source not found, name: %q", name)
return nil, util.NewNotExistErrorf("oauth2 source not found, name: %q", name)
}
return authSource, nil

View File

@@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/modules/timeutil"
"github.com/stretchr/testify/assert"
"golang.org/x/oauth2"
)
func TestOAuth2AuthorizationCodeValidity(t *testing.T) {
@@ -104,6 +105,47 @@ func TestOAuth2Application_ContainsRedirect_Slash(t *testing.T) {
assert.False(t, app.ContainsRedirectURI("http://127.0.0.1/other"))
}
func TestOAuth2Application_ContainsRedirectURI_ASCIIOnlyNormalization(t *testing.T) {
testCases := []struct {
name string
registered []string
redirectURI string
allowed bool
}{
{
name: "exact-match",
registered: []string{"https://signin.example.test/callback"},
redirectURI: "https://signin.example.test/callback",
allowed: true,
},
{
name: "ascii-case-insensitive",
registered: []string{"https://signin.example.test/callback"},
redirectURI: "https://signIN.example.test/callback",
allowed: true,
},
{
name: "non-ascii-not-folded",
registered: []string{"https://signin.example.test/callback"},
redirectURI: "https://signİn.example.test/callback",
allowed: false,
},
{
name: "loopback-strips-port",
registered: []string{"http://127.0.0.1/callback"},
redirectURI: "http://127.0.0.1:12345/callback",
allowed: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
app := &auth_model.OAuth2Application{RedirectURIs: tc.registered}
assert.Equal(t, tc.allowed, app.ContainsRedirectURI(tc.redirectURI))
})
}
}
func TestOAuth2Application_ValidateClientSecret(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1})
@@ -181,6 +223,16 @@ func TestOAuth2Grant_IncreaseCounter(t *testing.T) {
unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Counter: 2})
}
func TestOAuth2Grant_IncreaseCounterRejectsStaleCounter(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Counter: 1})
stale := *grant
assert.NoError(t, grant.IncreaseCounter(t.Context()))
err := stale.IncreaseCounter(t.Context())
assert.ErrorIs(t, err, auth_model.ErrOAuth2GrantStaleCounter)
}
func TestOAuth2Grant_ScopeContains(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Scope: "openid profile"})
@@ -238,35 +290,38 @@ func TestGetOAuth2AuthorizationByCode(t *testing.T) {
}
func TestOAuth2AuthorizationCode_ValidateCodeChallenge(t *testing.T) {
// test plain
code := &auth_model.OAuth2AuthorizationCode{
CodeChallengeMethod: "plain",
CodeChallenge: "test123",
}
assert.True(t, code.ValidateCodeChallenge("test123"))
assert.False(t, code.ValidateCodeChallenge("ierwgjoergjio"))
s256Verifier := "s256-verifier"
s256Challenge := oauth2.S256ChallengeFromVerifier(s256Verifier)
missingVerifierChallenge := oauth2.S256ChallengeFromVerifier("verifier-not-supplied")
// test S256
code = &auth_model.OAuth2AuthorizationCode{
CodeChallengeMethod: "S256",
CodeChallenge: "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg",
testCases := []struct {
name string
method string
challenge string
verifier string
valid bool
}{
{"plain-success", "plain", "plain-secret", "plain-secret", true},
{"plain-failure", "plain", "plain-secret", "ierwgjoergjio", false},
{"s256-success", "S256", s256Challenge, s256Verifier, true},
{"s256-failure", "S256", s256Challenge, "wiogjerogorewngoenrgoiuenorg", false},
{"unsupported-method", "monkey", "foiwgjioriogeiogjerger", "foiwgjioriogeiogjerger", false},
{"no-pkce-configured", "", "", "", true},
{"s256-missing-verifier", "S256", missingVerifierChallenge, "", false},
{"plain-missing-verifier", "plain", "plain-secret", "", false},
{"missing-method-with-challenge", "", "foierjiogerogerg", "", false},
{"missing-method-rejects-even-matching-verifier", "", "foierjiogerogerg", "foierjiogerogerg", false},
}
assert.True(t, code.ValidateCodeChallenge("N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt"))
assert.False(t, code.ValidateCodeChallenge("wiogjerogorewngoenrgoiuenorg"))
// test unknown
code = &auth_model.OAuth2AuthorizationCode{
CodeChallengeMethod: "monkey",
CodeChallenge: "foiwgjioriogeiogjerger",
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
code := &auth_model.OAuth2AuthorizationCode{
CodeChallengeMethod: tc.method,
CodeChallenge: tc.challenge,
}
assert.Equal(t, tc.valid, code.ValidateCodeChallenge(tc.verifier))
})
}
assert.False(t, code.ValidateCodeChallenge("foiwgjioriogeiogjerger"))
// test no code challenge
code = &auth_model.OAuth2AuthorizationCode{
CodeChallengeMethod: "",
CodeChallenge: "foierjiogerogerg",
}
assert.True(t, code.ValidateCodeChallenge(""))
}
func TestOAuth2AuthorizationCode_GenerateRedirectURI(t *testing.T) {

View File

@@ -89,7 +89,7 @@
ref: "refs/heads/test"
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
event: "push"
trigger_event: "push"
trigger_event: "schedule"
is_fork_pull_request: 0
status: 1
started: 1683636528

View File

@@ -399,16 +399,7 @@ func (c *Comment) LoadPoster(ctx context.Context) (err error) {
if c.Poster != nil {
return nil
}
c.Poster, err = user_model.GetPossibleUserByID(ctx, c.PosterID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
c.PosterID = user_model.GhostUserID
c.Poster = user_model.NewGhostUser()
} else {
log.Error("getUserByID[%d]: %v", c.ID, err)
}
}
c.PosterID, c.Poster, err = user_model.GetPossibleUserByID(ctx, c.PosterID)
return err
}

View File

@@ -190,17 +190,10 @@ func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool {
// LoadPoster loads poster
func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
if issue.Poster == nil && issue.PosterID != 0 {
issue.Poster, err = user_model.GetPossibleUserByID(ctx, issue.PosterID)
if err != nil {
issue.PosterID = user_model.GhostUserID
issue.Poster = user_model.NewGhostUser()
if !user_model.IsErrUserNotExist(err) {
return fmt.Errorf("getUserByID.(poster) [%d]: %w", issue.PosterID, err)
}
return nil
}
if issue.Poster != nil {
return nil
}
issue.PosterID, issue.Poster, err = user_model.GetPossibleUserByID(ctx, issue.PosterID)
return err
}

View File

@@ -877,7 +877,12 @@ func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule,
warnings := make([]string, 0)
expr := fmt.Sprintf("^%s$", strings.TrimPrefix(tokens[0], "!"))
// Strip leading "!" for negative rules, then strip leading "/" since
// git returns relative paths (e.g. "docs/foo.md" not "/docs/foo.md")
// and the regex is already anchored with ^...$, so the "/" is redundant.
pattern := strings.TrimPrefix(tokens[0], "!")
pattern = strings.TrimPrefix(pattern, "/")
expr := fmt.Sprintf("^%s$", pattern)
rule.Rule, err = regexp2.Compile(expr, regexp2.None)
if err != nil {
warnings = append(warnings, fmt.Sprintf("incorrect codeowner regexp: %s", err))

View File

@@ -71,38 +71,69 @@ func GetUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch
}
// CanMaintainerWriteToBranch check whether user is a maintainer and could write to the branch
func CanMaintainerWriteToBranch(ctx context.Context, p access_model.Permission, branch string, user *user_model.User) bool {
if p.CanWrite(unit.TypeCode) {
return true
func CanMaintainerWriteToBranch(ctx context.Context, headPerm access_model.Permission, headBranch string, doer *user_model.User) bool {
can, err := canMaintainerWriteToBranch(ctx, headPerm, headBranch, doer)
if err != nil {
log.Error("CanMaintainerWriteToBranch: %v", err)
return false
}
return can
}
func canMaintainerWriteToBranch(ctx context.Context, headPerm access_model.Permission, headBranch string, doer *user_model.User) (bool, error) {
if headPerm.CanWrite(unit.TypeCode) {
return true, nil
}
// the code below depends on units to get the repository ID, not ideal but just keep it for now
firstUnitRepoID := p.GetFirstUnitRepoID()
firstUnitRepoID := headPerm.GetFirstUnitRepoID()
if firstUnitRepoID == 0 {
return false
return false, nil
}
prs, err := GetUnmergedPullRequestsByHeadInfo(ctx, firstUnitRepoID, branch)
prs, err := GetUnmergedPullRequestsByHeadInfo(ctx, firstUnitRepoID, headBranch)
if err != nil {
return false
return false, err
}
if _, err := prs.LoadIssues(ctx); err != nil {
return false, err
}
for _, pr := range prs {
if pr.AllowMaintainerEdit {
err = pr.LoadBaseRepo(ctx)
if err != nil {
continue
}
prPerm, err := access_model.GetIndividualUserRepoPermission(ctx, pr.BaseRepo, user)
if err != nil {
continue
}
if prPerm.CanWrite(unit.TypeCode) {
return true
}
if !pr.AllowMaintainerEdit {
continue
}
// check the PR's poster's permissions
// If a "reader" poster created the PR in base repo from head repo, even if it is allowed to be edited by maintainers,
// the maintainers should not be allowed to write, because they don't really have "write" permission in the head repo
if err := pr.Issue.LoadPoster(ctx); err != nil {
return false, err
}
if err := pr.LoadHeadRepo(ctx); err != nil {
return false, err
}
posterHeadPerm, err := access_model.GetIndividualUserRepoPermission(ctx, pr.HeadRepo, pr.Issue.Poster)
if err != nil {
return false, err
}
if !posterHeadPerm.CanWrite(unit.TypeCode) {
continue
}
// check the doer's permission
// Only allow the doer to edit the PR if they have write access to the base repository
if err := pr.LoadBaseRepo(ctx); err != nil {
return false, err
}
doerBasePerm, err := access_model.GetIndividualUserRepoPermission(ctx, pr.BaseRepo, doer)
if err != nil {
return false, err
}
if doerBasePerm.CanWrite(unit.TypeCode) {
return true, nil
}
}
return false
return false, nil
}
// HasUnmergedPullRequestsByHeadInfo checks if there are open and not merged pull request

View File

@@ -6,15 +6,28 @@ package issues_test
import (
"testing"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"xorm.io/builder"
)
func TestPullRequestList_LoadAttributes(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func TestPullRequestList(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
t.Run("LoadAttributes", testPullRequestListLoadAttributes)
t.Run("LoadReviewCommentsCounts", testPullRequestListLoadReviewCommentsCounts)
t.Run("LoadReviews", testPullRequestListLoadReviews)
t.Run("CanMaintainerWriteToBranch", testCanMaintainerWriteToBranch)
}
func testPullRequestListLoadAttributes(t *testing.T) {
prs := issues_model.PullRequestList{
unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}),
unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}),
@@ -28,9 +41,7 @@ func TestPullRequestList_LoadAttributes(t *testing.T) {
assert.NoError(t, issues_model.PullRequestList([]*issues_model.PullRequest{}).LoadAttributes(t.Context()))
}
func TestPullRequestList_LoadReviewCommentsCounts(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func testPullRequestListLoadReviewCommentsCounts(t *testing.T) {
prs := issues_model.PullRequestList{
unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}),
unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}),
@@ -43,9 +54,7 @@ func TestPullRequestList_LoadReviewCommentsCounts(t *testing.T) {
}
}
func TestPullRequestList_LoadReviews(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func testPullRequestListLoadReviews(t *testing.T) {
prs := issues_model.PullRequestList{
unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}),
unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}),
@@ -61,3 +70,73 @@ func TestPullRequestList_LoadReviews(t *testing.T) {
assert.EqualValues(t, 10, reviewList[4].ID)
assert.EqualValues(t, 22, reviewList[5].ID)
}
func testCanMaintainerWriteToBranch(t *testing.T) {
ctx := t.Context()
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
headRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
_ = baseRepo.LoadOwner(ctx)
_ = headRepo.LoadOwner(ctx)
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// a PR from header's owner
headOwnerPR := &issues_model.PullRequest{
Issue: &issues_model.Issue{
RepoID: baseRepo.ID,
PosterID: headRepo.OwnerID,
},
HeadRepoID: headRepo.ID,
BaseRepoID: baseRepo.ID,
HeadBranch: "pr-from-head-owner",
BaseBranch: "master",
}
require.NoError(t, issues_model.NewPullRequest(ctx, baseRepo, headOwnerPR.Issue, nil, nil, headOwnerPR))
// a PR from a user, they might have or not have "write" permission in the target repo
anyUserPR := &issues_model.PullRequest{
Issue: &issues_model.Issue{
RepoID: baseRepo.ID,
PosterID: user.ID,
},
HeadRepoID: headRepo.ID,
BaseRepoID: baseRepo.ID,
HeadBranch: "pr-from-head-user",
BaseBranch: "master",
}
require.NoError(t, issues_model.NewPullRequest(ctx, baseRepo, anyUserPR.Issue, nil, nil, anyUserPR))
doerCanWrite := func(doer *user_model.User, pr *issues_model.PullRequest) bool {
headPerm, _ := access.GetIndividualUserRepoPermission(ctx, headRepo, doer)
return issues_model.CanMaintainerWriteToBranch(ctx, headPerm, pr.HeadBranch, doer)
}
t.Run("NoAllowMaintainerEdit", func(t *testing.T) {
assert.True(t, doerCanWrite(headRepo.Owner, headOwnerPR))
assert.False(t, doerCanWrite(baseRepo.Owner, headOwnerPR))
assert.False(t, doerCanWrite(baseRepo.Owner, anyUserPR))
assert.False(t, doerCanWrite(user, anyUserPR))
})
t.Run("WithAllowMaintainerEdit-HeadPosterReader", func(t *testing.T) {
_, err := db.GetEngine(ctx).Where(builder.In("id", []int64{headOwnerPR.ID, anyUserPR.ID})).
Cols("allow_maintainer_edit").
Update(&issues_model.PullRequest{AllowMaintainerEdit: true})
require.NoError(t, err)
assert.True(t, doerCanWrite(baseRepo.Owner, headOwnerPR))
assert.False(t, doerCanWrite(baseRepo.Owner, anyUserPR)) // poster doesn't have write permission, so maintainer can't write either
})
t.Run("WithAllowMaintainerEdit-HeadPosterWriter", func(t *testing.T) {
_, err := db.GetEngine(ctx).Where(builder.In("id", []int64{headOwnerPR.ID, anyUserPR.ID})).
Cols("allow_maintainer_edit").
Update(&issues_model.PullRequest{AllowMaintainerEdit: true})
require.NoError(t, err)
err = db.Insert(ctx, &repo_model.Collaboration{RepoID: headRepo.ID, UserID: user.ID, Mode: perm.AccessModeWrite})
require.NoError(t, err)
err = db.Insert(ctx, &access.Access{RepoID: headRepo.ID, UserID: user.ID, Mode: perm.AccessModeWrite})
require.NoError(t, err)
assert.True(t, doerCanWrite(baseRepo.Owner, headOwnerPR))
assert.True(t, doerCanWrite(baseRepo.Owner, anyUserPR)) // now the poster has the write permission
})
}

View File

@@ -17,16 +17,43 @@ import (
"github.com/stretchr/testify/require"
)
func TestPullRequest_LoadAttributes(t *testing.T) {
func TestPullRequest(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
t.Run("LoadAttributes", testPullRequestLoadAttributes)
t.Run("LoadIssue", testPullRequestLoadIssue)
t.Run("LoadBaseRepo", testPullRequestLoadBaseRepo)
t.Run("LoadHeadRepo", testPullRequestLoadHeadRepo)
t.Run("PullRequestsNewest", testPullRequestsNewest)
t.Run("PullRequestsOldest", testPullRequestsOldest)
t.Run("GetUnmergedPullRequest", testGetUnmergedPullRequest)
t.Run("HasUnmergedPullRequestsByHeadInfo", testHasUnmergedPullRequestsByHeadInfo)
t.Run("GetUnmergedPullRequestsByHeadInfo", testGetUnmergedPullRequestsByHeadInfo)
t.Run("GetUnmergedPullRequestsByBaseInfo", testGetUnmergedPullRequestsByBaseInfo)
t.Run("GetPullRequestByIndex", testGetPullRequestByIndex)
t.Run("GetPullRequestByID", testGetPullRequestByID)
t.Run("GetPullRequestByIssueID", testGetPullRequestByIssueID)
t.Run("PullRequest_UpdateCols", testPullRequestUpdateCols)
t.Run("PullRequest_IsWorkInProgress", testPullRequestIsWorkInProgress)
t.Run("PullRequest_GetWorkInProgressPrefixWorkInProgress", testPullRequestGetWorkInProgressPrefixWorkInProgress)
t.Run("DeleteOrphanedObjects", testDeleteOrphanedObjects)
t.Run("ParseCodeOwnersLine", testParseCodeOwnersLine)
t.Run("CodeOwnerAbsolutePathPatterns", testCodeOwnerAbsolutePathPatterns)
t.Run("GetApprovers", testGetApprovers)
t.Run("GetPullRequestByMergedCommit", testGetPullRequestByMergedCommit)
t.Run("Migrate_InsertPullRequests", testMigrateInsertPullRequests)
t.Run("PullRequestsClosedRecentSortType", testPullRequestsClosedRecentSortType)
t.Run("LoadRequestedReviewers", testLoadRequestedReviewers)
}
func testPullRequestLoadAttributes(t *testing.T) {
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
assert.NoError(t, pr.LoadAttributes(t.Context()))
assert.NotNil(t, pr.Merger)
assert.Equal(t, pr.MergerID, pr.Merger.ID)
}
func TestPullRequest_LoadIssue(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func testPullRequestLoadIssue(t *testing.T) {
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
assert.NoError(t, pr.LoadIssue(t.Context()))
assert.NotNil(t, pr.Issue)
@@ -36,8 +63,7 @@ func TestPullRequest_LoadIssue(t *testing.T) {
assert.Equal(t, int64(2), pr.Issue.ID)
}
func TestPullRequest_LoadBaseRepo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func testPullRequestLoadBaseRepo(t *testing.T) {
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
assert.NoError(t, pr.LoadBaseRepo(t.Context()))
assert.NotNil(t, pr.BaseRepo)
@@ -47,8 +73,7 @@ func TestPullRequest_LoadBaseRepo(t *testing.T) {
assert.Equal(t, pr.BaseRepoID, pr.BaseRepo.ID)
}
func TestPullRequest_LoadHeadRepo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func testPullRequestLoadHeadRepo(t *testing.T) {
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
assert.NoError(t, pr.LoadHeadRepo(t.Context()))
assert.NotNil(t, pr.HeadRepo)
@@ -59,8 +84,7 @@ func TestPullRequest_LoadHeadRepo(t *testing.T) {
// TODO TestNewPullRequest
func TestPullRequestsNewest(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func testPullRequestsNewest(t *testing.T) {
prs, count, err := issues_model.PullRequests(t.Context(), 1, &issues_model.PullRequestsOptions{
ListOptions: db.ListOptions{
Page: 1,
@@ -77,7 +101,7 @@ func TestPullRequestsNewest(t *testing.T) {
}
}
func TestPullRequests_Closed_RecentSortType(t *testing.T) {
func testPullRequestsClosedRecentSortType(t *testing.T) {
// Issue ID | Closed At. | Updated At
// 2 | 1707270001 | 1707270001
// 3 | 1707271000 | 1707279999
@@ -90,7 +114,6 @@ func TestPullRequests_Closed_RecentSortType(t *testing.T) {
{"recentclose", []int64{11, 3, 2}},
}
assert.NoError(t, unittest.PrepareTestDatabase())
_, err := db.Exec(t.Context(), "UPDATE issue SET closed_unix = 1707270001, updated_unix = 1707270001, is_closed = true WHERE id = 2")
require.NoError(t, err)
_, err = db.Exec(t.Context(), "UPDATE issue SET closed_unix = 1707271000, updated_unix = 1707279999, is_closed = true WHERE id = 3")
@@ -118,9 +141,7 @@ func TestPullRequests_Closed_RecentSortType(t *testing.T) {
}
}
func TestLoadRequestedReviewers(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func testLoadRequestedReviewers(t *testing.T) {
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
assert.NoError(t, pull.LoadIssue(t.Context()))
issue := pull.Issue
@@ -146,8 +167,7 @@ func TestLoadRequestedReviewers(t *testing.T) {
assert.Empty(t, pull.RequestedReviewers)
}
func TestPullRequestsOldest(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func testPullRequestsOldest(t *testing.T) {
prs, count, err := issues_model.PullRequests(t.Context(), 1, &issues_model.PullRequestsOptions{
ListOptions: db.ListOptions{
Page: 1,
@@ -164,8 +184,7 @@ func TestPullRequestsOldest(t *testing.T) {
}
}
func TestGetUnmergedPullRequest(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func testGetUnmergedPullRequest(t *testing.T) {
pr, err := issues_model.GetUnmergedPullRequest(t.Context(), 1, 1, "branch2", "master", issues_model.PullRequestFlowGithub)
assert.NoError(t, err)
assert.Equal(t, int64(2), pr.ID)
@@ -175,9 +194,7 @@ func TestGetUnmergedPullRequest(t *testing.T) {
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
}
func TestHasUnmergedPullRequestsByHeadInfo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func testHasUnmergedPullRequestsByHeadInfo(t *testing.T) {
exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(t.Context(), 1, "branch2")
assert.NoError(t, err)
assert.True(t, exist)
@@ -187,8 +204,7 @@ func TestHasUnmergedPullRequestsByHeadInfo(t *testing.T) {
assert.False(t, exist)
}
func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func testGetUnmergedPullRequestsByHeadInfo(t *testing.T) {
prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(t.Context(), 1, "branch2")
assert.NoError(t, err)
assert.Len(t, prs, 1)
@@ -198,8 +214,7 @@ func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) {
}
}
func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func testGetUnmergedPullRequestsByBaseInfo(t *testing.T) {
prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(t.Context(), 1, "master")
assert.NoError(t, err)
assert.Len(t, prs, 1)
@@ -209,8 +224,7 @@ func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) {
assert.Equal(t, "master", pr.BaseBranch)
}
func TestGetPullRequestByIndex(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func testGetPullRequestByIndex(t *testing.T) {
pr, err := issues_model.GetPullRequestByIndex(t.Context(), 1, 2)
assert.NoError(t, err)
assert.Equal(t, int64(1), pr.BaseRepoID)
@@ -225,8 +239,7 @@ func TestGetPullRequestByIndex(t *testing.T) {
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
}
func TestGetPullRequestByID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func testGetPullRequestByID(t *testing.T) {
pr, err := issues_model.GetPullRequestByID(t.Context(), 1)
assert.NoError(t, err)
assert.Equal(t, int64(1), pr.ID)
@@ -237,8 +250,7 @@ func TestGetPullRequestByID(t *testing.T) {
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
}
func TestGetPullRequestByIssueID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func testGetPullRequestByIssueID(t *testing.T) {
pr, err := issues_model.GetPullRequestByIssueID(t.Context(), 2)
assert.NoError(t, err)
assert.Equal(t, int64(2), pr.IssueID)
@@ -248,8 +260,7 @@ func TestGetPullRequestByIssueID(t *testing.T) {
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
}
func TestPullRequest_UpdateCols(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func testPullRequestUpdateCols(t *testing.T) {
pr := &issues_model.PullRequest{
ID: 1,
BaseBranch: "baseBranch",
@@ -265,9 +276,7 @@ func TestPullRequest_UpdateCols(t *testing.T) {
// TODO TestAddTestPullRequestTask
func TestPullRequest_IsWorkInProgress(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func testPullRequestIsWorkInProgress(t *testing.T) {
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
pr.LoadIssue(t.Context())
@@ -280,9 +289,7 @@ func TestPullRequest_IsWorkInProgress(t *testing.T) {
assert.True(t, pr.IsWorkInProgress(t.Context()))
}
func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func testPullRequestGetWorkInProgressPrefixWorkInProgress(t *testing.T) {
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
pr.LoadIssue(t.Context())
@@ -296,9 +303,7 @@ func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) {
assert.Equal(t, "[wip]", pr.GetWorkInProgressPrefix(t.Context()))
}
func TestDeleteOrphanedObjects(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func testDeleteOrphanedObjects(t *testing.T) {
countBefore, err := db.GetEngine(t.Context()).Count(&issues_model.PullRequest{})
assert.NoError(t, err)
@@ -317,7 +322,7 @@ func TestDeleteOrphanedObjects(t *testing.T) {
assert.Equal(t, countBefore, countAfter)
}
func TestParseCodeOwnersLine(t *testing.T) {
func testParseCodeOwnersLine(t *testing.T) {
type CodeOwnerTest struct {
Line string
Tokens []string
@@ -331,6 +336,8 @@ func TestParseCodeOwnersLine(t *testing.T) {
{Line: `docs/(aws|google|azure)/[^/]*\\.(md|txt) @org3 @org2/team2`, Tokens: []string{`docs/(aws|google|azure)/[^/]*\.(md|txt)`, "@org3", "@org2/team2"}},
{Line: `\#path @org3`, Tokens: []string{`#path`, "@org3"}},
{Line: `path\ with\ spaces/ @org3`, Tokens: []string{`path with spaces/`, "@org3"}},
{Line: `/docs/.*\\.md @user1`, Tokens: []string{`/docs/.*\.md`, "@user1"}},
{Line: `!/assets/.*\\.(bin|exe|msi) @user1`, Tokens: []string{`!/assets/.*\.(bin|exe|msi)`, "@user1"}},
}
for _, g := range given {
@@ -339,8 +346,37 @@ func TestParseCodeOwnersLine(t *testing.T) {
}
}
func TestGetApprovers(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func testCodeOwnerAbsolutePathPatterns(t *testing.T) {
type testCase struct {
content string
file string
expected bool
}
cases := []testCase{
// Absolute path pattern should match (leading "/" stripped)
{content: "/README.md @user5\n", file: "README.md", expected: true},
// Absolute path pattern in subdirectory
{content: "/docs/.* @user5\n", file: "docs/foo.md", expected: true},
// Absolute path should not match nested paths it shouldn't
{content: "/docs/.* @user5\n", file: "other/docs/foo.md", expected: false},
// Relative path still works
{content: "README.md @user5\n", file: "README.md", expected: true},
// Negated absolute path pattern
{content: "!/.* @user5\n", file: "README.md", expected: false},
}
for _, c := range cases {
rules, _ := issues_model.GetCodeOwnersFromContent(t.Context(), c.content)
require.NotEmpty(t, rules)
rule := rules[0]
regexpMatched, _ := rule.Rule.MatchString(c.file)
ruleMatched := regexpMatched == !rule.Negative
assert.Equal(t, c.expected, ruleMatched, "pattern %q against file %q", c.content, c.file)
}
}
func testGetApprovers(t *testing.T) {
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 5})
// Official reviews are already deduplicated. Allow unofficial reviews
// to assert that there are no duplicated approvers.
@@ -350,8 +386,7 @@ func TestGetApprovers(t *testing.T) {
assert.Equal(t, expected, approvers)
}
func TestGetPullRequestByMergedCommit(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func testGetPullRequestByMergedCommit(t *testing.T) {
pr, err := issues_model.GetPullRequestByMergedCommit(t.Context(), 1, "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3")
assert.NoError(t, err)
assert.EqualValues(t, 1, pr.ID)
@@ -362,8 +397,7 @@ func TestGetPullRequestByMergedCommit(t *testing.T) {
assert.ErrorAs(t, err, &issues_model.ErrPullRequestNotExist{})
}
func TestMigrate_InsertPullRequests(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func testMigrateInsertPullRequests(t *testing.T) {
reponame := "repo1"
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})

View File

@@ -176,15 +176,7 @@ func (r *Review) LoadReviewer(ctx context.Context) (err error) {
if r.ReviewerID == 0 || r.Reviewer != nil {
return err
}
r.Reviewer, err = user_model.GetPossibleUserByID(ctx, r.ReviewerID)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
return fmt.Errorf("GetPossibleUserByID [%d]: %w", r.ReviewerID, err)
}
r.ReviewerID = user_model.GhostUserID
r.Reviewer = user_model.NewGhostUser()
return nil
}
r.ReviewerID, r.Reviewer, err = user_model.GetPossibleUserByID(ctx, r.ReviewerID)
return err
}

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 {

View File

@@ -8,10 +8,7 @@ import (
"fmt"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
"xorm.io/builder"
@@ -129,49 +126,6 @@ func IsUserOrgOwner(ctx context.Context, users user_model.UserList, orgID int64)
return results
}
// GetOrgAssignees returns all users that have write access and can be assigned to issues
// of the any repository in the organization.
func GetOrgAssignees(ctx context.Context, orgID int64) (_ []*user_model.User, err error) {
e := db.GetEngine(ctx)
userIDs := make([]int64, 0, 10)
if err = e.Table("access").
Join("INNER", "repository", "`repository`.id = `access`.repo_id").
Where("`repository`.owner_id = ? AND `access`.mode >= ?", orgID, perm.AccessModeWrite).
Select("user_id").
Find(&userIDs); err != nil {
return nil, err
}
additionalUserIDs := make([]int64, 0, 10)
if err = e.Table("team_user").
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
Join("INNER", "repository", "`repository`.id = `team_repo`.repo_id").
Where("`repository`.owner_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))",
orgID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests).
Distinct("`team_user`.uid").
Select("`team_user`.uid").
Find(&additionalUserIDs); err != nil {
return nil, err
}
uniqueUserIDs := make(container.Set[int64])
uniqueUserIDs.AddMultiple(userIDs...)
uniqueUserIDs.AddMultiple(additionalUserIDs...)
users := make([]*user_model.User, 0, len(uniqueUserIDs))
if len(userIDs) > 0 {
if err = e.In("id", uniqueUserIDs.Values()).
Where(builder.Eq{"`user`.is_active": true}).
OrderBy(user_model.GetOrderByName()).
Find(&users); err != nil {
return nil, err
}
}
return users, nil
}
func loadOrganizationOwners(ctx context.Context, users user_model.UserList, orgID int64) (map[int64]*TeamUser, error) {
if len(users) == 0 {
return nil, nil //nolint:nilnil // return nil when there are no users

View File

@@ -68,7 +68,7 @@ func TryInsertFile(ctx context.Context, pf *PackageFile) (*PackageFile, error) {
// GetFilesByVersionID gets all files of a version
func GetFilesByVersionID(ctx context.Context, versionID int64) ([]*PackageFile, error) {
pfs := make([]*PackageFile, 0, 10)
return pfs, db.GetEngine(ctx).Where("version_id = ?", versionID).Find(&pfs)
return pfs, db.GetEngine(ctx).Where("version_id = ?", versionID).OrderBy("lower_name, created_unix, id").Find(&pfs)
}
// GetFileForVersionByID gets a file of a version by id

View File

@@ -52,13 +52,13 @@ func InsertProperty(ctx context.Context, refType PropertyType, refID int64, name
// GetProperties gets all properties
func GetProperties(ctx context.Context, refType PropertyType, refID int64) ([]*PackageProperty, error) {
pps := make([]*PackageProperty, 0, 10)
return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ?", refType, refID).Find(&pps)
return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ?", refType, refID).OrderBy("id").Find(&pps)
}
// GetPropertiesByName gets all properties with a specific name
func GetPropertiesByName(ctx context.Context, refType PropertyType, refID int64, name string) ([]*PackageProperty, error) {
pps := make([]*PackageProperty, 0, 10)
return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).Find(&pps)
return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).OrderBy("id").Find(&pps)
}
// UpdateProperty updates a property

View File

@@ -5,14 +5,12 @@ package pull
import (
"context"
"errors"
"fmt"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
)
// AutoMerge represents a pull request scheduled for merging when checks succeed
@@ -78,16 +76,8 @@ func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMe
return false, nil, err
}
doer, err := user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID)
if errors.Is(err, util.ErrNotExist) {
doer, err = user_model.NewGhostUser(), nil
}
if err != nil {
return false, nil, err
}
scheduledPRM.Doer = doer
return true, scheduledPRM, nil
scheduledPRM.DoerID, scheduledPRM.Doer, err = user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID)
return true, scheduledPRM, err
}
// DeleteScheduledAutoMerge delete a scheduled pull request

View File

@@ -50,8 +50,8 @@ type RepoFileOptions struct {
DeprecatedRepoName string // it is only a patch for the non-standard "markup" api
DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api
CurrentRefPath string // eg: "branch/main"
CurrentTreePath string // eg: "path/to/file" in the repo
CurrentRefPath string // eg: "branch/main", it is a sub URL path escaped by callers, TODO: rename to CurrentRefSubURL
CurrentTreePath string // eg: "path/to/file" in the repo, it is the tree path without URL path escaping
}
func NewRenderContextRepoFile(ctx context.Context, repo *repo_model.Repository, opts ...RepoFileOptions) *markup.RenderContext {
@@ -70,6 +70,10 @@ func NewRenderContextRepoFile(ctx context.Context, repo *repo_model.Repository,
"repo": helper.opts.DeprecatedRepoName,
})
}
// External render's iframe needs this to generate correct links
// TODO: maybe need to make it access "CurrentRefPath" directly (but impossible at the moment due to cycle-import)
// CurrentRefPath is already path-escaped by callers
rctx.RenderOptions.Metas["RefTypeNameSubURL"] = helper.opts.CurrentRefPath
rctx = rctx.WithHelper(helper).WithEnableHeadingIDGeneration(true)
return rctx
}

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{

View File

@@ -8,6 +8,7 @@ import (
"strings"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
@@ -23,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,
@@ -61,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,
@@ -94,8 +107,7 @@ func GetWatchedRepos(ctx context.Context, opts *WatchedReposOptions) ([]*Reposit
return db.FindAndCount[Repository](ctx, opts)
}
// GetRepoAssignees returns all users that have write access and can be assigned to issues
// of the repository,
// GetRepoAssignees returns all users that have write access and can be assigned to issues or pull-requests of the repository,
func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.User, err error) {
if err = repo.LoadOwner(ctx); err != nil {
return nil, err
@@ -114,15 +126,9 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us
uniqueUserIDs.AddMultiple(userIDs...)
if repo.Owner.IsOrganization() {
additionalUserIDs := make([]int64, 0, 10)
if err = e.Table("team_user").
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))",
repo.ID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests).
Distinct("`team_user`.uid").
Select("`team_user`.uid").
Find(&additionalUserIDs); err != nil {
// issues and pull requests both need "assignee list"
additionalUserIDs, err := organization.GetTeamUserIDsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypeIssues, unit.TypePullRequests)
if err != nil {
return nil, err
}
uniqueUserIDs.AddMultiple(additionalUserIDs...)

View File

@@ -6,7 +6,12 @@ package repo_test
import (
"testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
perm_model "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
@@ -14,9 +19,14 @@ import (
"github.com/stretchr/testify/require"
)
func TestRepoAssignees(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func TestUserRepo(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
t.Run("GetIssuePostersWithSearch", testUserRepoGetIssuePostersWithSearch)
t.Run("Assignees", testUserRepoAssignees)
t.Run("AssigneesNoTeamUnit", testRepoAssigneesNoTeamUnit)
}
func testUserRepoAssignees(t *testing.T) {
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
users, err := repo_model.GetRepoAssignees(t.Context(), repo2)
assert.NoError(t, err)
@@ -39,9 +49,29 @@ func TestRepoAssignees(t *testing.T) {
}
}
func TestGetIssuePostersWithSearch(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func testRepoAssigneesNoTeamUnit(t *testing.T) {
ctx := t.Context()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32})
require.NoError(t, repo.LoadOwner(ctx))
require.True(t, repo.Owner.IsOrganization())
require.NoError(t, db.TruncateBeans(ctx, &organization.Team{}, &organization.TeamUser{}, &organization.TeamRepo{}, &organization.TeamUnit{}, &access_model.Access{}))
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
team := &organization.Team{OrgID: repo.OwnerID, LowerName: "admin-team", AccessMode: perm_model.AccessModeAdmin}
require.NoError(t, db.Insert(ctx, team))
require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: repo.OwnerID, TeamID: team.ID, UID: user.ID}))
require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: repo.OwnerID, TeamID: team.ID, RepoID: repo.ID}))
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: repo.OwnerID, TeamID: team.ID, Type: unit.TypePullRequests, AccessMode: perm_model.AccessModeNone}))
users, err := repo_model.GetRepoAssignees(ctx, repo)
require.NoError(t, err)
require.Len(t, users, 1)
assert.ElementsMatch(t, []int64{4}, []int64{users[0].ID})
}
func testUserRepoGetIssuePostersWithSearch(t *testing.T) {
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
users, err := repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "USER")

View File

@@ -64,7 +64,7 @@ type GetBadgeUsersOptions struct {
func GetBadgeUsers(ctx context.Context, opts *GetBadgeUsersOptions) ([]*User, int64, error) {
sess := db.GetEngine(ctx).
Select("`user`.*").
Join("INNER", "user_badge", "`user_badge`.user_id=user.id").
Join("INNER", "user_badge", "`user_badge`.user_id=`user`.id").
Join("INNER", "badge", "`user_badge`.badge_id=badge.id").
Where("badge.slug=?", opts.BadgeSlug)

View File

@@ -59,6 +59,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) *xorm.Session {
var cond builder.Cond
cond = builder.In("type", opts.Types)

View File

@@ -7,6 +7,7 @@ package user
import (
"context"
"encoding/hex"
"errors"
"fmt"
"html/template"
"mime"
@@ -306,6 +307,13 @@ func (u *User) DashboardLink() string {
return setting.AppSubURL + "/"
}
func (u *User) SettingsLink() string {
if u.IsOrganization() {
return u.OrganisationLink() + "/settings"
}
return setting.AppSubURL + "/user/settings"
}
// HomeLink returns the user or organization home page link.
func (u *User) HomeLink() string {
return setting.AppSubURL + "/" + url.PathEscape(u.Name)
@@ -1016,17 +1024,22 @@ func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
return users, err
}
// GetPossibleUserByID returns the user if id > 0 or returns system user if id < 0
func GetPossibleUserByID(ctx context.Context, id int64) (*User, error) {
// GetPossibleUserByID returns the possible user and its ID. If the user doesn't exist, it returns Ghost user
func GetPossibleUserByID(ctx context.Context, id int64) (_ int64, u *User, err error) {
if id < 0 {
if newFunc, ok := globalVars().systemUserNewFuncs[id]; ok {
return newFunc(), nil
u = newFunc()
}
return nil, ErrUserNotExist{UID: id}
} else if id == 0 {
return nil, ErrUserNotExist{}
}
return GetUserByID(ctx, id)
if u == nil {
u, err = GetUserByID(ctx, id)
if errors.Is(err, util.ErrNotExist) {
u = NewGhostUser()
} else if err != nil {
return 0, nil, err
}
}
return u.ID, u, nil
}
// GetPossibleUserByIDs returns the users if id > 0 or returns system users if id < 0
@@ -1047,13 +1060,13 @@ func GetPossibleUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
return users, nil
}
// GetUserByName returns user by given name.
func GetUserByName(ctx context.Context, name string) (*User, error) {
if len(name) == 0 {
return nil, ErrUserNotExist{Name: name}
func getUserByNameWithTypes(ctx context.Context, name string, types ...UserType) (*User, error) {
u := &User{}
sess := db.GetEngine(ctx).Where(builder.Eq{"lower_name": strings.ToLower(name)})
if len(types) > 0 {
sess.In("`type`", types)
}
u := &User{LowerName: strings.ToLower(name), Type: UserTypeIndividual}
has, err := db.GetEngine(ctx).Get(u)
has, err := sess.Get(u)
if err != nil {
return nil, err
} else if !has {
@@ -1062,6 +1075,15 @@ func GetUserByName(ctx context.Context, name string) (*User, error) {
return u, nil
}
// GetUserByName returns the user object by given name, any user type.
func GetUserByName(ctx context.Context, name string) (*User, error) {
return getUserByNameWithTypes(ctx, name)
}
func GetIndividualUserByName(ctx context.Context, name string) (*User, error) {
return getUserByNameWithTypes(ctx, name, UserTypeIndividual)
}
// GetUserEmailsByNames returns a list of e-mails corresponds to names of users
// that have their email notifications set to enabled or onmention.
func GetUserEmailsByNames(ctx context.Context, names []string) []string {
@@ -1104,19 +1126,6 @@ func GetMailableUsersByIDs(ctx context.Context, ids []int64, isMention bool) ([]
Find(&ous)
}
// GetUserNameByID returns username for the id
func GetUserNameByID(ctx context.Context, id int64) (string, error) {
var name string
has, err := db.GetEngine(ctx).Table("user").Where("id = ?", id).Cols("name").Get(&name)
if err != nil {
return "", err
}
if has {
return name, nil
}
return "", nil
}
// GetUserIDsByNames returns a slice of ids corresponds to names.
func GetUserIDsByNames(ctx context.Context, names []string, ignoreNonExistent bool) ([]int64, error) {
ids := make([]int64, 0, len(names))
@@ -1317,13 +1326,14 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) {
if id != 0 {
return GetUserByID(ctx, id)
}
return GetUserByName(ctx, name)
return GetIndividualUserByName(ctx, name)
}
return nil, ErrUserNotExist{Name: email}
}
func GetIndividualUser(ctx context.Context, user *User) (bool, error) {
// FIXME: the design is wrong, empty User fields won't apply, this function should be removed in the future
has, err := db.GetEngine(ctx).Get(user)
if has && user.Type != UserTypeIndividual {
has = false
@@ -1498,27 +1508,3 @@ func DisabledFeaturesWithLoginType(user *User) *container.Set[string] {
}
return &setting.Admin.UserDisabledFeatures
}
// GetUserOrOrgIDByName returns the id for a user or an org by name
func GetUserOrOrgIDByName(ctx context.Context, name string) (int64, error) {
var id int64
has, err := db.GetEngine(ctx).Table("user").Where("name = ?", name).Cols("id").Get(&id)
if err != nil {
return 0, err
} else if !has {
return 0, fmt.Errorf("user or org with name %s: %w", name, util.ErrNotExist)
}
return id, nil
}
// GetUserOrOrgByName returns the user or org by name
func GetUserOrOrgByName(ctx context.Context, name string) (*User, error) {
var u User
has, err := db.GetEngine(ctx).Where("lower_name = ?", strings.ToLower(name)).Get(&u)
if err != nil {
return nil, err
} else if !has {
return nil, ErrUserNotExist{Name: name}
}
return &u, nil
}

View File

@@ -11,8 +11,9 @@ import (
)
func TestSystemUser(t *testing.T) {
u, err := GetPossibleUserByID(t.Context(), -1)
uid, u, err := GetPossibleUserByID(t.Context(), -1)
require.NoError(t, err)
assert.Equal(t, int64(-1), uid)
assert.Equal(t, "Ghost", u.Name)
assert.Equal(t, "ghost", u.LowerName)
assert.True(t, u.IsGhost())
@@ -21,8 +22,9 @@ func TestSystemUser(t *testing.T) {
require.NotNil(t, u)
assert.Equal(t, "Ghost", u.Name)
u, err = GetPossibleUserByID(t.Context(), -2)
uid, u, err = GetPossibleUserByID(t.Context(), -2)
require.NoError(t, err)
assert.Equal(t, int64(-2), uid)
assert.Equal(t, "gitea-actions", u.Name)
assert.Equal(t, "gitea-actions", u.LowerName)
assert.True(t, u.IsGiteaActions())
@@ -31,6 +33,8 @@ func TestSystemUser(t *testing.T) {
require.NotNil(t, u)
assert.Equal(t, "Gitea Actions", u.FullName)
_, err = GetPossibleUserByID(t.Context(), -3)
require.Error(t, err)
uid, u, err = GetPossibleUserByID(t.Context(), 999999)
require.NoError(t, err)
assert.Equal(t, int64(-1), uid)
assert.Equal(t, "Ghost", u.Name)
}

View File

@@ -4,6 +4,10 @@
package actions
import (
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"io"
"net/http"
"strings"
@@ -15,6 +19,22 @@ import (
"code.gitea.io/gitea/services/context"
)
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)
}
// IsArtifactV4 detects whether the artifact is likely from v4.
// V4 backend stores the files as a single combined zip file per artifact, and ensures ContentEncoding contains a slash
// (otherwise this uses application/zip instead of the custom mime type), which is not the case for the old backend.

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)
}

View File

@@ -31,6 +31,9 @@ func Parse(content []byte, options ...ParseOption) ([]*SingleWorkflow, error) {
}
results := map[string]*JobResult{}
for id, job := range origin.Jobs {
if job == nil {
return nil, fmt.Errorf("needed job not found: %q", id)
}
results[id] = &JobResult{
Needs: job.Needs(),
Result: pc.jobResults[id],

View File

@@ -59,6 +59,13 @@ func TestParse(t *testing.T) {
wantErr: false,
},
}
invalidFileTests := []struct {
name string
}{
{name: "null_job_implicit"},
{name: "null_job_explicit"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
content := ReadTestdata(t, tt.name+".in.yaml")
@@ -84,4 +91,14 @@ func TestParse(t *testing.T) {
assert.Equal(t, string(want), builder.String())
})
}
for _, tt := range invalidFileTests {
t.Run(tt.name, func(t *testing.T) {
content := ReadTestdata(t, tt.name+".in.yaml")
require.NotPanics(t, func() {
_, err := Parse(content)
require.Error(t, err)
})
})
}
}

View File

@@ -0,0 +1,9 @@
# null_job_explicit.in.yaml
on: push
jobs:
empty: null
notempty:
needs: empty
runs-on: ubuntu-latest
steps:
- run: echo ok

View File

@@ -0,0 +1,9 @@
# null_job_implicit.in.yaml
on: push
jobs:
empty:
notempty:
needs: empty
runs-on: ubuntu-latest
steps:
- run: echo ok

View File

@@ -103,10 +103,20 @@ func GetEventsFromContent(content []byte) ([]*jobparser.Event, error) {
if err != nil {
return nil, err
}
if err := ValidateWorkflowContent(content); err != nil {
return nil, err
}
return events, nil
}
// ValidateWorkflowContent catches structural errors (e.g. blank lines in run: | blocks)
// that model.ReadWorkflow alone does not detect.
func ValidateWorkflowContent(content []byte) error {
_, err := jobparser.Parse(content)
return err
}
func DetectWorkflows(
gitRepo *git.Repository,
commit *git.Commit,

View File

@@ -9,16 +9,26 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/stretchr/testify/assert"
)
func fullWorkflowContent(part string) []byte {
return []byte(`
name: test
` + part + `
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo hello
`)
}
func TestIsWorkflow(t *testing.T) {
oldDirs := setting.Actions.WorkflowDirs
defer func() {
setting.Actions.WorkflowDirs = oldDirs
}()
defer test.MockVariableValue(&setting.Actions.WorkflowDirs)()
tests := []struct {
name string
@@ -218,7 +228,7 @@ func TestDetectMatched(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
evts, err := GetEventsFromContent([]byte(tc.yamlOn))
evts, err := GetEventsFromContent(fullWorkflowContent(tc.yamlOn))
assert.NoError(t, err)
assert.Len(t, evts, 1)
assert.Equal(t, tc.expected, detectMatched(nil, tc.commit, tc.triggedEvent, tc.payload, evts[0]))
@@ -373,7 +383,7 @@ func TestMatchIssuesEvent(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
evts, err := GetEventsFromContent([]byte(tc.yamlOn))
evts, err := GetEventsFromContent(fullWorkflowContent(tc.yamlOn))
assert.NoError(t, err)
assert.Len(t, evts, 1)

View File

@@ -4,6 +4,7 @@
package dump
import (
"archive/zip"
"context"
"errors"
"fmt"
@@ -85,7 +86,7 @@ func NewDumper(ctx context.Context, format string, output io.Writer) (*Dumper, e
var comp archives.ArchiverAsync
switch format {
case "zip":
comp = archives.Zip{}
comp = archives.Zip{Compression: zip.Deflate}
case "tar":
comp = archives.Tar{}
case "tar.sz":

View File

@@ -7,8 +7,10 @@ import (
"context"
"os"
"path/filepath"
"strings"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
@@ -39,6 +41,9 @@ func (b *catFileBatchCommand) getBatch() *catFileBatchCommunicator {
}
func (b *catFileBatchCommand) QueryContent(obj string) (*CatFileObject, BufferedReader, error) {
if strings.Contains(obj, "\n") {
setting.PanicInDevOrTesting("invalid object name with newline: %q", obj)
}
_, err := b.getBatch().reqWriter.Write([]byte("contents " + obj + "\n"))
if err != nil {
return nil, nil, err
@@ -51,6 +56,9 @@ func (b *catFileBatchCommand) QueryContent(obj string) (*CatFileObject, Buffered
}
func (b *catFileBatchCommand) QueryInfo(obj string) (*CatFileObject, error) {
if strings.Contains(obj, "\n") {
setting.PanicInDevOrTesting("invalid object name with newline: %q", obj)
}
_, err := b.getBatch().reqWriter.Write([]byte("info " + obj + "\n"))
if err != nil {
return nil, err

View File

@@ -8,8 +8,10 @@ import (
"io"
"os"
"path/filepath"
"strings"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
@@ -50,6 +52,9 @@ func (b *catFileBatchLegacy) getBatchCheck() *catFileBatchCommunicator {
}
func (b *catFileBatchLegacy) QueryContent(obj string) (*CatFileObject, BufferedReader, error) {
if strings.Contains(obj, "\n") {
setting.PanicInDevOrTesting("invalid object name with newline: %q", obj)
}
_, err := io.WriteString(b.getBatchContent().reqWriter, obj+"\n")
if err != nil {
return nil, nil, err
@@ -62,6 +67,9 @@ func (b *catFileBatchLegacy) QueryContent(obj string) (*CatFileObject, BufferedR
}
func (b *catFileBatchLegacy) QueryInfo(obj string) (*CatFileObject, error) {
if strings.Contains(obj, "\n") {
setting.PanicInDevOrTesting("invalid object name with newline: %q", obj)
}
_, err := io.WriteString(b.getBatchCheck().reqWriter, obj+"\n")
if err != nil {
return nil, err

View File

@@ -13,63 +13,45 @@ import (
"strconv"
"strings"
"sync/atomic"
"time"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
)
var catFileBatchDebugWaitClose atomic.Int64
type catFileBatchCommunicator struct {
closeFunc func(err error)
closeFunc atomic.Pointer[func(err error)]
reqWriter io.Writer
respReader *bufio.Reader
debugGitCmd *gitcmd.Command
}
func (b *catFileBatchCommunicator) Close() {
if b.closeFunc != nil {
b.closeFunc(nil)
b.closeFunc = nil
func (b *catFileBatchCommunicator) Close(err ...error) {
if fn := b.closeFunc.Swap(nil); fn != nil {
(*fn)(util.OptionalArg(err))
}
}
// newCatFileBatch opens git cat-file --batch in the provided repo and returns a stdin pipe, a stdout reader and cancel function
func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Command) (ret *catFileBatchCommunicator) {
// newCatFileBatch opens git cat-file --batch/--batch-check/--batch-command command and prepares the stdin/stdout pipes for communication.
func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Command) *catFileBatchCommunicator {
ctx, ctxCancel := context.WithCancelCause(ctx)
// We often want to feed the commits in order into cat-file --batch, followed by their trees and subtrees as necessary.
stdinWriter, stdoutReader, stdPipeClose := cmdCatFile.MakeStdinStdoutPipe()
pipeClose := func() {
if delay := catFileBatchDebugWaitClose.Load(); delay > 0 {
time.Sleep(time.Duration(delay)) // for testing purpose only
}
stdPipeClose()
}
closeFunc := func(err error) {
ctxCancel(err)
pipeClose()
}
return newCatFileBatchWithCloseFunc(ctx, repoPath, cmdCatFile, stdinWriter, stdoutReader, closeFunc)
}
func newCatFileBatchWithCloseFunc(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Command,
stdinWriter gitcmd.PipeWriter, stdoutReader gitcmd.PipeReader, closeFunc func(err error),
) *catFileBatchCommunicator {
ret := &catFileBatchCommunicator{
debugGitCmd: cmdCatFile,
closeFunc: closeFunc,
reqWriter: stdinWriter,
respReader: bufio.NewReaderSize(stdoutReader, 32*1024), // use a buffered reader for rich operations
}
ret.closeFunc.Store(new(func(err error) {
ctxCancel(err)
stdPipeClose()
}))
err := cmdCatFile.WithDir(repoPath).StartWithStderr(ctx)
if err != nil {
log.Error("Unable to start git command %v: %v", cmdCatFile.LogString(), err)
// ideally here it should return the error, but it would require refactoring all callers
// so just return a dummy communicator that does nothing, almost the same behavior as before, not bad
closeFunc(err)
ret.Close(err)
return ret
}
@@ -78,12 +60,33 @@ func newCatFileBatchWithCloseFunc(ctx context.Context, repoPath string, cmdCatFi
if err != nil && !errors.Is(err, context.Canceled) {
log.Error("cat-file --batch command failed in repo %s, error: %v", repoPath, err)
}
closeFunc(err)
ret.Close(err)
}()
return ret
}
func (b *catFileBatchCommunicator) debugKill() (ret struct {
beforeClose chan struct{}
blockClose chan struct{}
afterClose chan struct{}
},
) {
ret.beforeClose = make(chan struct{})
ret.blockClose = make(chan struct{})
ret.afterClose = make(chan struct{})
oldCloseFunc := b.closeFunc.Load()
b.closeFunc.Store(new(func(err error) {
b.closeFunc.Store(nil)
close(ret.beforeClose)
<-ret.blockClose
(*oldCloseFunc)(err)
close(ret.afterClose)
}))
b.debugGitCmd.DebugKill()
return ret
}
// catFileBatchParseInfoLine reads the header line from cat-file --batch
// We expect: <oid> SP <type> SP <size> LF
// then leaving the rest of the stream "<contents> LF" to be read

View File

@@ -7,9 +7,7 @@ import (
"io"
"os"
"path/filepath"
"sync"
"testing"
"time"
"code.gitea.io/gitea/modules/test"
@@ -39,13 +37,22 @@ func testCatFileBatch(t *testing.T) {
require.Error(t, err)
})
simulateQueryTerminated := func(pipeCloseDelay, pipeReadDelay time.Duration) (errRead error) {
catFileBatchDebugWaitClose.Store(int64(pipeCloseDelay))
defer catFileBatchDebugWaitClose.Store(0)
simulateQueryTerminated := func(t *testing.T, errBeforePipeClose, errAfterPipeClose error) {
readError := func(t *testing.T, r io.Reader, expectedErr error) {
if expectedErr == nil {
return // expectedErr == nil means this read should be skipped
}
n, err := r.Read(make([]byte, 100))
assert.Zero(t, n)
assert.ErrorIs(t, err, expectedErr)
}
batch, err := NewBatch(t.Context(), filepath.Join(testReposDir, "repo1_bare"))
require.NoError(t, err)
defer batch.Close()
_, _ = batch.QueryInfo("e2129701f1a4d54dc44f03c93bca0a2aec7c5449")
_, err = batch.QueryInfo("e2129701f1a4d54dc44f03c93bca0a2aec7c5449")
require.NoError(t, err)
var c *catFileBatchCommunicator
switch b := batch.(type) {
case *catFileBatchLegacy:
@@ -58,24 +65,18 @@ func testCatFileBatch(t *testing.T) {
t.FailNow()
}
wg := sync.WaitGroup{}
wg.Go(func() {
time.Sleep(pipeReadDelay)
var n int
n, errRead = c.respReader.Read(make([]byte, 100))
assert.Zero(t, n)
})
time.Sleep(10 * time.Millisecond)
c.debugGitCmd.DebugKill()
wg.Wait()
return errRead
}
require.NotEqual(t, errBeforePipeClose == nil, errAfterPipeClose == nil, "must set exactly one of the expected errors")
inceptor := c.debugKill()
<-inceptor.beforeClose // wait for the command's Close to be called, the pipe is not closed yet
readError(t, c.respReader, errBeforePipeClose) // then caller will read on an open pipe which will be closed soon
close(inceptor.blockClose) // continue to close the pipe
<-inceptor.afterClose // wait for the pipe to be closed
readError(t, c.respReader, errAfterPipeClose) // then caller will read on a closed pipe
}
t.Run("QueryTerminated", func(t *testing.T) {
err := simulateQueryTerminated(0, 20*time.Millisecond)
assert.ErrorIs(t, err, os.ErrClosed) // pipes are closed faster
err = simulateQueryTerminated(40*time.Millisecond, 20*time.Millisecond)
assert.ErrorIs(t, err, io.EOF) // reader is faster
simulateQueryTerminated(t, io.EOF, nil) // reader is faster
simulateQueryTerminated(t, nil, os.ErrClosed) // pipes are closed faster
})
batch, err := NewBatch(t.Context(), filepath.Join(testReposDir, "repo1_bare"))

View File

@@ -37,11 +37,7 @@ type CommitSignature struct {
// Message returns the commit message. Same as retrieving CommitMessage directly.
func (c *Commit) Message() string {
// FIXME: GIT-COMMIT-MESSAGE-ENCODING: this logic is not right
// * When need to use commit message in templates/database, it should be valid UTF-8
// * When need to get the original commit message, it should just use "c.CommitMessage"
// It's not easy to refactor at the moment, many templates need to be updated and tested
return c.CommitMessage
return strings.ToValidUTF8(c.CommitMessage, "?")
}
// Summary returns first line of commit message.

View File

@@ -159,6 +159,14 @@ ISO-8859-1`, commitFromReader.Signature.Payload)
assert.Equal(t, commitFromReader, commitFromReader2)
}
func TestCommitMessageSanitizesInvalidUTF8(t *testing.T) {
commit := &Commit{
CommitMessage: "title \xff\n\n\nbody \xff\n\n\n",
}
assert.Equal(t, "title ?\n\n\nbody ?\n\n\n", commit.Message())
assert.Equal(t, "title ?", commit.Summary())
}
func TestHasPreviousCommit(t *testing.T) {
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")

View File

@@ -57,14 +57,12 @@ type Command struct {
}
func logArgSanitize(arg string) string {
if strings.Contains(arg, "://") && strings.Contains(arg, "@") {
return util.SanitizeCredentialURLs(arg)
} else if filepath.IsAbs(arg) {
if filepath.IsAbs(arg) {
base := filepath.Base(arg)
dir := filepath.Dir(arg)
return ".../" + filepath.Join(filepath.Base(dir), base)
}
return arg
return util.SanitizeCredentialURLs(arg)
}
func (c *Command) LogString() string {

View File

@@ -109,7 +109,10 @@ func TestCommandString(t *testing.T) {
assert.Equal(t, cmd.prog+` a "-m msg" "it's a test" "say \"hello\""`, cmd.LogString())
cmd = NewCommand("url: https://a:b@c/", "/root/dir-a/dir-b")
assert.Equal(t, cmd.prog+` "url: https://sanitized-credential@c/" .../dir-a/dir-b`, cmd.LogString())
assert.Equal(t, cmd.prog+` "url: https://(masked)@c/" .../dir-a/dir-b`, cmd.LogString())
cmd = NewCommand("url: a:b@c/", "/root/dir-a/dir-b")
assert.Equal(t, cmd.prog+` "url: (masked)@c/" .../dir-a/dir-b`, cmd.LogString())
}
func TestRunStdError(t *testing.T) {

View File

@@ -11,7 +11,6 @@ import (
"encoding/hex"
"io"
"sort"
"strings"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
@@ -102,7 +101,7 @@ func findLFSFileFunc(repo *git.Repository, objectID git.ObjectID, revListReader
result := LFSResult{
Name: curPath + string(fname),
SHA: curCommit.ID.String(),
Summary: strings.Split(strings.TrimSpace(curCommit.CommitMessage), "\n")[0],
Summary: curCommit.Summary(),
When: curCommit.Author.When,
ParentHashes: curCommit.Parents,
}

View File

@@ -4,9 +4,18 @@
package gitrepo
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
"time"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type mockRepository struct {
@@ -17,6 +26,61 @@ func (r *mockRepository) RelativePath() string {
return r.path
}
func commitRootTree(t *testing.T, repoDir, fileName, content, message string) string {
t.Helper()
require.NoError(t, gitcmd.NewCommand("read-tree", "--empty").WithDir(repoDir).Run(t.Context()))
stdout, _, err := gitcmd.NewCommand("hash-object", "-w", "--stdin").
WithDir(repoDir).
WithStdinBytes([]byte(content)).
RunStdString(t.Context())
require.NoError(t, err)
blobSHA := strings.TrimSpace(stdout)
_, _, err = gitcmd.NewCommand("update-index", "--add", "--replace", "--cacheinfo").
AddDynamicArguments("100644", blobSHA, fileName).
WithDir(repoDir).
RunStdString(t.Context())
require.NoError(t, err)
stdout, _, err = gitcmd.NewCommand("write-tree").WithDir(repoDir).RunStdString(t.Context())
require.NoError(t, err)
treeSHA := strings.TrimSpace(stdout)
commitTimeStr := time.Now().Format(time.RFC3339)
env := append(os.Environ(),
"GIT_AUTHOR_NAME=Test",
"GIT_AUTHOR_EMAIL=test@example.com",
"GIT_AUTHOR_DATE="+commitTimeStr,
"GIT_COMMITTER_NAME=Test",
"GIT_COMMITTER_EMAIL=test@example.com",
"GIT_COMMITTER_DATE="+commitTimeStr,
)
messageBytes := bytes.NewBufferString(message + "\n")
stdout, _, err = gitcmd.NewCommand("commit-tree").AddDynamicArguments(treeSHA).
WithEnv(env).
WithDir(repoDir).
WithStdinBytes(messageBytes.Bytes()).
RunStdString(t.Context())
require.NoError(t, err)
return strings.TrimSpace(stdout)
}
func TestMergeBaseNoCommonHistory(t *testing.T) {
repoDir := filepath.Join(t.TempDir(), "repo.git")
require.NoError(t, gitcmd.NewCommand("init").AddDynamicArguments(repoDir).Run(t.Context()))
baseCommit := commitRootTree(t, repoDir, "base.txt", "base", "base")
headCommit := commitRootTree(t, repoDir, "head.txt", "head", "head")
mergeBase, err := MergeBase(t.Context(), &mockRepository{path: repoDir}, baseCommit, headCommit)
assert.Empty(t, mergeBase)
assert.ErrorIs(t, err, util.ErrNotExist)
}
func TestRepoGetDivergingCommits(t *testing.T) {
repo := &mockRepository{path: "repo1_bare"}
do, err := GetDivergingCommits(t.Context(), repo, "master", "branch2")

View File

@@ -9,6 +9,7 @@ import (
"strings"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/util"
)
// MergeBase checks and returns merge base of two commits.
@@ -16,6 +17,9 @@ func MergeBase(ctx context.Context, repo Repository, baseCommitID, headCommitID
mergeBase, _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("merge-base").
AddDashesAndList(baseCommitID, headCommitID))
if err != nil {
if gitcmd.IsErrorExitCode(err, 1) {
return "", util.NewNotExistErrorf("get merge-base of %s and %s failed", baseCommitID, headCommitID)
}
return "", fmt.Errorf("get merge-base of %s and %s failed: %w", baseCommitID, headCommitID, err)
}
return strings.TrimSpace(mergeBase), nil

View File

@@ -37,6 +37,42 @@ type ServeHeaderOptions struct {
LastModified time.Time
}
const (
// Disable JS execution on the same origin, since we serve the file from the same origin as Gitea server.
// This rule can be relaxed in the future as long as it is properly sandboxed.
// "style-src" is for SVG inline styles (from Display SVG files as images instead of text #14101)
serveHeaderCspDefault = "default-src 'none'; style-src 'unsafe-inline'; sandbox"
// No sandbox attribute for PDF as it breaks rendering in at least Safari.
// This should generally be safe as scripts inside PDF can not escape the PDF document.
// See https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion.
// HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context
serveHeaderCspPdf = "default-src 'none'; style-src 'unsafe-inline'"
// For audios and videos, actually it doesn't really need CSP (just like Gitea <= 1.25)
serveHeaderCspAudioVideo = ""
)
func serveSetHeaderContentRelated(w http.ResponseWriter, contentType string) {
header := w.Header()
contentType = util.IfZero(contentType, typesniffer.MimeTypeApplicationOctetStream)
header.Set("Content-Type", contentType)
header.Set("X-Content-Type-Options", "nosniff")
csp := serveHeaderCspDefault
if strings.HasPrefix(contentType, "application/pdf") {
csp = serveHeaderCspPdf
}
if strings.HasPrefix(contentType, "video/") || strings.HasPrefix(contentType, "audio/") {
csp = serveHeaderCspAudioVideo
}
if csp != "" {
header.Set("Content-Security-Policy", csp)
} else {
header.Del("Content-Security-Policy")
}
}
// ServeSetHeaders sets necessary content serve headers
func ServeSetHeaders(w http.ResponseWriter, opts ServeHeaderOptions) {
header := w.Header()
@@ -46,26 +82,14 @@ func ServeSetHeaders(w http.ResponseWriter, opts ServeHeaderOptions) {
w.Header().Add(gzhttp.HeaderNoCompression, "1")
}
contentType := util.IfZero(opts.ContentType, typesniffer.MimeTypeApplicationOctetStream)
header.Set("Content-Type", contentType)
header.Set("X-Content-Type-Options", "nosniff")
serveSetHeaderContentRelated(w, opts.ContentType)
if opts.ContentLength != nil {
header.Set("Content-Length", strconv.FormatInt(*opts.ContentLength, 10))
}
// Disable script execution of HTML/SVG files, since we serve the file from the same origin as Gitea server
header.Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
if strings.Contains(contentType, "application/pdf") {
// no sandbox attribute for PDF as it breaks rendering in at least safari. this
// should generally be safe as scripts inside PDF can not escape the PDF document
// see https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion
// HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context
header.Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
}
if opts.Filename != "" && opts.ContentDisposition != "" {
header.Set("Content-Disposition", encodeContentDisposition(opts.ContentDisposition, path.Base(opts.Filename)))
if opts.Filename != "" {
contentDisposition := util.IfZero(opts.ContentDisposition, ContentDispositionAttachment)
header.Set("Content-Disposition", encodeContentDisposition(contentDisposition, path.Base(opts.Filename)))
header.Set("Access-Control-Expose-Headers", "Content-Disposition")
}

View File

@@ -12,6 +12,8 @@ import (
"strings"
"testing"
"code.gitea.io/gitea/modules/typesniffer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -106,3 +108,36 @@ func TestServeUserContentByFile(t *testing.T) {
test(t, http.StatusPartialContent, data[1:])
})
}
func TestServeSetHeaderContentRelated(t *testing.T) {
cases := []struct {
contentType string
csp string
}{
{"", serveHeaderCspDefault},
{"any", serveHeaderCspDefault},
{"application/pdf", serveHeaderCspPdf},
{"application/pdf; other", serveHeaderCspPdf},
{"audio/mp4", serveHeaderCspAudioVideo},
{"video/ogg; other", serveHeaderCspAudioVideo},
{typesniffer.MimeTypeImageSvg, serveHeaderCspDefault},
}
for _, c := range cases {
w := httptest.NewRecorder()
serveSetHeaderContentRelated(w, c.contentType)
csp := w.Header().Get("Content-Security-Policy")
assert.Equal(t, c.csp, csp, "content-type: %s", c.contentType)
assert.Equal(t, "nosniff", w.Header().Get("X-Content-Type-Options")) // it should always be there
}
// make sure sandboxed
require.Contains(t, serveHeaderCspDefault, "; sandbox")
}
func TestServeSetHeaders(t *testing.T) {
w := httptest.NewRecorder()
ServeSetHeaders(w, ServeHeaderOptions{Filename: "foo.zip"})
assert.Equal(t, "attachment; filename=foo.zip", w.Header().Get("Content-Disposition"))
ServeSetHeaders(w, ServeHeaderOptions{Filename: "foo.zip", ContentDisposition: ContentDispositionInline})
assert.Equal(t, "inline; filename=foo.zip", w.Header().Get("Content-Disposition"))
}

View File

@@ -1,35 +0,0 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package json
import (
"bytes"
"io"
"github.com/goccy/go-json"
)
var _ Interface = jsonGoccy{}
type jsonGoccy struct{}
func (jsonGoccy) Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
func (jsonGoccy) Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
func (jsonGoccy) NewEncoder(writer io.Writer) Encoder {
return json.NewEncoder(writer)
}
func (jsonGoccy) NewDecoder(reader io.Reader) Decoder {
return json.NewDecoder(reader)
}
func (jsonGoccy) Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
return json.Indent(dst, src, prefix, indent)
}

View File

@@ -6,12 +6,12 @@
package json
import (
"encoding/json"
"encoding/json" //nolint:depguard // this package wraps it
"io"
)
func getDefaultJSONHandler() Interface {
return jsonGoccy{}
return jsonV1{}
}
func MarshalKeepOptionalEmpty(v any) ([]byte, error) {

View File

@@ -5,6 +5,7 @@ package log
import (
"context"
"net/url"
"reflect"
"runtime"
"strings"
@@ -226,6 +227,8 @@ func (l *LoggerImpl) Log(skip int, event *Event, format string, logArgs ...any)
}
} else if ls := asLogStringer(v); ls != nil {
msgArgs[i] = logStringFormatter{v: ls}
} else if str, ok := v.(string); ok {
msgArgs[i] = protectSensitiveInfo(str)
}
}
@@ -235,6 +238,24 @@ func (l *LoggerImpl) Log(skip int, event *Event, format string, logArgs ...any)
l.SendLogEvent(event)
}
func protectSensitiveInfo(s string) string {
u, err := url.Parse(s)
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
return s
}
q := u.Query()
for _, vals := range q {
for i := range vals {
vals[i] = "_"
}
}
masked := &url.URL{Scheme: u.Scheme, Host: u.Host, Path: u.Path, RawQuery: q.Encode()}
if u.User != nil {
masked.User = url.User("_masked_")
}
return masked.String()
}
func (l *LoggerImpl) GetLevel() Level {
return Level(l.level.Load())
}

View File

@@ -177,3 +177,10 @@ func TestLoggerExpressionFilter(t *testing.T) {
assert.Equal(t, []string{"foo\n", "foo bar\n", "by filename\n"}, w1.FetchLogs())
}
func TestProtectSensitiveInfo(t *testing.T) {
assert.Empty(t, protectSensitiveInfo(""))
assert.Equal(t, "mailto:user@example.com", protectSensitiveInfo("mailto:user@example.com"))
assert.Equal(t, "https://example.com", protectSensitiveInfo("https://example.com"))
assert.Equal(t, "https://_masked_@example.com/path?k=_", protectSensitiveInfo("https://u:p@example.com/path?k=v#hash"))
}

View File

@@ -21,7 +21,33 @@ import (
// RegisterRenderers registers all supported third part renderers according settings
func RegisterRenderers() {
markup.RegisterRenderer(&openAPIRenderer{})
markup.RegisterRenderer(&frontendRenderer{
name: "openapi-swagger",
patterns: []string{
"openapi.yaml",
"openapi.yml",
"openapi.json",
"swagger.yaml",
"swagger.yml",
"swagger.json",
},
})
markup.RegisterRenderer(&frontendRenderer{
name: "viewer-3d",
patterns: []string{
// It needs more logic to make it overall right (render a text 3D model automatically):
// we need to distinguish the ambiguous filename extensions.
// For example: "*.amf, *.obj, *.off, *.step" might be or not be a 3D model file.
// So when it is a text file, we can't assume that "we only render it by 3D plugin",
// otherwise the end users would be impossible to view its real content when the file is not a 3D model.
"*.3dm", "*.3ds", "*.3mf", "*.amf", "*.bim", "*.brep",
"*.dae", "*.fbx", "*.fcstd", "*.glb", "*.gltf",
"*.ifc", "*.igs", "*.iges", "*.stp", "*.step",
"*.stl", "*.obj", "*.off", "*.ply", "*.wrl",
},
})
for _, renderer := range setting.ExternalMarkupRenderers {
markup.RegisterRenderer(&Renderer{renderer})
}

95
modules/markup/external/frontend.go vendored Normal file
View File

@@ -0,0 +1,95 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package external
import (
"encoding/base64"
"io"
"unicode/utf8"
"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
type frontendRenderer struct {
name string
patterns []string
}
var (
_ markup.PostProcessRenderer = (*frontendRenderer)(nil)
_ markup.ExternalRenderer = (*frontendRenderer)(nil)
)
func (p *frontendRenderer) Name() string {
return p.name
}
func (p *frontendRenderer) NeedPostProcess() bool {
return false
}
func (p *frontendRenderer) FileNamePatterns() []string {
// TODO: the file extensions are ambiguous, even if the file name matches, it doesn't mean that the file is a 3D model
// There are some approaches to make it more accurate, but they are all complicated:
// A. Make backend know everything (detect a file is a 3D model or not)
// B. Let frontend renders to try render one by one
//
// If there would be more frontend renders in the future, we need to implement the "frontend" approach:
// 1. Make backend or parent window collect the supported extensions of frontend renders (done: backend external render framework)
// 2. If the current file matches any extension, start the general iframe embedded render (done: this renderer)
// 3. The iframe window calls the frontend renders one by one (done: frontend external render)
// 4. Report the render result to parent by postMessage (TODO: when needed)
return p.patterns
}
func (p *frontendRenderer) SanitizerRules() []setting.MarkupSanitizerRule {
return nil
}
func (p *frontendRenderer) GetExternalRendererOptions() (ret markup.ExternalRendererOptions) {
ret.SanitizerDisabled = true
ret.DisplayInIframe = true
ret.ContentSandbox = "allow-scripts allow-forms allow-modals allow-popups allow-downloads"
return ret
}
func (p *frontendRenderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
if ctx.RenderOptions.StandalonePageOptions == nil {
opts := p.GetExternalRendererOptions()
return markup.RenderIFrame(ctx, &opts, output)
}
content, err := util.ReadWithLimit(input, int(setting.UI.MaxDisplayFileSize))
if err != nil {
return err
}
contentEncoding, contentString := "text", util.UnsafeBytesToString(content)
if !utf8.Valid(content) {
contentEncoding = "base64"
contentString = base64.StdEncoding.EncodeToString(content)
}
_, err = htmlutil.HTMLPrintf(output,
`<!DOCTYPE html>
<html>
<head>
<!-- external-render-helper will be injected here by the markup render -->
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id="frontend-render-viewer" data-frontend-renders="%s" data-file-tree-path="%s"></div>
<textarea id="frontend-render-data" data-content-encoding="%s" hidden>%s</textarea>
<script nonce type="module" src="%s"></script>
</body>
</html>`,
p.name, ctx.RenderOptions.RelativePath,
contentEncoding, contentString,
public.AssetURI("js/external-render-frontend.js"))
return err
}

View File

@@ -1,84 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package external
import (
"fmt"
"html"
"io"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
type openAPIRenderer struct{}
var (
_ markup.PostProcessRenderer = (*openAPIRenderer)(nil)
_ markup.ExternalRenderer = (*openAPIRenderer)(nil)
)
func (p *openAPIRenderer) Name() string {
return "openapi"
}
func (p *openAPIRenderer) NeedPostProcess() bool {
return false
}
func (p *openAPIRenderer) FileNamePatterns() []string {
return []string{
"openapi.yaml",
"openapi.yml",
"openapi.json",
"swagger.yaml",
"swagger.yml",
"swagger.json",
}
}
func (p *openAPIRenderer) SanitizerRules() []setting.MarkupSanitizerRule {
return nil
}
func (p *openAPIRenderer) GetExternalRendererOptions() (ret markup.ExternalRendererOptions) {
ret.SanitizerDisabled = true
ret.DisplayInIframe = true
ret.ContentSandbox = "allow-scripts allow-forms allow-modals allow-popups allow-downloads"
return ret
}
func (p *openAPIRenderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
if ctx.RenderOptions.StandalonePageOptions == nil {
opts := p.GetExternalRendererOptions()
return markup.RenderIFrame(ctx, &opts, output)
}
content, err := util.ReadWithLimit(input, int(setting.UI.MaxDisplayFileSize))
if err != nil {
return err
}
// HINT: SWAGGER-OPENAPI-VIEWER: another place "templates/swagger/openapi-viewer.tmpl"
_, err = io.WriteString(output, fmt.Sprintf(
`<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="%s">
</head>
<body>
<div id="swagger-ui"><textarea class="swagger-spec-content" data-spec-filename="%s">%s</textarea></div>
<script type="module" src="%s"></script>
</body>
</html>`,
public.AssetURI("css/swagger.css"),
html.EscapeString(ctx.RenderOptions.RelativePath),
html.EscapeString(util.UnsafeBytesToString(content)),
public.AssetURI("js/swagger.js"),
))
return err
}

View File

@@ -270,7 +270,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 template.HTML(template.HTMLEscapeString(content)), err
}
return template.HTML(buf.String()), nil
}

View File

@@ -6,6 +6,7 @@ package markup
import (
"bytes"
"context"
"errors"
"fmt"
"html/template"
"io"
@@ -43,7 +44,8 @@ type WebThemeInterface interface {
}
type StandalonePageOptions struct {
CurrentWebTheme WebThemeInterface
CurrentWebTheme WebThemeInterface
RenderQueryString string
}
type RenderOptions struct {
@@ -206,17 +208,23 @@ func RenderString(ctx *RenderContext, content string) (string, error) {
}
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"]
if ownerName == "" || repoName == "" || refSubURL == "" {
setting.PanicInDevOrTesting("RenderIFrame requires user, repo and RefTypeNameSubURL metas")
return errors.New("RenderIFrame requires user, repo and RefTypeNameSubURL metas")
}
src := fmt.Sprintf("%s/%s/%s/render/%s/%s", setting.AppSubURL,
url.PathEscape(ctx.RenderOptions.Metas["user"]),
url.PathEscape(ctx.RenderOptions.Metas["repo"]),
util.PathEscapeSegments(ctx.RenderOptions.Metas["RefTypeNameSubURL"]),
url.PathEscape(ownerName),
url.PathEscape(repoName),
ctx.RenderOptions.Metas["RefTypeNameSubURL"],
util.PathEscapeSegments(ctx.RenderOptions.RelativePath),
)
var extraAttrs template.HTML
if opts.ContentSandbox != "" {
extraAttrs = htmlutil.HTMLFormat(` sandbox="%s"`, opts.ContentSandbox)
}
_, err := htmlutil.HTMLPrintf(output, `<iframe data-src="%s" class="external-render-iframe"%s></iframe>`, src, extraAttrs)
_, err := htmlutil.HTMLPrintf(output, `<iframe data-src="%s" data-global-init="initExternalRenderIframe" class="external-render-iframe"%s></iframe>`, src, extraAttrs)
return err
}
@@ -228,7 +236,7 @@ func pipes() (io.ReadCloser, io.WriteCloser, func()) {
}
}
func getExternalRendererOptions(renderer Renderer) (ret ExternalRendererOptions, _ bool) {
func GetExternalRendererOptions(renderer Renderer) (ret ExternalRendererOptions, _ bool) {
if externalRender, ok := renderer.(ExternalRenderer); ok {
return externalRender.GetExternalRendererOptions(), true
}
@@ -237,7 +245,7 @@ func getExternalRendererOptions(renderer Renderer) (ret ExternalRendererOptions,
func RenderWithRenderer(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
var extraHeadHTML template.HTML
if extOpts, ok := getExternalRendererOptions(renderer); ok && extOpts.DisplayInIframe {
if extOpts, ok := GetExternalRendererOptions(renderer); ok && extOpts.DisplayInIframe {
if ctx.RenderOptions.StandalonePageOptions == nil {
// for an external "DisplayInIFrame" render, it could only output its content in a standalone page
// otherwise, a <iframe> should be outputted to embed the external rendered page
@@ -248,7 +256,12 @@ func RenderWithRenderer(ctx *RenderContext, renderer Renderer, input io.Reader,
extraLinkHref := ctx.RenderOptions.StandalonePageOptions.CurrentWebTheme.PublicAssetURI()
// "<script>" must go before "<link>", to make Golang's http.DetectContentType() can still recognize the content as "text/html"
// DO NOT use "type=module", the script must run as early as possible, to set up the environment in the iframe
extraHeadHTML = htmlutil.HTMLFormat(`<script crossorigin src="%s"></script><link rel="stylesheet" href="%s">`, extraScriptSrc, extraLinkHref)
extraHeadHTML = htmlutil.HTMLFormat(
`<script nonce crossorigin src="%s" id="gitea-external-render-helper" data-render-query-string="%s"></script>`+
`<link rel="stylesheet" href="%s">`,
extraScriptSrc, ctx.RenderOptions.StandalonePageOptions.RenderQueryString,
extraLinkHref,
)
}
ctx.usedByRender = true

View File

@@ -24,8 +24,8 @@ func TestRenderIFrame(t *testing.T) {
// the value is read from config RENDER_CONTENT_SANDBOX, empty means "disabled"
ret := render(ctx, ExternalRendererOptions{ContentSandbox: ""})
assert.Equal(t, `<iframe data-src="/test-owner/test-repo/render/src/branch/master/tree-path" class="external-render-iframe"></iframe>`, ret)
assert.Equal(t, `<iframe data-src="/test-owner/test-repo/render/src/branch/master/tree-path" data-global-init="initExternalRenderIframe" class="external-render-iframe"></iframe>`, ret)
ret = render(ctx, ExternalRendererOptions{ContentSandbox: "allow"})
assert.Equal(t, `<iframe data-src="/test-owner/test-repo/render/src/branch/master/tree-path" class="external-render-iframe" sandbox="allow"></iframe>`, ret)
assert.Equal(t, `<iframe data-src="/test-owner/test-repo/render/src/branch/master/tree-path" data-global-init="initExternalRenderIframe" class="external-render-iframe" sandbox="allow"></iframe>`, ret)
}

View File

@@ -40,5 +40,7 @@ type MigrateOptions struct {
MirrorInterval string `json:"mirror_interval"`
AWSAccessKeyID string
AWSSecretAccessKey string
AWSSecretAccessKey string `json:",omitempty"`
AWSSecretAccessKeyEncrypted string `json:"aws_secret_access_key_encrypted,omitempty"`
}

View File

@@ -47,6 +47,7 @@ type Metadata struct {
Keywords []string `json:"keywords,omitempty"`
RepositoryURL string `json:"repository_url,omitempty"`
License string `json:"license,omitempty"`
LicenseURL string `json:"license_url,omitempty"`
Author Person `json:"author"`
Manifests map[string]*Manifest `json:"manifests,omitempty"`
}
@@ -67,7 +68,8 @@ type SoftwareSourceCode struct {
Keywords []string `json:"keywords,omitempty"`
CodeRepository string `json:"codeRepository,omitempty"`
License string `json:"license,omitempty"`
Author Person `json:"author"`
LicenseURL string `json:"licenseURL,omitempty"`
Author *Person `json:"author,omitempty"`
ProgrammingLanguage ProgrammingLanguage `json:"programmingLanguage"`
RepositoryURLs []string `json:"repositoryURLs,omitempty"`
}
@@ -181,26 +183,31 @@ func ParsePackage(sr io.ReaderAt, size int64, mr io.Reader) (*Package, error) {
if err := json.NewDecoder(mr).Decode(&ssc); err != nil {
return nil, err
}
p.Metadata.Description = ssc.Description
p.Metadata.Keywords = ssc.Keywords
p.Metadata.License = ssc.License
author := Person{
Name: ssc.Author.Name,
GivenName: ssc.Author.GivenName,
MiddleName: ssc.Author.MiddleName,
FamilyName: ssc.Author.FamilyName,
p.Metadata.LicenseURL = ssc.LicenseURL
if ssc.Author != nil {
author := Person{
Name: ssc.Author.Name,
GivenName: ssc.Author.GivenName,
MiddleName: ssc.Author.MiddleName,
FamilyName: ssc.Author.FamilyName,
}
// If Name is not provided, generate it from individual name components
if author.Name == "" {
author.Name = author.String()
}
p.Metadata.Author = author
}
// If Name is not provided, generate it from individual name components
if author.Name == "" {
author.Name = author.String()
}
p.Metadata.Author = author
p.Metadata.RepositoryURL = ssc.CodeRepository
if !validation.IsValidURL(p.Metadata.RepositoryURL) {
p.Metadata.RepositoryURL = ""
}
if !validation.IsValidURL(p.Metadata.LicenseURL) {
p.Metadata.LicenseURL = ""
}
p.RepositoryURLs = ssc.RepositoryURLs
}

View File

@@ -4,11 +4,12 @@
package swift
import (
"archive/zip"
"bytes"
"strings"
"testing"
"code.gitea.io/gitea/modules/test"
"github.com/hashicorp/go-version"
"github.com/stretchr/testify/assert"
)
@@ -18,36 +19,24 @@ const (
packageVersion = "1.0.1"
packageDescription = "Package Description"
packageRepositoryURL = "https://gitea.io/gitea/gitea"
packageLicenseURL = "https://opensource.org/license/mit"
packageAuthor = "KN4CK3R"
packageLicense = "MIT"
)
func TestParsePackage(t *testing.T) {
createArchive := func(files map[string][]byte) *bytes.Reader {
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
for filename, content := range files {
w, _ := zw.Create(filename)
w.Write(content)
}
zw.Close()
return bytes.NewReader(buf.Bytes())
}
t.Run("MissingManifestFile", func(t *testing.T) {
data := createArchive(map[string][]byte{"dummy.txt": {}})
p, err := ParsePackage(data, data.Size(), nil)
data := test.WriteZipArchive(map[string]string{"dummy.txt": ""})
p, err := ParsePackage(bytes.NewReader(data.Bytes()), int64(data.Len()), nil)
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrMissingManifestFile)
})
t.Run("ManifestFileTooLarge", func(t *testing.T) {
data := createArchive(map[string][]byte{
"Package.swift": make([]byte, maxManifestFileSize+1),
data := test.WriteZipArchive(map[string]string{
"Package.swift": strings.Repeat("a", maxManifestFileSize+1),
})
p, err := ParsePackage(data, data.Size(), nil)
p, err := ParsePackage(bytes.NewReader(data.Bytes()), int64(data.Len()), nil)
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrManifestFileTooLarge)
})
@@ -56,12 +45,12 @@ func TestParsePackage(t *testing.T) {
content1 := "// swift-tools-version:5.7\n//\n// Package.swift"
content2 := "// swift-tools-version:5.6\n//\n// Package@swift-5.6.swift"
data := createArchive(map[string][]byte{
"Package.swift": []byte(content1),
"Package@swift-5.5.swift": []byte(content2),
data := test.WriteZipArchive(map[string]string{
"Package.swift": content1,
"Package@swift-5.5.swift": content2,
})
p, err := ParsePackage(data, data.Size(), nil)
p, err := ParsePackage(bytes.NewReader(data.Bytes()), int64(data.Len()), nil)
assert.NotNil(t, p)
assert.NoError(t, err)
@@ -77,14 +66,13 @@ func TestParsePackage(t *testing.T) {
})
t.Run("WithMetadata", func(t *testing.T) {
data := createArchive(map[string][]byte{
"Package.swift": []byte("// swift-tools-version:5.7\n//\n// Package.swift"),
data := test.WriteZipArchive(map[string]string{
"Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift",
})
p, err := ParsePackage(
data,
data.Size(),
strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","keywords":["swift","package"],"license":"`+packageLicense+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`),
bytes.NewReader(data.Bytes()), int64(data.Len()),
strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","keywords":["swift","package"],"license":"`+packageLicense+`","licenseURL":"`+packageLicenseURL+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`),
)
assert.NotNil(t, p)
assert.NoError(t, err)
@@ -97,6 +85,7 @@ func TestParsePackage(t *testing.T) {
assert.Equal(t, packageDescription, p.Metadata.Description)
assert.ElementsMatch(t, []string{"swift", "package"}, p.Metadata.Keywords)
assert.Equal(t, packageLicense, p.Metadata.License)
assert.Equal(t, packageLicenseURL, p.Metadata.LicenseURL)
assert.Equal(t, packageAuthor, p.Metadata.Author.Name)
assert.Equal(t, packageAuthor, p.Metadata.Author.GivenName)
assert.Equal(t, packageRepositoryURL, p.Metadata.RepositoryURL)
@@ -104,14 +93,13 @@ func TestParsePackage(t *testing.T) {
})
t.Run("WithExplicitNameField", func(t *testing.T) {
data := createArchive(map[string][]byte{
"Package.swift": []byte("// swift-tools-version:5.7\n//\n// Package.swift"),
data := test.WriteZipArchive(map[string]string{
"Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift",
})
authorName := "John Doe"
p, err := ParsePackage(
data,
data.Size(),
bytes.NewReader(data.Bytes()), int64(data.Len()),
strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","author":{"name":"`+authorName+`","givenName":"John","familyName":"Doe"}}`),
)
assert.NotNil(t, p)
@@ -122,15 +110,30 @@ func TestParsePackage(t *testing.T) {
assert.Equal(t, "Doe", p.Metadata.Author.FamilyName)
})
t.Run("WithEmptyJSONMetadata", func(t *testing.T) {
data := test.WriteZipArchive(map[string]string{
"Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift",
})
p, err := ParsePackage(
bytes.NewReader(data.Bytes()), int64(data.Len()),
strings.NewReader(`{}`),
)
assert.NotNil(t, p)
assert.NoError(t, err)
assert.NotNil(t, p.Metadata)
assert.Empty(t, p.Metadata.Author.Name)
assert.Empty(t, p.RepositoryURLs)
})
t.Run("NameFieldGeneration", func(t *testing.T) {
data := createArchive(map[string][]byte{
"Package.swift": []byte("// swift-tools-version:5.7\n//\n// Package.swift"),
data := test.WriteZipArchive(map[string]string{
"Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift",
})
// Test with only individual name components - Name should be auto-generated
p, err := ParsePackage(
data,
data.Size(),
bytes.NewReader(data.Bytes()), int64(data.Len()),
strings.NewReader(`{"author":{"givenName":"John","middleName":"Q","familyName":"Doe"}}`),
)
assert.NotNil(t, p)

View File

@@ -56,6 +56,8 @@ func parseManifest(data []byte) (map[string]string, map[string]string) {
paths[key] = entry.File
names[entry.File] = entry.Name
// Map associated CSS files, e.g. "css/index.css" -> "css/index.B3zrQPqD.css"
// FIXME: INCORRECT-VITE-MANIFEST-PARSER: the logic is wrong, Vite manifest doesn't work this way
// It just happens to be correct for the current modules dependencies
for _, css := range entry.CSS {
cssKey := path.Dir(css) + "/" + entry.Name + path.Ext(css)
paths[cssKey] = css

View File

@@ -12,10 +12,11 @@ import (
"code.gitea.io/gitea/modules/log"
)
const IncomingEmailTokenPlaceholder = "%{token}"
var IncomingEmail = struct {
Enabled bool
ReplyToAddress string
TokenPlaceholder string `ini:"-"`
Host string
Port int
UseTLS bool `ini:"USE_TLS"`
@@ -28,7 +29,6 @@ var IncomingEmail = struct {
}{
Mailbox: "INBOX",
DeleteHandledMessage: true,
TokenPlaceholder: "%{token}",
MaximumMessageSize: 10485760,
}
@@ -54,19 +54,10 @@ func checkReplyToAddress() error {
return errors.New("name must not be set")
}
c := strings.Count(IncomingEmail.ReplyToAddress, IncomingEmail.TokenPlaceholder)
switch c {
case 0:
return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmail.TokenPlaceholder)
case 1:
default:
return fmt.Errorf("%s must appear only once", IncomingEmail.TokenPlaceholder)
placeholderCount := strings.Count(IncomingEmail.ReplyToAddress, IncomingEmailTokenPlaceholder)
userPart, _, _ := strings.Cut(IncomingEmail.ReplyToAddress, "@")
if placeholderCount != 1 || !strings.Contains(userPart, IncomingEmailTokenPlaceholder) {
return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmailTokenPlaceholder)
}
parts := strings.Split(IncomingEmail.ReplyToAddress, "@")
if !strings.Contains(parts[0], IncomingEmail.TokenPlaceholder) {
return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmail.TokenPlaceholder)
}
return nil
}

View File

@@ -18,6 +18,12 @@ const (
RepoCreatingPublic = "public"
)
// enumerates the values for [repository.pull-request] DEFAULT_TITLE_SOURCE
const (
RepoPRTitleSourceFirstCommit = "first-commit"
RepoPRTitleSourceAuto = "auto"
)
// ItemsPerPage maximum items per page in forks, watchers and stars of a repo
const ItemsPerPage = 40
@@ -89,6 +95,7 @@ var (
RetargetChildrenOnMerge bool
DelayCheckForInactiveDays int
DefaultDeleteBranchAfterMerge bool
DefaultTitleSource string
} `ini:"repository.pull-request"`
// Issue Setting
@@ -213,6 +220,7 @@ var (
RetargetChildrenOnMerge bool
DelayCheckForInactiveDays int
DefaultDeleteBranchAfterMerge bool
DefaultTitleSource string
}{
WorkInProgressPrefixes: []string{"WIP:", "[WIP]"},
// Same as GitHub. See
@@ -229,6 +237,7 @@ var (
AddCoCommitterTrailers: true,
RetargetChildrenOnMerge: true,
DelayCheckForInactiveDays: 7,
DefaultTitleSource: RepoPRTitleSourceAuto,
},
// Issue settings

View File

@@ -31,6 +31,7 @@ var (
ReverseProxyAuthEmail string
ReverseProxyAuthFullName string
ReverseProxyLimit int
ReverseProxyLogoutRedirect string
ReverseProxyTrustedProxies []string
MinPasswordLength int
ImportLocalPaths bool
@@ -124,6 +125,7 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
ReverseProxyAuthFullName = sec.Key("REVERSE_PROXY_AUTHENTICATION_FULL_NAME").MustString("X-WEBAUTH-FULLNAME")
ReverseProxyLimit = sec.Key("REVERSE_PROXY_LIMIT").MustInt(1)
ReverseProxyLogoutRedirect = sec.Key("REVERSE_PROXY_LOGOUT_REDIRECT").String()
ReverseProxyTrustedProxies = sec.Key("REVERSE_PROXY_TRUSTED_PROXIES").Strings(",")
if len(ReverseProxyTrustedProxies) == 0 {
ReverseProxyTrustedProxies = []string{"127.0.0.0/8", "::1/128"}

View File

@@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/user"
"code.gitea.io/gitea/modules/util"
)
// settings
@@ -163,32 +164,38 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
func loadRunModeFrom(rootCfg ConfigProvider) {
rootSec := rootCfg.Section("")
mustNotRunAsRoot(rootSec)
runModeValue := os.Getenv("GITEA_RUN_MODE")
runModeValue = util.IfZero(runModeValue, rootSec.Key("RUN_MODE").String())
// non-dev mode is treated as prod mode, to protect users from accidentally running in dev mode if there is a typo in this value.
IsProd = !strings.EqualFold(runModeValue, "dev") // TODO: can use case-sensitive comparing in the future
RunMode = util.Iif(IsProd, "prod", "dev")
// there is a separate check: mustCurrentRunUserMatch (IsRunUserMatchCurrentUser)
RunUser = rootSec.Key("RUN_USER").MustString(user.CurrentUsername())
}
func mustNotRunAsRoot(rootSec ConfigSection) {
if os.Getuid() != 0 {
return
}
mustRunAsRoot := os.Getenv("SNAP") != "" && os.Getenv("SNAP_NAME") != "" // snap container runs the app as uid=0
if mustRunAsRoot {
return
}
// The following is a purposefully undocumented option. Please do not run Gitea as root. It will only cause future headaches.
// Please don't use root as a bandaid to "fix" something that is broken, instead the broken thing should instead be fixed properly.
unsafeAllowRunAsRoot := ConfigSectionKeyBool(rootSec, "I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")
unsafeAllowRunAsRoot = unsafeAllowRunAsRoot || optional.ParseBool(os.Getenv("GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")).Value()
RunMode = os.Getenv("GITEA_RUN_MODE")
if RunMode == "" {
RunMode = rootSec.Key("RUN_MODE").MustString("prod")
}
allowRunAsRoot := ConfigSectionKeyBool(rootSec, "I_AM_BEING_UNSAFE_RUNNING_AS_ROOT") || // check gitea config
optional.ParseBool(os.Getenv("GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")).Value() // check gitea env var
// non-dev mode is treated as prod mode, to protect users from accidentally running in dev mode if there is a typo in this value.
RunMode = strings.ToLower(RunMode)
if RunMode != "dev" {
RunMode = "prod"
}
IsProd = RunMode != "dev"
// check if we run as root
if os.Getuid() == 0 {
if !unsafeAllowRunAsRoot {
// Special thanks to VLC which inspired the wording of this messaging.
log.Fatal("Gitea is not supposed to be run as root. Sorry. If you need to use privileged TCP ports please instead use setcap and the `cap_net_bind_service` permission")
}
log.Critical("You are running Gitea using the root user, and have purposely chosen to skip built-in protections around this. You have been warned against this.")
if !allowRunAsRoot {
// Special thanks to VLC which inspired the wording of this messaging.
log.Fatal("Gitea is not supposed to be run as root. If you need to use privileged TCP ports please instead use `setcap` and the `cap_net_bind_service` permission.")
}
log.Warn("You are running Gitea using the root user, and have purposely chosen to skip built-in protections around this. You have been warned against this.")
}
// HasInstallLock checks the install-lock in ConfigProvider directly, because sometimes the config file is not loaded into setting variables yet.
@@ -201,7 +208,7 @@ func mustCurrentRunUserMatch(rootCfg ConfigProvider) {
if HasInstallLock(rootCfg) {
currentUser, match := IsRunUserMatchCurrentUser(RunUser)
if !match {
log.Fatal("Expect user '%s' but current user is: %s", RunUser, currentUser)
log.Fatal("Expect user '%s' (RUN_USER in app.ini) but current user is: %s", RunUser, currentUser)
}
}
}

View File

@@ -5,6 +5,7 @@ package templates
import (
"html/template"
"net/url"
"strings"
"testing"
@@ -169,9 +170,21 @@ func TestQueryBuild(t *testing.T) {
})
}
const queryNonASCII = " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" // all non-letter & non-number chars
func TestQueryEscape(t *testing.T) {
// this test is a reference for "urlQueryEscape" in JS
in := "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" // all non-letter & non-number chars
expected := "%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F%3A%3B%3C%3D%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~"
assert.Equal(t, expected, string(queryEscape(in)))
// Special case for space encoding:
// * RFC 3986: Uniform Resource Identifier (URI): %20
// * WHATWG HTML: application/x-www-form-urlencoded: +
// * JavaScript: encodeURIComponent() uses "%20". URLSearchParams uses "+"
// * Golang: QueryEscape uses "+"
expected := "+%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F%3A%3B%3C%3D%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~"
assert.Equal(t, expected, url.QueryEscape(queryNonASCII))
}
func TestPathEscape(t *testing.T) {
// this test is a reference for "pathEscape" in JS
expected := "%20%21%22%23$%25&%27%28%29%2A+%2C-.%2F:%3B%3C=%3E%3F@%5B%5C%5D%5E_%60%7B%7C%7D~"
assert.Equal(t, expected, url.PathEscape(queryNonASCII))
}

View File

@@ -5,6 +5,7 @@ package test
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"io"
@@ -97,6 +98,17 @@ func WriteTarArchive(files map[string]string) *bytes.Buffer {
return WriteTarCompression(func(w io.Writer) io.WriteCloser { return util.NopCloser{Writer: w} }, files)
}
func WriteZipArchive(files map[string]string) *bytes.Buffer {
buf := &bytes.Buffer{}
zw := zip.NewWriter(buf)
for name, content := range files {
w, _ := zw.Create(name)
_, _ = w.Write([]byte(content))
}
_ = zw.Close()
return buf
}
func WriteTarCompression[F func(io.Writer) io.WriteCloser | func(io.Writer) (io.WriteCloser, error)](compression F, files map[string]string) *bytes.Buffer {
buf := &bytes.Buffer{}
var cw io.WriteCloser

View File

@@ -5,7 +5,8 @@ package util
import (
"bytes"
"unicode"
"net"
"strings"
)
type sanitizedError struct {
@@ -25,48 +26,103 @@ func SanitizeErrorCredentialURLs(err error) error {
return sanitizedError{err: err}
}
const userPlaceholder = "sanitized-credential"
var schemeSep = []byte("://")
// SanitizeCredentialURLs remove all credentials in URLs (starting with "scheme://") for the input string: "https://user:pass@domain.com" => "https://sanitized-credential@domain.com"
const userInfoPlaceholder = "(masked)"
// SanitizeCredentialURLs remove all credentials in URLs for the input string:
// * "https://userinfo@domain.com" => "https://***@domain.com"
// * "user:pass@domain.com" => "***@domain.com"
// "***" is a magic string internally used, doesn't guarantee to be anything.
func SanitizeCredentialURLs(s string) string {
sepColPos := strings.Index(s, ":")
if sepColPos == -1 {
return s // fast path: no colon, unlikely contain any URL credential
}
sepAtPos := strings.Index(s[sepColPos+1:], "@")
for sepAtPos == -1 {
return s // fast path: no "@" after colon, unlikely contain any URL credential
}
sepAtPos += sepColPos + 1
res := make([]byte, 0, len(s)+len(userInfoPlaceholder)) // a best guess to avoid too many re-allocations
bs := UnsafeStringToBytes(s)
schemeSepPos := bytes.Index(bs, schemeSep)
if schemeSepPos == -1 || bytes.IndexByte(bs[schemeSepPos:], '@') == -1 {
return s // fast return if there is no URL scheme or no userinfo
}
out := make([]byte, 0, len(bs)+len(userPlaceholder))
for schemeSepPos != -1 {
schemeSepPos += 3 // skip the "://"
sepAtPos := -1 // the possible '@' position: "https://foo@[^here]host"
sepEndPos := schemeSepPos // the possible end position: "The https://host[^here] in log for test"
sepLoop:
for ; sepEndPos < len(bs); sepEndPos++ {
c := bs[sepEndPos]
if ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') || ('0' <= c && c <= '9') {
continue
}
for {
// left part (before "@") is likely to be the "userinfo" (single username, or "username:password")
leftPos := sepAtPos - 1
leftLoop:
for leftPos >= 0 {
c := bs[leftPos]
switch c {
case '@':
sepAtPos = sepEndPos
case '-', '.', '_', '~', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '%':
continue // due to RFC 3986, userinfo can contain - . _ ~ ! $ & ' ( ) * + , ; = : and any percent-encoded chars
// RFC 3986, userinfo can contain - . _ ~ ! $ & ' ( ) * + , ; = : and any percent-encoded chars
default:
break sepLoop // if it is an invalid char for URL (eg: space, '/', and others), stop the loop
valid := 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9'
if !valid {
break leftLoop
}
}
leftPos--
}
// if there is '@', and the string is like "s://u@h", then hide the "u" part
if sepAtPos != -1 && (schemeSepPos >= 4 && unicode.IsLetter(rune(bs[schemeSepPos-4]))) && sepAtPos-schemeSepPos > 0 && sepEndPos-sepAtPos > 0 {
out = append(out, bs[:schemeSepPos]...)
out = append(out, userPlaceholder...)
out = append(out, bs[sepAtPos:sepEndPos]...)
// left pos should point to the beginning of the left part, this pos is always valid in the buffer
leftPos++
// right part is likely to be the host (domain name, ip address)
rightPos := sepAtPos + 1
rightLoop:
for rightPos < len(bs) {
c := bs[rightPos]
switch c {
case '.', '-':
// valid host char
case '[':
// ipv6 begin
if rightPos != sepAtPos+1 {
break rightLoop
}
case ']':
// ipv6 end
rightPos++
break rightLoop
default:
valid := 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9'
if bs[sepAtPos+1] == '[' {
// ipv6 host
valid = 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' || '0' <= c && c <= '9' || c == ':'
}
if !valid {
break rightLoop
}
}
rightPos++
}
leading, leftPart, rightPart := bs[:leftPos], bs[leftPos:sepAtPos], bs[sepAtPos+1:rightPos]
// Either:
// * git log message: "user:pass@host" (it contains a colon in userinfo), ignore "git@host" pattern
// * http like URL: "https://userinfo@host.com" (it has "://" before the userinfo)
needSanitize := bytes.IndexByte(leftPart, ':') >= 0 || bytes.HasSuffix(leading, schemeSep)
needSanitize = needSanitize && len(leftPart) > 0 && len(rightPart) > 0
// TODO: can also do more checks for right part
// for example: ipv6 quick check
if needSanitize && rightPart[0] == '[' {
needSanitize = rightPart[len(rightPart)-1] == ']' && net.ParseIP(UnsafeBytesToString(rightPart[1:len(rightPart)-1])) != nil
}
if needSanitize {
res = append(res, leading...)
res = append(res, userInfoPlaceholder...)
res = append(res, '@')
res = append(res, rightPart...)
} else {
out = append(out, bs[:sepEndPos]...)
res = append(res, bs[:rightPos]...)
}
bs = bs[rightPos:]
sepAtPos = bytes.IndexByte(bs, '@')
if sepAtPos == -1 {
break
}
bs = bs[sepEndPos:]
schemeSepPos = bytes.Index(bs, schemeSep)
}
out = append(out, bs...)
return UnsafeBytesToString(out)
res = append(res, bs...)
return UnsafeBytesToString(res)
}

View File

@@ -13,7 +13,7 @@ import (
func TestSanitizeErrorCredentialURLs(t *testing.T) {
err := errors.New("error with https://a@b.com")
se := SanitizeErrorCredentialURLs(err)
assert.Equal(t, "error with https://"+userPlaceholder+"@b.com", se.Error())
assert.Equal(t, "error with https://"+userInfoPlaceholder+"@b.com", se.Error())
}
func TestSanitizeCredentialURLs(t *testing.T) {
@@ -27,15 +27,35 @@ func TestSanitizeCredentialURLs(t *testing.T) {
},
{
"https://mytoken@github.com/go-gitea/test_repo.git",
"https://" + userPlaceholder + "@github.com/go-gitea/test_repo.git",
"https://" + userInfoPlaceholder + "@github.com/go-gitea/test_repo.git",
},
{
"https://user:password@github.com/go-gitea/test_repo.git",
"https://" + userPlaceholder + "@github.com/go-gitea/test_repo.git",
"https://" + userInfoPlaceholder + "@github.com/go-gitea/test_repo.git",
},
{
"https://user:password@[::]/go-gitea/test_repo.git",
"https://" + userInfoPlaceholder + "@[::]/go-gitea/test_repo.git",
},
{
"https://user:password@[2001:db8::1]:8080/go-gitea/test_repo.git",
"https://" + userInfoPlaceholder + "@[2001:db8::1]:8080/go-gitea/test_repo.git",
},
{
"see https://u:p@[::1]/x and https://u2:p2@h2",
"see https://" + userInfoPlaceholder + "@[::1]/x and https://" + userInfoPlaceholder + "@h2",
},
{
"https://user:secret@[unclosed-ipv6",
"https://user:secret@[unclosed-ipv6",
},
{
"https://user:secret@[invalid-ipv6]",
"https://user:secret@[invalid-ipv6]",
},
{
"ftp://x@",
"ftp://" + userPlaceholder + "@",
"ftp://x@",
},
{
"ftp://x/@",
@@ -43,27 +63,40 @@ func TestSanitizeCredentialURLs(t *testing.T) {
},
{
"ftp://u@x/@", // test multiple @ chars
"ftp://" + userPlaceholder + "@x/@",
"ftp://" + userInfoPlaceholder + "@x/@",
},
{
"😊ftp://u@x😊", // test unicode
"😊ftp://" + userPlaceholder + "@x😊",
"😊ftp://" + userInfoPlaceholder + "@x😊",
},
{
"://@",
"://@",
},
{
"//u:p@h", // do not process URLs without explicit scheme, they are not treated as "valid" URLs because there is no scheme context in string
"//u:p@h",
"//" + userInfoPlaceholder + "@h",
},
{
"s://u@h", // the minimal pattern to be sanitized
"s://" + userPlaceholder + "@h",
"s://u@h",
"s://" + userInfoPlaceholder + "@h",
},
{
"URLs in log https://u:b@h and https://u:b@h:80/, with https://h.com and u@h.com",
"URLs in log https://" + userPlaceholder + "@h and https://" + userPlaceholder + "@h:80/, with https://h.com and u@h.com",
"URLs in log https://" + userInfoPlaceholder + "@h and https://" + userInfoPlaceholder + "@h:80/, with https://h.com and u@h.com",
},
{
"fatal: unable to look up username:token@github.com (port 9418)",
"fatal: unable to look up " + userInfoPlaceholder + "@github.com (port 9418)",
},
{
"git failed for user:token@github.com/go-gitea/test_repo.git",
"git failed for " + userInfoPlaceholder + "@github.com/go-gitea/test_repo.git",
},
{
// SSH-form git URL ("git@host:path") must not let a later credential URL through
"failed remote git@github.com:foo, retried via https://user:tok@github.com/foo",
"failed remote git@github.com:foo, retried via https://" + userInfoPlaceholder + "@github.com/foo",
},
}

View File

@@ -255,11 +255,13 @@ func EnumValue[T comparable](val EnumConst[T]) (ret T, valid bool) {
return enums[0], false
}
func ReserveLineBreakForTextarea(input string) string {
func NormalizeStringEOL(input string) string {
// Since the content is from a form which is a textarea, the line endings are \r\n.
// It's a standard behavior of HTML.
// But we want to store them as \n like what GitHub does.
// And users are unlikely to really need to keep the \r.
// But in most cases, we only want "\n" for EOL
// * Text files: use "\n" by default because "\r\n" sometimes doesn't work in POSIX
// * Actions values: store them as "\n" like what GitHub does.
// And users are unlikely to really need the "\r".
// Other than this, we should respect the original content, even leading or trailing spaces.
return strings.ReplaceAll(input, "\r\n", "\n")
return UnsafeBytesToString(NormalizeEOL(UnsafeStringToBytes(input)))
}

View File

@@ -175,9 +175,9 @@ func TestToTitleCase(t *testing.T) {
assert.Equal(t, `Foo Bar Baz`, ToTitleCase(`FOO BAR BAZ`))
}
func TestReserveLineBreakForTextarea(t *testing.T) {
assert.Equal(t, "test\ndata", ReserveLineBreakForTextarea("test\r\ndata"))
assert.Equal(t, "test\ndata\n", ReserveLineBreakForTextarea("test\r\ndata\r\n"))
func TestNormalizeStringEOL(t *testing.T) {
assert.Equal(t, "test\ndata", NormalizeStringEOL("test\r\ndata"))
assert.Equal(t, " test\ndata\n ", NormalizeStringEOL(" test\rdata\r "))
}
func TestOptionalArg(t *testing.T) {
@@ -192,3 +192,10 @@ func TestOptionalArg(t *testing.T) {
assert.Equal(t, 42, bar(nil))
assert.Equal(t, 100, bar(nil, 100))
}
func TestPathEscapeSegments(t *testing.T) {
assert.Equal(t, "a", PathEscapeSegments("a"))
assert.Equal(t, "a/b", PathEscapeSegments("a/b"))
assert.Equal(t, "a/b%20c", PathEscapeSegments("a/b c"))
assert.Equal(t, "a/b+c", PathEscapeSegments("a/b+c"))
}

View File

@@ -5,12 +5,14 @@ package validation
import (
"fmt"
"io"
"regexp"
"strings"
"code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/glob"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/util"
"gitea.com/go-chi/binding"
@@ -31,8 +33,23 @@ const (
ErrInvalidBadgeSlug = "InvalidBadgeSlug"
)
type jsonProvider struct{}
func (j jsonProvider) Marshal(v any) ([]byte, error) { return json.Marshal(v) }
func (j jsonProvider) Unmarshal(data []byte, v any) error { return json.Unmarshal(data, v) }
func (j jsonProvider) NewDecoder(reader io.Reader) binding.JSONDecoder {
return json.NewDecoder(reader)
}
func (j jsonProvider) NewEncoder(writer io.Writer) binding.JSONEncoder {
return json.NewEncoder(writer)
}
// AddBindingRules adds additional binding rules
func AddBindingRules() {
binding.JSONProvider = jsonProvider{}
addGitRefNameBindingRule()
addValidURLListBindingRule()
addValidURLBindingRule()

View File

@@ -269,7 +269,7 @@
"install.lfs_path": "Git LFS Root Path",
"install.lfs_path_helper": "Files tracked by Git LFS will be stored in this directory. Leave empty to disable.",
"install.run_user": "Run As Username",
"install.run_user_helper": "The operating system username that Gitea runs as. Note that this user must have access to the repository root path.",
"install.run_user_helper": "The operating system username that Gitea runs as, it must have write access to the data paths. This value is auto-detected and cannot be changed here. To use a different user, restart Gitea under that account.",
"install.domain": "Server Domain",
"install.domain_helper": "Domain or host address for the server.",
"install.ssh_port": "SSH Server Port",
@@ -316,7 +316,6 @@
"install.invalid_db_table": "The database table \"%s\" is invalid: %v",
"install.invalid_repo_path": "The repository root path is invalid: %v",
"install.invalid_app_data_path": "The app data path is invalid: %v",
"install.run_user_not_match": "The 'run as' username is not the current username: %s -> %s",
"install.internal_token_failed": "Failed to generate internal token: %v",
"install.secret_key_failed": "Failed to generate secret key: %v",
"install.save_config_failed": "Failed to save configuration: %v",
@@ -638,14 +637,8 @@
"user.block.unblock.failure": "Failed to unblock user: %s",
"user.block.blocked": "You have blocked this user.",
"user.block.title": "Block a user",
"user.block.info": "Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.",
"user.block.info_1": "Blocking a user prevents the following actions on your account and your repositories:",
"user.block.info_2": "following your account",
"user.block.info_3": "send you notifications by @mentioning your username",
"user.block.info_4": "inviting you as a collaborator to their repositories",
"user.block.info_5": "starring, forking or watching on repositories",
"user.block.info_6": "opening and commenting on issues or pull requests",
"user.block.info_7": "reacting to your comments in issues or pull requests",
"user.block.info": "Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues.",
"user.block.info.docs": "Learn more about blocking a user.",
"user.block.user_to_block": "User to block",
"user.block.note": "Note",
"user.block.note.title": "Optional note:",
@@ -1788,6 +1781,7 @@
"repo.pulls.review_only_possible_for_full_diff": "Review is only possible when viewing the full diff",
"repo.pulls.filter_changes_by_commit": "Filter by commit",
"repo.pulls.nothing_to_compare": "These branches are equal. There is no need to create a pull request.",
"repo.pulls.no_common_history": "These branches do not share a common merge base. Select a different base or compare branch.",
"repo.pulls.nothing_to_compare_have_tag": "The selected branches/tags are equal.",
"repo.pulls.nothing_to_compare_and_allow_empty_pr": "These branches are equal. This PR will be empty.",
"repo.pulls.has_pull_request": "A pull request between these branches already exists: <a href=\"%[1]s\">%[2]s#%[3]d</a>",
@@ -3625,7 +3619,13 @@
"packages.terraform.delete.latest": "The latest version of a Terraform state cannot be deleted.",
"packages.vagrant.install": "To add a Vagrant box, run the following command:",
"packages.settings.link": "Link this package to a repository",
"packages.settings.link.description": "If you link a package with a repository, the package will appear in the repository's package list. Only repositories under the same owner can be linked. Leaving the field empty will remove the link.",
"packages.settings.link.description": "If you link a package with a repository, the package will appear in the repository's package list.",
"packages.settings.link.notice1": "Only repositories under the same owner can be linked.",
"packages.settings.link.notice2": "Linking a repository does not change the package visibility.",
"packages.settings.link.notice3": "Leaving the field empty will remove the link.",
"packages.settings.visibility": "Package visibility",
"packages.settings.visibility.inherit": "Package visibility is inherited from the owner and cannot be changed independently here. To change it, update the visibility settings of the user or organization that owns this package.",
"packages.settings.visibility.button": "Change owner visibility",
"packages.settings.link.select": "Select Repository",
"packages.settings.link.button": "Update Repository Link",
"packages.settings.link.success": "Repository link was successfully updated.",

View File

@@ -25,7 +25,7 @@
"@github/paste-markdown": "1.5.3",
"@github/text-expander-element": "2.9.4",
"@lezer/highlight": "1.2.3",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@mcaptcha/vanilla-glue": "0.1.0-rc2",
"@mermaid-js/layout-elk": "0.2.1",
"@primer/octicons": "19.23.1",
"@replit/codemirror-indentation-markers": "6.5.3",
@@ -54,12 +54,12 @@
"jquery": "4.0.0",
"js-yaml": "4.1.1",
"katex": "0.16.44",
"mermaid": "11.14.0",
"mermaid": "11.15.0",
"online-3d-viewer": "0.18.0",
"pdfobject": "2.3.1",
"perfect-debounce": "2.1.0",
"postcss": "8.5.8",
"rollup-plugin-license": "3.7.0",
"rolldown-license-plugin": "2.2.0",
"sortablejs": "1.15.7",
"swagger-ui-dist": "5.32.1",
"tailwindcss": "3.4.19",
@@ -73,8 +73,7 @@
"vite-string-plugin": "2.0.2",
"vue": "3.5.31",
"vue-bar-graph": "2.2.0",
"vue-chartjs": "5.3.3",
"wrap-ansi": "10.0.0"
"vue-chartjs": "5.3.3"
},
"devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "4.7.1",

1230
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -161,13 +161,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.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 {

View File

@@ -264,7 +264,16 @@ func (s *Service) UpdateLog(
}
ack := task.LogLength
if len(req.Msg.Rows) == 0 || req.Msg.Index > ack || int64(len(req.Msg.Rows))+req.Msg.Index <= ack {
// Trim rows the runner already had acked.
var rows []*runnerv1.LogRow
if req.Msg.Index <= ack && int64(len(req.Msg.Rows))+req.Msg.Index > ack {
rows = req.Msg.Rows[ack-req.Msg.Index:]
}
// Bail unless we have new rows or a NoMore to finalize. Even with
// NoMore, bail when the runner has outrun the server — archiving a
// log with a gap is worse than asking it to retry.
if len(rows) == 0 && (!req.Msg.NoMore || req.Msg.Index > ack) {
res.Msg.AckIndex = ack
return res, nil
}
@@ -273,7 +282,9 @@ func (s *Service) UpdateLog(
return nil, status.Errorf(codes.AlreadyExists, "log file has been archived")
}
rows := req.Msg.Rows[ack-req.Msg.Index:]
// WriteLogs is called even with no rows: with offset==0 it bootstraps
// an empty DBFS file so TransferLogs below has something to read when
// the runner finalizes a task that produced no log output.
ns, err := actions.WriteLogs(ctx, task.LogFilename, task.LogSize, rows)
if err != nil {
return nil, status.Errorf(codes.Internal, "unable to append logs to dbfs file: %v", err)

View File

@@ -9,7 +9,10 @@ import (
"time"
packages_model "code.gitea.io/gitea/models/packages"
access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/modules/log"
composer_module "code.gitea.io/gitea/modules/packages/composer"
"code.gitea.io/gitea/services/context"
)
// ServiceIndexResponse contains registry endpoints
@@ -91,7 +94,7 @@ type Source struct {
Reference string `json:"reference"`
}
func createPackageMetadataResponse(registryURL string, pds []*packages_model.PackageDescriptor) *PackageMetadataResponse {
func createPackageMetadataResponse(ctx *context.Context, registryURL string, pds []*packages_model.PackageDescriptor) *PackageMetadataResponse {
versions := make([]*PackageVersionMetadata, 0, len(pds))
for _, pd := range pds {
@@ -116,10 +119,15 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
},
}
if pd.Repository != nil {
pkg.Source = Source{
URL: pd.Repository.HTMLURL(),
Type: "git",
Reference: pd.Version.Version,
permission, err := access_model.GetDoerRepoPermission(ctx, pd.Repository, ctx.Doer)
if err != nil {
log.Error("GetDoerRepoPermission[%d]: %v", pd.Repository.ID, err)
} else if permission.HasAnyUnitAccessOrPublicAccess() {
pkg.Source = Source{
URL: pd.Repository.HTMLURL(),
Type: "git",
Reference: pd.Version.Version,
}
}
}

View File

@@ -146,6 +146,7 @@ func PackageMetadata(ctx *context.Context) {
}
resp := createPackageMetadataResponse(
ctx,
setting.AppURL+"api/packages/"+ctx.Package.Owner.Name+"/composer",
pds,
)

View File

@@ -27,6 +27,7 @@ import (
container_module "code.gitea.io/gitea/modules/packages/container"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
auth_service "code.gitea.io/gitea/services/auth"
@@ -125,8 +126,15 @@ func APIUnauthorizedError(ctx *context.Context) {
// container registry requires that the "/v2" must be in the root, so the sub-path in AppURL should be removed
realmURL := httplib.GuessCurrentHostURL(ctx) + "/v2/token"
ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+realmURL+`",service="container_registry",scope="*"`)
// support apple container like: container registry login <gitea-host> -u
ctx.Resp.Header().Add("WWW-Authenticate", `Basic realm="Gitea Container Registry"`)
ownerName := ctx.PathParam("username")
owner, _ := user_model.GetUserByName(ctx, ownerName)
requireSignIn := owner != nil && owner.Visibility != structs.VisibleTypePublic
requireSignIn = requireSignIn || setting.Service.RequireSignInViewStrict
if requireSignIn {
// support apple container like: container registry login <gitea-host> -u
ctx.Resp.Header().Add("WWW-Authenticate", `Basic realm="Gitea Container Registry"`)
}
apiErrorDefined(ctx, errUnauthorized)
}

View File

@@ -9,6 +9,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
@@ -220,30 +221,38 @@ func UploadPackageFile(ctx *context.Context) {
func DownloadPackageFile(ctx *context.Context) {
name := ctx.PathParam("name")
version := ctx.PathParam("version")
architecture := ctx.PathParam("architecture")
group := ctx.PathParam("group")
s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion(
ctx,
&packages_service.PackageInfo{
Owner: ctx.Package.Owner,
PackageType: packages_model.TypeRpm,
Name: name,
Version: version,
},
&packages_service.PackageFileInfo{
Filename: fmt.Sprintf("%s-%s.%s.rpm", name, version, ctx.PathParam("architecture")),
CompositeKey: ctx.PathParam("group"),
},
ctx.Req.Method,
)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
apiError(ctx, http.StatusNotFound, err)
} else {
apiError(ctx, http.StatusInternalServerError, err)
}
return
openForDownload := func(filename string) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) {
return packages_service.OpenFileForDownloadByPackageNameAndVersion(
ctx,
&packages_service.PackageInfo{
Owner: ctx.Package.Owner,
PackageType: packages_model.TypeRpm,
Name: name,
Version: version,
},
&packages_service.PackageFileInfo{
Filename: filename,
CompositeKey: group,
},
ctx.Req.Method,
)
}
s, u, pf, err := openForDownload(fmt.Sprintf("%s-%s.%s.rpm", name, version, architecture))
if errors.Is(err, util.ErrNotExist) && architecture != "noarch" {
s, u, pf, err = openForDownload(fmt.Sprintf("%s-%s.%s.rpm", name, version, "noarch"))
}
if errors.Is(err, util.ErrNotExist) {
apiError(ctx, http.StatusNotFound, err)
return
} else if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
helper.ServePackageFile(ctx, s, u, pf)
}

View File

@@ -198,6 +198,23 @@ func PackageVersionMetadata(ctx *context.Context) {
}
metadata := pd.Metadata.(*swift_module.Metadata)
repositoryURLs := make([]string, 0, len(pd.VersionProperties))
for _, property := range pd.VersionProperties {
if property.Name == swift_module.PropertyRepositoryURL {
repositoryURLs = append(repositoryURLs, property.Value)
}
}
var author *swift_module.Person
if metadata.Author.Name != "" || metadata.Author.GivenName != "" || metadata.Author.MiddleName != "" || metadata.Author.FamilyName != "" {
author = &swift_module.Person{
Type: "Person",
Name: metadata.Author.Name,
GivenName: metadata.Author.GivenName,
MiddleName: metadata.Author.MiddleName,
FamilyName: metadata.Author.FamilyName,
}
}
setResponseHeaders(ctx.Resp, &headers{})
@@ -220,18 +237,14 @@ func PackageVersionMetadata(ctx *context.Context) {
Keywords: metadata.Keywords,
CodeRepository: metadata.RepositoryURL,
License: metadata.License,
LicenseURL: metadata.LicenseURL,
Author: author,
ProgrammingLanguage: swift_module.ProgrammingLanguage{
Type: "ComputerLanguage",
Name: "Swift",
URL: "https://swift.org",
},
Author: swift_module.Person{
Type: "Person",
Name: metadata.Author.String(),
GivenName: metadata.Author.GivenName,
MiddleName: metadata.Author.MiddleName,
FamilyName: metadata.Author.FamilyName,
},
RepositoryURLs: repositoryURLs,
},
})
}

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) {
@@ -958,6 +978,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).
@@ -966,7 +988,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() {
@@ -1014,8 +1036,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)
@@ -1047,7 +1070,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() {
@@ -1065,7 +1088,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() {
@@ -1076,7 +1099,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() {
@@ -1084,13 +1107,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() {
@@ -1101,22 +1124,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)
@@ -1125,8 +1148,8 @@ func Routes() *web.Router {
m.Put("", user.BlockUser)
m.Delete("", user.UnblockUser)
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
})
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
}, rejectPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken(), contextAuthenticatedUser(), checkTokenPublicOnly())
// Repositories (requires repo scope, org scope)
m.Post("/org/{org}/repos",
@@ -1426,9 +1449,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)
@@ -1597,7 +1620,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)

View File

@@ -33,6 +33,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)
@@ -192,7 +193,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)
@@ -201,13 +202,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
@@ -487,6 +491,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 {

View File

@@ -6,7 +6,6 @@ package repo
import (
go_context "context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
@@ -23,7 +22,6 @@ import (
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/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
@@ -1770,11 +1768,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 {

View File

@@ -62,13 +62,20 @@ func CompareDiff(ctx *context.APIContext) {
apiCommits := make([]*api.Commit, 0, len(compareInfo.Commits))
userCache := make(map[string]*user_model.User)
for i := 0; i < len(compareInfo.Commits); i++ {
apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, compareInfo.Commits[i], userCache,
apiCommit, err := convert.ToCommit(
ctx,
compareInfo.HeadRepo,
compareInfo.HeadGitRepo,
compareInfo.Commits[i],
userCache,
convert.ToCommitOptions{
Stat: true,
Verification: verification,
Files: files,
})
},
)
if err != nil {
ctx.APIErrorInternal(err)
return

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 {

View File

@@ -22,6 +22,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
@@ -993,7 +994,7 @@ func MergePullRequest(ctx *context.APIContext) {
return
}
if strings.Contains(err.Error(), "Wrong commit ID") {
ctx.JSON(http.StatusConflict, err)
ctx.APIError(http.StatusConflict, err)
return
}
ctx.APIErrorInternal(err)
@@ -1429,7 +1430,11 @@ func GetPullRequestCommits(ctx *context.APIContext) {
} else {
compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefNameFromBranch(pr.BaseBranch), git.RefName(pr.GetGitHeadRefName()), false, false)
}
if err != nil {
if gitcmd.StderrHasPrefix(err, "fatal: bad revision") {
ctx.APIError(http.StatusNotFound, "invalid base branch or revision")
return
} else if err != nil {
ctx.APIErrorInternal(err)
return
}

View File

@@ -131,9 +131,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),
@@ -149,6 +146,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"))
@@ -567,6 +565,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 {
@@ -1254,6 +1256,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 {

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 {

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