Compare commits

...
100 Commits
Author SHA1 Message Date
Lunny XiaoandGitHub bb4cccc6e9 fix: parse HEAD ref (#38119)
backport #38088
2026-06-14 19:52:44 +00:00
98cc15b307 fix(api): nil pointer panic when filtering tracked times by a non-existent user (#38112) (#38115)
Backport #38112

## Problem

`GET /repos/{owner}/{repo}/times` and `GET
/repos/{owner}/{repo}/issues/{index}/times` crash with a nil pointer
dereference when the `user` query filter names a user that does not
exist.

## Root cause

In `ListTrackedTimes` and `ListTrackedTimesByRepository`, the
`IsErrUserNotExist` branch sends the 404 but is missing a `return`, so
execution falls through to `opts.UserID = user.ID` with a nil `user`.

---------

Co-authored-by: Pycub <iamsokhandan@gmail.com>
2026-06-14 20:06:04 +02:00
9b8bfdceb1 fix: bound debian ParseControlFile to a single control stanza (#38044) (#38055)
Backport #38044 by @metsw24-max

**Packages-index stanza injection via Debian control file**

A `.deb` whose `control` file appends extra paragraphs after a blank
line was still accepted, and `ParseControlFile` stored the whole
multi-stanza blob in `p.Control`. That blob is re-emitted verbatim into
the generated `Packages` index, so the embedded blank line splits it
into separate stanzas and an uploader can smuggle a package entry with
an attacker-chosen `Filename` into the shared index. A binary control
file only holds one stanza, so parsing now stops at the blank line that
terminates it; well-formed packages are unaffected and the new subtest
covers the trailing-stanza case.

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: metsw24-max <metsw24@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: bircni <bircni@icloud.com>
2026-06-14 14:29:27 +00:00
GiteabotandGitHub e156ac8063 fix: keep literal "false" value displayed in workflow_dispatch choice dropdowns (#38080) (#38096) 2026-06-13 11:56:35 +02:00
4751adf42d feat(api): add Link header in ListForks (#38052) (#38063)
Backport #38052
Fixes #38051.

Signed-off-by: Eugenio Paolantonio <eugenio.paolantonio@suse.com>
Co-authored-by: Eugenio Paolantonio <me@medesimo.eu>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-06-12 18:21:46 +00:00
3bebddedc0 fix: git cmd (#38084) (#38087)
Backport #38084

fix #37370

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-06-12 21:28:13 +08:00
GiteabotandGitHub 617b6948b1 fix(hostmatcher): block reserved IP ranges from external/private filters (#38039) (#38059) 2026-06-10 12:05:23 +02:00
GiteabotandGitHub 1c7b7ea72d fix(lfs): require Code-unit access for cross-repo LFS object reuse (#38006) (#38050) 2026-06-10 09:32:28 +02:00
e107498f3b fix(actions)!: require merged PR to bypass fork PR approval gate (#38010) (#38041)
Backport #38010 by @bircni

`ifNeedApproval` in `services/actions/notifier_helper.go` decided
whether a
fork PR's workflow run had to wait for maintainer approval. The bypass
clause
counted any prior `approved_by > 0` run for `(repo_id,
trigger_user_id)`, so
the very first Approve-and-run click on a contributor's fork PR
permanently
trusted that user for every future fork PR in the same repository —
including
PRs whose only change is the workflow YAML itself.

Approving a workflow *run* is not the same as merging *code*. This
change
aligns the gate with GitHub Actions' first-time-contributor model: trust
is
granted only after the user has had a pull request merged in the repo.

## Behavior change

- **Before**: one approval = permanent trust for that user in that repo.
- **After**: every fork PR is gated until the contributor has at least
one
  merged PR in the repo.

Existing already-approved runs and merged PRs continue to work; only the
trust criterion for *future* fork PRs changes. Maintainers who rely on
the
implicit "approve once" trust will see the approval banner reappear
until
they merge a PR from that contributor.

---------

Signed-off-by: bircni <bircni@icloud.com>
Co-authored-by: bircni <bircni@icloud.com>
2026-06-09 15:53:42 +02:00
3b705738ab fix: bound CODEOWNERS regex match time (#38011) (#38025)
Backport #38011 by @bircni

User-supplied CODEOWNERS patterns were compiled without a match timeout,
so a crafted pattern (e.g. (a+)+) against a crafted file path could
backtrack for tens of seconds inside the PR creation transaction and
exhaust the database connection pool. Set MatchTimeout on each compiled
rule; the caller already treats match errors as non-matches.

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-06-07 16:40:34 +00:00
GiteabotandGitHub 8f4b7ebbf6 fix(lfs): reject unknown SSH LFS sub-verbs to prevent auth bypass (#38008) (#38015) 2026-06-06 22:07:06 +02:00
603c8ece00 fix(releases): generate notes for initial tag (#37697) (#37986)
Backport #37697

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

Automatic release notes for the first release in a repository were empty
when there was no previous tag.

Before this change, the release notes generator used the tag name to
build the changelog link, but reused that state for pull request
collection. When `PreviousTag` was empty, the PR collection logic did
not scan a useful commit range, so merged pull requests were omitted
from the generated notes.

This pull request fixes that by decoupling the internal PR collection
range from the rendered changelog link:
- when a previous tag exists, behavior stays unchanged
- when no previous tag exists, release notes collect merged pull
requests from the full reachable history up to the target tag
- the displayed full changelog link for the first release still uses the
existing `/commits/tag/{tag}` format

Tests were updated to cover:
- generating notes for a repository with no previous tags
- including merged pull requests before the first tag
- preserving existing behavior when a previous tag exists

<!--
Before submitting:
- Target the `main` branch; release branches are for backports only.
- Use a Conventional Commits title, e.g. `fix(repo): handle empty branch
names`.
- Read the contributing guidelines:
https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md
- Documentation changes go to https://gitea.com/gitea/docs

Describe your change below and link any issue it fixes.
-->

Co-authored-by: OpenAI GPT-5.5 <openai-gpt-5.5@users.noreply.github.com>
Co-authored-by: Giteabot <teabot@gitea.io>
2026-06-05 19:13:16 +00:00
4a19964921 fix(actions): return 404 when job log blob is missing (#38003) (#38004)
Backport #38003 by @bircni

- When the `action_task` row exists but the underlying dbfs/storage blob
is gone, `OpenLogs` returns a wrapped `os.ErrNotExist` which surfaces as
a 500 on the job logs endpoints.
- Translate it to the same `util.NewNotExistErrorf` shape already used
for unknown job ids / expired logs, so both the API
(`/api/v1/repos/.../actions/jobs/<id>/logs`) and the web download
handler return a clean 404 instead.

Fixes #37990.

Co-authored-by: bircni <bircni@icloud.com>
2026-06-05 18:42:15 +00:00
38711f2696 fix(actions): exclude workflow_call from workflow trigger detection (#37894) (#37899)
Backport #37894 by @Zettat123

Gitea now only allows `workflow_dispatch.inputs`. If a workflow contains
`workflow_call.inputs`, the workflow cannot be triggered, even though
the `on:` section contains other trigger events.


https://github.com/go-gitea/gitea/blob/428ee9fcce7928bf5405900345d43e9ba1b01564/modules/actions/jobparser/model.go#L402-L405

For example, this workflow cannot be triggered due to
`workflow_call.inputs`:
```yaml
on:
  push:
  pull_request:
  workflow_call:
    inputs:
      name:
        type: string
```

---

This PR is extracted from #37478 for backport

Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.8) <noreply@anthropic.com>
2026-05-29 06:40:50 +00:00
64ad4bb0ff fix(actions): keep action run title clickable when commit subject is a URL (#37867) (#37898)
Backport #37867 by @bircni

- When a commit subject is a bare URL, `linkProcessor` wrapped it in its
own `<a>` to that URL. Because HTML cannot nest anchors, the wrapping
default link (the action run / commit link) was lost and the action
title became unclickable — clicking it sent the user to the URL from the
commit message instead of the action log.
- Drop `linkProcessor` from `PostProcessCommitMessageSubject` so the
whole subject stays wrapped in the default link. URLs in subjects now
render as text inside that link; URLs in commit bodies are unaffected.

Fixes #37865

Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-05-29 07:59:47 +02:00
8bf445e86a fix(actions): reject workflow_dispatch for workflows without that trigger (#37660) (#37895)
Backport #37660 by @jorgeortiz85

## Summary

Fixes #37528

This PR makes the workflow dispatch API reject workflows that do not
declare `workflow_dispatch`. Previously, `POST
/repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches` could
create an `ActionRun` for a workflow that only declared another event
such as `push`.

The service now validates that the target workflow has a
`workflow_dispatch` trigger before inserting the run. The API maps that
validation failure to `422 Unprocessable Entity`, matching existing
validation failures in this handler.

The regression test creates a push-only workflow, dispatches it through
the public API, asserts the `workflow_dispatch` validation message, and
verifies that no run was inserted.

## Testing

- `go test ./services/actions`
- `TAGS="sqlite sqlite_unlock_notify" make
test-integration#TestWorkflowDispatchPublicApiRequiresWorkflowDispatchTrigger`
- `TAGS="sqlite sqlite_unlock_notify" make
test-integration#TestWorkflowDispatchPublicApi`

## Disclosure

Developed with assistance from OpenAI Codex.

Co-authored-by: Jorge Ortiz <jorge.ortiz@gmail.com>
Co-authored-by: Nicolas <bircni@icloud.com>
2026-05-29 05:09:08 +02:00
094eeee365 fix(actions): ack re-sent UpdateLog finalize idempotently (#37885) (#37892)
Backport #37885 by @silverwind

Fixes https://github.com/go-gitea/gitea/issues/37871, full backwards and
forwards compatible with runners.

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-05-28 03:52:50 +00:00
cc3ee01fd8 fix: http content file render (#37850) (#37856)
Backport #37850

Fix #37849

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
2026-05-26 06:41:47 +00:00
c044b0f48c fix(deps): update module golang.org/x/net to v0.55.0 [security] (#37813) (#37829)
Backport #37813

Co-authored-by: Giteabot <teabot@gitea.io>
Co-authored-by: silverwind <me@silverwind.io>
2026-05-24 06:07:36 +00:00
10fc85e263 ci: add tools/ci-tools.ts for the PR labeler workflow (#37831)
Backport `./tools/ci-tools.ts` to 1.26 which is needed for the
`pr-title` flow to succeed on that branch.

Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
2026-05-24 07:36:20 +02:00
bb6ca9da4d fix(issues): clear stale ReviewTypeRequest when submitting pending re… (#37809) (#37815)
Backport #37809 by @Powerscore

When SubmitReview updates an existing pending review in-place, it was
not deleting the reviewer's ReviewTypeRequest row, unlike the
CreateReview path. That leftover row causes AddReviewRequest to bail out
silently, making the re-request icon in the PR sidebar a no-op.

Fixes #37808

 (Claude Opus 4.7)

<!--
Before submitting:
- Target the `main` branch; release branches are for backports only.
- Use a Conventional Commits title, e.g. `fix(repo): handle empty branch
names`.
- Read the contributing guidelines:
https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md
- Documentation changes go to https://gitea.com/gitea/docs

Describe your change below and link any issue it fixes.
-->

Co-authored-by: Alaa Abdelwahab <82750565+Powerscore@users.noreply.github.com>
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-22 20:43:03 +02:00
53877583f0 fix(build): swagger css import (#37801) (#37803)
Backport #37801 by @lunny

Snap build failure caused by missed swagger ui css file.

```
:: [plugin vite:css] /build/gitea/parts/gitea/build/web_src/css/swagger-standalone.css:undefined:NaN
:: Error: [postcss] ENOENT: no such file or directory, open '../../node_modules/swagger-ui-dist/swagger-ui.css'
```

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-05-21 16:48:16 +02:00
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 XiaoandGitHub f540f57354 update changelog for 1.26.2 (#37577) 2026-05-20 17:24:06 +00:00
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
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
wxiaoguangandGitHub d37f7b44a9 fix(markup): make RenderString never fail (#37779) (#37780)
Backport #37779
2026-05-19 18:08:11 +00:00
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
GiteabotandGitHub 6d2b02dac1 fix(permissions): Fix reading permission (#37769) (#37781) 2026-05-19 17:06:09 +02:00
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
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
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
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
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
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
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
Zettat123andGitHub 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
GiteabotandGitHub 2965b0c08a fix(web): enforce token scopes on raw, media, and attachment downloads (#37698) (#37733) 2026-05-16 18:18:44 +02:00
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
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
Zettat123andGitHub 7b82ded82a fix(actions): deadlock between PrepareRunAndInsert and UpdateTaskByState (#37692) (#37718) 2026-05-16 07:02:14 +02:00
wxiaoguangandGitHub 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
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
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
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
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
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
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
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
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
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
GiteabotandGitHub 439984474c chore(deps): update dependency go to v1.26.3 (#37601) (#37613) 2026-05-08 19:40:49 +00:00
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
GiteabotandGitHub 65f3feaa84 fix: treat email addresses case-insensitively (#37600) (#37611) 2026-05-08 18:32:25 +00:00
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
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
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
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
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
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
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
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
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
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
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
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
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
GiteabotandGitHub 8a49e9d346 Fix mCaptcha broken after Vite migration (#37492) (#37509) 2026-05-02 18:20:52 +02:00
NicolasandGitHub b88bad2a01 Fix basic auth bug (#37503)
Backport for #37486
2026-05-02 10:58:40 +00:00
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
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
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
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
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
wxiaoguangandGitHub fb3c1b031d Add CurrentURL template variable back (#37444) (#37449)
Backport #37444
2026-04-27 21:05:24 +08:00
wxiaoguangandGitHub cff6eb5661 Make GetPossibleUserByID can handle deleted user (#37430) (#37431)
Backport #37430
2026-04-27 00:33:09 +08:00
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
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
afdbd9b7c5 change log for 1.26.1 (#37357)
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-04-24 12:40:36 -07:00
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
269 changed files with 6471 additions and 1533 deletions
+84
View File
@@ -4,6 +4,90 @@ 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
+14 -9
View File
@@ -113,23 +113,25 @@ func handleCliResponseExtra(extra private.ResponseExtra) error {
return nil
}
func getAccessMode(verb, lfsVerb string) perm.AccessMode {
// getAccessMode maps an SSH git/LFS verb to the access mode it requires, with
// ok=false for an unrecognised verb. Callers MUST reject the request when ok is
// false: AccessModeNone would otherwise pass the `userMode < mode` permission
// check in routers/private/serv.go and grant access.
func getAccessMode(verb, lfsVerb string) (mode perm.AccessMode, ok bool) {
switch verb {
case git.CmdVerbUploadPack, git.CmdVerbUploadArchive:
return perm.AccessModeRead
return perm.AccessModeRead, true
case git.CmdVerbReceivePack:
return perm.AccessModeWrite
return perm.AccessModeWrite, true
case git.CmdVerbLfsAuthenticate, git.CmdVerbLfsTransfer:
switch lfsVerb {
case git.CmdSubVerbLfsUpload:
return perm.AccessModeWrite
return perm.AccessModeWrite, true
case git.CmdSubVerbLfsDownload:
return perm.AccessModeRead
return perm.AccessModeRead, true
}
}
// should be unreachable
setting.PanicInDevOrTesting("unknown verb: %s %s", verb, lfsVerb)
return perm.AccessModeNone
return perm.AccessModeNone, false
}
func runServ(ctx context.Context, c *cli.Command) error {
@@ -247,7 +249,10 @@ func runServ(ctx context.Context, c *cli.Command) error {
}
}
requestedMode := getAccessMode(verb, lfsVerb)
requestedMode, ok := getAccessMode(verb, lfsVerb)
if !ok {
return fail(ctx, "Unknown git command", "Unknown git command %s %s", verb, lfsVerb)
}
results, extra := private.ServCommand(ctx, keyID, username, reponame, requestedMode, verb, lfsVerb)
if extra.HasError() {
+56
View File
@@ -0,0 +1,56 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"testing"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/modules/git"
"github.com/stretchr/testify/assert"
)
func TestGetAccessMode(t *testing.T) {
cases := []struct {
verb, lfsVerb string
expected perm.AccessMode
}{
{git.CmdVerbUploadPack, "", perm.AccessModeRead},
{git.CmdVerbUploadArchive, "", perm.AccessModeRead},
{git.CmdVerbReceivePack, "", perm.AccessModeWrite},
{git.CmdVerbLfsAuthenticate, git.CmdSubVerbLfsUpload, perm.AccessModeWrite},
{git.CmdVerbLfsAuthenticate, git.CmdSubVerbLfsDownload, perm.AccessModeRead},
{git.CmdVerbLfsTransfer, git.CmdSubVerbLfsUpload, perm.AccessModeWrite},
{git.CmdVerbLfsTransfer, git.CmdSubVerbLfsDownload, perm.AccessModeRead},
}
for _, tc := range cases {
t.Run(tc.verb+"/"+tc.lfsVerb, func(t *testing.T) {
mode, ok := getAccessMode(tc.verb, tc.lfsVerb)
assert.True(t, ok)
assert.Equal(t, tc.expected, mode)
})
}
}
// TestGetAccessModeUnknownVerb locks in the invariant that getAccessMode reports
// ok=false for unrecognised verbs and LFS sub-verbs, so runServ rejects them. An
// unknown verb has no valid access mode; if it were treated as AccessModeNone (0)
// it would pass the `userMode < mode` permission check in routers/private/serv.go
// and hand out valid LFS JWTs for any private repository.
func TestGetAccessModeUnknownVerb(t *testing.T) {
cases := []struct{ verb, lfsVerb string }{
{git.CmdVerbLfsAuthenticate, ""},
{git.CmdVerbLfsAuthenticate, "badverb"},
{git.CmdVerbLfsTransfer, "badverb"},
{"git-unknown-verb", ""},
}
for _, tc := range cases {
t.Run(tc.verb+"/"+tc.lfsVerb, func(t *testing.T) {
mode, ok := getAccessMode(tc.verb, tc.lfsVerb)
assert.False(t, ok)
assert.Equal(t, perm.AccessModeNone, mode)
})
}
}
+5
View File
@@ -1161,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
+4 -2
View File
@@ -570,8 +570,6 @@ export default defineConfig([
'no-redeclare': [0], // must be disabled for typescript overloads
'no-regex-spaces': [2],
'no-restricted-exports': [0],
'no-restricted-globals': [2, ...restrictedGlobals],
'no-restricted-properties': [2, ...restrictedProperties],
'no-restricted-imports': [2, {paths: [
{name: 'jquery', message: 'Use the global $ instead', allowTypeImports: true},
{name: 'htmx.org', message: 'Use the global htmx instead', allowTypeImports: true},
@@ -1024,5 +1022,9 @@ export default defineConfig([
{
files: ['web_src/**/*'],
languageOptions: {globals: {...globals.browser, ...globals.jquery, htmx: false}},
rules: {
'no-restricted-globals': [2, ...restrictedGlobals],
'no-restricted-properties': [2, ...restrictedProperties],
},
},
]);
+11 -12
View File
@@ -1,6 +1,6 @@
module code.gitea.io/gitea
go 1.26.2
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:
@@ -51,8 +51,8 @@ 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.18.0
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
@@ -110,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/image v0.38.0
golang.org/x/net v0.52.0
golang.org/x/crypto v0.52.0
golang.org/x/image v0.40.0
golang.org/x/net v0.55.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.45.0
golang.org/x/text v0.37.0
google.golang.org/grpc v1.79.3
google.golang.org/protobuf v1.36.11
gopkg.in/ini.v1 v1.67.1
@@ -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
)
+24 -24
View File
@@ -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.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM=
github.com/go-git/go-git/v5 v5.18.0/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,12 +785,12 @@ 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/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/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
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.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8=
golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -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.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
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.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.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.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
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.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
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=
+30 -40
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
}
-4
View File
@@ -70,7 +70,6 @@ type FindRunOptions struct {
Ref string // the commit/tag/… that caused this workflow
TriggerUserID int64
TriggerEvent webhook_module.HookEventType
Approved bool // not util.OptionalBool, it works only when it's true
Status []Status
ConcurrencyGroup string
CommitSHA string
@@ -87,9 +86,6 @@ func (opts FindRunOptions) ToConds() builder.Cond {
if opts.TriggerUserID > 0 {
cond = cond.And(builder.Eq{"`action_run`.trigger_user_id": opts.TriggerUserID})
}
if opts.Approved {
cond = cond.And(builder.Gt{"`action_run`.approved_by": 0})
}
if len(opts.Status) > 0 {
cond = cond.And(builder.In("`action_run`.status", opts.Status))
}
+1 -2
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)
+7 -9
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 ||
+7
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
+1 -1
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
}
}
+67 -39
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
+80 -25
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) {
+1 -1
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
+5 -2
View File
@@ -196,7 +196,10 @@ func LFSObjectAccessible(ctx context.Context, user *user_model.User, oid string)
count, err := db.GetEngine(ctx).Count(&LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}})
return count > 0, err
}
cond := repo_model.AccessibleRepositoryCondition(user, unit.TypeInvalid)
// LFS objects are repository code content, so authorization must require
// Code-unit access; other unit accesses (e.g. Issues) must not authorize
// reuse of an existing LFS object across repositories.
cond := repo_model.AccessibleRepositoryCondition(user, unit.TypeCode)
count, err := db.GetEngine(ctx).Where(cond).Join("INNER", "repository", "`lfs_meta_object`.repository_id = `repository`.id").Count(&LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}})
return count > 0, err
}
@@ -220,7 +223,7 @@ func LFSAutoAssociate(ctx context.Context, metas []*LFSMetaObject, user *user_mo
newMetas := make([]*LFSMetaObject, 0, len(metas))
cond := builder.In(
"`lfs_meta_object`.repository_id",
builder.Select("`repository`.id").From("repository").Where(repo_model.AccessibleRepositoryCondition(user, unit.TypeInvalid)),
builder.Select("`repository`.id").From("repository").Where(repo_model.AccessibleRepositoryCondition(user, unit.TypeCode)),
)
if err := db.GetEngine(ctx).Cols("oid").Where(cond).In("oid", oids...).GroupBy("oid").Find(&newMetas); err != nil {
return err
+1 -10
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
}
+3 -10
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
}
+8
View File
@@ -10,6 +10,7 @@ import (
"fmt"
"io"
"strings"
"time"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
@@ -860,6 +861,11 @@ func GetCodeOwnersFromContent(ctx context.Context, data string) ([]*CodeOwnerRul
return rules, warnings
}
// codeOwnerMatchTimeout bounds a single pattern match so a crafted pattern
// cannot stall via catastrophic backtracking. See also the aggregate budget
// enforced by the caller across the whole rules×files match loop.
const codeOwnerMatchTimeout = 150 * time.Millisecond
type CodeOwnerRule struct {
Rule *regexp2.Regexp // it supports negative lookahead, does better for end users
Negative bool
@@ -888,6 +894,8 @@ func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule,
warnings = append(warnings, fmt.Sprintf("incorrect codeowner regexp: %s", err))
return nil, warnings
}
// Bound matching time so user-supplied patterns cannot stall PR creation via catastrophic backtracking.
rule.Rule.MatchTimeout = codeOwnerMatchTimeout
for _, user := range tokens[1:] {
user = strings.TrimPrefix(user, "@")
+52 -21
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
+87 -8
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
})
}
+19
View File
@@ -4,7 +4,9 @@
package issues_test
import (
"strings"
"testing"
"time"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
@@ -39,6 +41,7 @@ func TestPullRequest(t *testing.T) {
t.Run("DeleteOrphanedObjects", testDeleteOrphanedObjects)
t.Run("ParseCodeOwnersLine", testParseCodeOwnersLine)
t.Run("CodeOwnerAbsolutePathPatterns", testCodeOwnerAbsolutePathPatterns)
t.Run("CodeOwnerPatternMatchTimeout", testCodeOwnerPatternMatchTimeout)
t.Run("GetApprovers", testGetApprovers)
t.Run("GetPullRequestByMergedCommit", testGetPullRequestByMergedCommit)
t.Run("Migrate_InsertPullRequests", testMigrateInsertPullRequests)
@@ -376,6 +379,22 @@ func testCodeOwnerAbsolutePathPatterns(t *testing.T) {
}
}
// testCodeOwnerPatternMatchTimeout ensures user-supplied CODEOWNERS patterns
// cannot stall pull request processing through catastrophic regex backtracking:
// each compiled rule must enforce a bounded match time.
func testCodeOwnerPatternMatchTimeout(t *testing.T) {
rules, _ := issues_model.GetCodeOwnersFromContent(t.Context(), "(a+)+ @user5\n")
require.Len(t, rules, 1)
maliciousInput := strings.Repeat("a", 30) + "X"
start := time.Now()
_, err := rules[0].Rule.MatchString(maliciousInput)
elapsed := time.Since(start)
require.Error(t, err, "expected MatchTimeout error on pathological input")
assert.Less(t, elapsed, time.Second, "match timeout did not bound regex evaluation; took %s", elapsed)
}
func testGetApprovers(t *testing.T) {
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 5})
// Official reviews are already deduplicated. Allow unofficial reviews
+9 -9
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
}
@@ -491,6 +483,14 @@ func SubmitReview(ctx context.Context, doer *user_model.User, issue *Issue, revi
if _, err := sess.ID(review.ID).Cols("content, type, official, commit_id, stale").Update(review); err != nil {
return nil, nil, err
}
// make sure the leftover review request is cleared, consistent with CreateReview
if reviewType != ReviewTypePending {
if _, err := sess.Where(builder.Eq{"reviewer_id": doer.ID, "issue_id": issue.ID, "type": ReviewTypeRequest}).
Delete(new(Review)); err != nil {
return nil, nil, err
}
}
}
comm, err := CreateComment(ctx, &CreateCommentOptions{
+40
View File
@@ -303,6 +303,46 @@ func TestDeleteDismissedReview(t *testing.T) {
unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: comment.ID})
}
func TestSubmitReviewClearsStaleReviewRequest(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
assert.NoError(t, issue.LoadRepo(t.Context()))
assert.NoError(t, issue.Repo.LoadOwner(t.Context()))
reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// the reviewer is requested to review the pull request
requestReview, err := issues_model.CreateReview(t.Context(), issues_model.CreateReviewOptions{
Type: issues_model.ReviewTypeRequest,
Issue: issue,
Reviewer: reviewer,
})
assert.NoError(t, err)
// the reviewer starts a pending review (e.g. by adding code comments)
pendingReview, err := issues_model.CreateReview(t.Context(), issues_model.CreateReviewOptions{
Type: issues_model.ReviewTypePending,
Issue: issue,
Reviewer: reviewer,
})
assert.NoError(t, err)
// submitting the pending review must clear the leftover review request,
// otherwise the reviewer can no longer be re-requested afterwards
review, _, err := issues_model.SubmitReview(t.Context(), reviewer, issue, issues_model.ReviewTypeComment, "looks good", "", false, nil)
assert.NoError(t, err)
assert.Equal(t, pendingReview.ID, review.ID)
assert.Equal(t, issues_model.ReviewTypeComment, review.Type)
unittest.AssertNotExistsBean(t, &issues_model.Review{ID: requestReview.ID})
// the reviewer can be re-requested afterwards (no-op before the fix)
comment, err := issues_model.AddReviewRequest(t.Context(), issue, reviewer, doer, false)
assert.NoError(t, err)
assert.NotNil(t, comment)
}
func TestAddReviewRequest(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
+6
View File
@@ -54,6 +54,12 @@ type FindOrgOptions struct {
IncludeVisibility structs.VisibleType
}
func (opts *FindOrgOptions) ApplyPublicOnly(publicOnly bool) {
if publicOnly {
opts.IncludeVisibility = structs.VisibleTypePublic
}
}
func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder {
cond := builder.Eq{"uid": userID}
if !includePrivate {
-46
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
+1 -1
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
+2 -12
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
+7
View File
@@ -212,6 +212,13 @@ type SearchRepoOptions struct {
OnlyShowRelevant bool
}
func (opts *SearchRepoOptions) ApplyPublicOnly(publicOnly bool) {
if publicOnly {
opts.Private = false
opts.AllLimited = false
}
}
// UserOwnedRepoCond returns user ownered repositories
func UserOwnedRepoCond(userID int64) builder.Cond {
return builder.Eq{
+17 -11
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...)
+34 -4
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")
+1 -1
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)
+6
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)
+37 -51
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
}
+8 -4
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)
}
+20
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.
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestBuildSignature(t *testing.T) {
a := BuildSignature("v0", "x")
b := BuildSignature("v0", "x")
assert.Equal(t, a, b)
a = BuildSignature("v0", "x", "yz")
b = BuildSignature("v0", "xy", "z")
assert.NotEqual(t, a, b)
a = BuildSignature("v1", "x")
b = BuildSignature("v2", "x")
assert.NotEqual(t, a, b)
a = BuildSignature("v0", "x")
b = BuildSignature("v0x")
assert.NotEqual(t, a, b)
a = BuildSignature("v0", "", "x")
b = BuildSignature("v0", "x", "")
assert.NotEqual(t, a, b)
a = BuildSignature("v0")
b = BuildSignature("v0")
assert.Equal(t, a, b)
}
+3
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],
@@ -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)
})
})
}
}
+12
View File
@@ -298,6 +298,9 @@ func toGitContext(input map[string]any) *model.GithubContext {
return gitContext
}
// workflowCallEvent is only fired by another workflow's `uses:`, so it is excluded from trigger detection.
const workflowCallEvent = "workflow_call"
func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) {
switch rawOn.Kind {
case yaml.ScalarNode:
@@ -306,6 +309,9 @@ func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) {
if err != nil {
return nil, err
}
if val == workflowCallEvent {
return []*Event{}, nil
}
return []*Event{
{Name: val},
}, nil
@@ -319,6 +325,9 @@ func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) {
for _, v := range val {
switch t := v.(type) {
case string:
if t == workflowCallEvent {
continue
}
res = append(res, &Event{Name: t})
default:
return nil, fmt.Errorf("invalid type %T", t)
@@ -332,6 +341,9 @@ func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) {
}
res := make([]*Event, 0, len(events))
for i, k := range events {
if k == workflowCallEvent {
continue
}
v := triggers[i]
switch v.Kind {
case yaml.ScalarNode:
+47
View File
@@ -254,6 +254,53 @@ func TestParseRawOn(t *testing.T) {
},
},
},
{
// `workflow_call` is only fired by another workflow's `uses:`, so ParseRawOn intentionally excludes it from trigger detection.
input: `on:
workflow_call:
inputs:
env:
type: string
required: true
outputs:
sha:
value: ${{ jobs.build.outputs.commit }}
secrets:
DEPLOY_KEY:
required: true
`,
result: []*Event{},
},
{
// Mixed: a workflow that is both callable AND triggered by push. Only the "push" event surfaces.
input: `on:
workflow_call:
inputs:
env:
type: string
push:
branches: [main]
`,
result: []*Event{
{
Name: "push",
acts: map[string][]string{"branches": {"main"}},
},
},
},
{
// Scalar form: a purely reusable workflow has no event triggers.
input: "on: workflow_call",
result: []*Event{},
},
{
// Sequence form: `workflow_call` is excluded while sibling events are kept.
input: "on:\n - push\n - workflow_call\n - pull_request",
result: []*Event{
{Name: "push"},
{Name: "pull_request"},
},
},
}
for _, kase := range kases {
t.Run(kase.input, func(t *testing.T) {
@@ -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
@@ -0,0 +1,9 @@
# null_job_implicit.in.yaml
on: push
jobs:
empty:
notempty:
needs: empty
runs-on: ubuntu-latest
steps:
- run: echo ok
+2 -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":
+8
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
+8
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
+1 -5
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.
+8
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")
+13 -4
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 {
@@ -447,6 +445,17 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
c.cmd.Stdout = c.cmdStdout
c.cmd.Stdin = c.cmdStdin
c.cmd.Stderr = c.cmdStderr
c.cmd.Cancel = func() error {
// Golang's default cmd.Cancel only calls Process.Kill(), but here we need to close the parent pipes together:
// * for some commands like "git --batch-xxx", Windows git might have 2 processes (a wrapper and a real git process)
// * on Windows, if parent process is killed (context canceled), the children process won't be killed, and the pipe handles are still open.
// * if we don't close the parent pipes here, the children process won't exit.
//
// There is no such problem on POSIX, while it won't make things worse by closing the parent pipes also on POSIX.
err := c.cmd.Process.Kill()
c.closePipeFiles(c.parentPipeFiles)
return err
}
return c.cmd.Start()
}
+4 -1
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) {
+1 -2
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,
}
+1 -1
View File
@@ -161,7 +161,7 @@ func (ref RefName) ShortName() string {
if ref.IsFor() {
return ref.ForBranchName()
}
return string(ref) // usually it is a commit ID
return string(ref) // usually it is a commit ID, or "HEAD"
}
// RefGroup returns the group type of the reference
+5 -1
View File
@@ -8,6 +8,7 @@ import (
"strings"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
@@ -86,8 +87,11 @@ func (repo *Repository) UnstableGuessRefByShortName(shortName string) RefName {
commit, err := repo.GetCommit(shortName)
if err == nil {
commitIDString := commit.ID.String()
if strings.HasPrefix(commitIDString, shortName) {
// Make sure the short name is either a partial commit ID, or the symbolic HEAD ref.
if strings.HasPrefix(commitIDString, shortName) || shortName == "HEAD" {
return RefName(commitIDString)
} else {
setting.PanicInDevOrTesting("abuse of UnstableGuessRefByShortName, queried %s, got %s", shortName, commitIDString)
}
}
return ""
+15
View File
@@ -53,3 +53,18 @@ func TestRepository_GetRefsFiltered(t *testing.T) {
assert.Equal(t, "3ad28a9149a2864384548f3d17ed7f38014c9e8a", refs[1].Object.String())
}
}
func TestRepository_UnstableGuessRefByShortName(t *testing.T) {
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
bareRepo1, err := OpenRepository(t.Context(), bareRepo1Path)
assert.NoError(t, err)
defer bareRepo1.Close()
headCommit, err := bareRepo1.GetCommit("HEAD")
assert.NoError(t, err)
assert.Equal(t, RefName(headCommit.ID.String()), bareRepo1.UnstableGuessRefByShortName("HEAD"))
assert.Equal(t, RefName(headCommit.ID.String()), bareRepo1.UnstableGuessRefByShortName(headCommit.ID.String()[:8]))
assert.Equal(t, RefNameFromBranch("master"), bareRepo1.UnstableGuessRefByShortName("master"))
assert.Empty(t, bareRepo1.UnstableGuessRefByShortName("NotExisting"))
}
+64
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")
+1 -1
View File
@@ -40,7 +40,7 @@ type contextKey struct {
}
// RepositoryFromContextOrOpen attempts to get the repository from the context or just opens it
// The caller must call "defer gitRepo.Close()"
// The caller must call Closer.Close()
func RepositoryFromContextOrOpen(ctx context.Context, repo Repository) (*git.Repository, io.Closer, error) {
reqCtx := reqctx.FromContext(ctx)
if reqCtx != nil {
+4
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
+14 -2
View File
@@ -63,7 +63,7 @@ func TestFile(t *testing.T) {
{
name: "tags.py",
code: "<>",
want: lines(`<span class="o">&lt;</span><span class="o">&gt;</span>`),
want: lines(`<span class="o">&lt;&gt;</span>`),
lexerName: "Python",
},
{
@@ -102,7 +102,7 @@ c=2
<span class="n">def</span><span class="p">:</span>\n
<span class="n">a</span><span class="o">=</span><span class="mi">1</span>\n
\n
<span class="n">b</span><span class="o">=</span><span class="sa"></span><span class="s1">&#39;</span><span class="s1">&#39;</span>\n
<span class="n">b</span><span class="o">=</span><span class="s1">&#39;&#39;</span>\n
\n
<span class="n">c</span><span class="o">=</span><span class="mi">2</span>`,
),
@@ -114,6 +114,18 @@ c=2
want: []template.HTML{"<span class=\"c1\">--\n</span>", `<span class="k">SELECT</span>`},
lexerName: "SQL",
},
{
name: "test.http",
code: `HTTP/1.0 400 Bad request
Content-Type: text/html
<html></html>`,
want: lines(`<span class="kr">HTTP</span><span class="o">/</span><span class="m">1.0</span> <span class="m">400</span> <span class="ne">Bad request</span>\n
<span class="n">Content-Type</span><span class="o">:</span> <span class="l">text/html</span>\n
\n
<span class="p">&lt;</span><span class="nt">html</span><span class="p">&gt;&lt;/</span><span class="nt">html</span><span class="p">&gt;</span>`),
lexerName: "HTTP",
},
}
for _, tt := range tests {
+4 -4
View File
@@ -288,24 +288,24 @@ func detectChromaLexerWithAnalyze(fileName, lang string, code []byte) chroma.Lex
// if lang is provided, and it matches a lexer, use it directly
if byLang {
return lexer
return chroma.Coalesce(lexer)
}
// if a lexer is detected and there is no conflict for the file extension, use it directly
fileExt := path.Ext(fileName)
_, hasConflicts := chromaLexers().conflictingExtLangMap[fileExt]
if !hasConflicts && lexer != lexers.Fallback {
return lexer
return chroma.Coalesce(lexer)
}
// try to detect language by content, for best guessing for the language
// when using "code" to detect, analyze.GetCodeLanguage is slow, it iterates many rules to detect language from content
analyzedLanguage := analyze.GetCodeLanguage(fileName, code)
lexer = DetectChromaLexerByFileName(fileName, analyzedLanguage)
lexer, _ = detectChromaLexerByFileName(fileName, analyzedLanguage)
if lexer == lexers.Fallback {
if analyzedLanguage != enry.OtherLanguage {
log.Warn("No chroma lexer found for enry detected language: %s (file: %s), need to fix the language mapping between enry and chroma.", analyzedLanguage, fileName)
}
}
return lexer
return chroma.Coalesce(lexer)
}
+59 -4
View File
@@ -8,6 +8,7 @@ import (
"path/filepath"
"slices"
"strings"
"sync"
)
// HostMatchList is used to check if a host or IP is in a list.
@@ -23,10 +24,64 @@ type HostMatchList struct {
ipNets []*net.IPNet
}
// MatchBuiltinExternal A valid non-private unicast IP, all hosts on public internet are matched
// MatchBuiltinExternal A valid global-unicast IP that is neither private (see MatchBuiltinPrivate)
// nor a reserved special-purpose range (see reservedIPNets); i.e. a routable host on the public internet.
const MatchBuiltinExternal = "external"
// MatchBuiltinPrivate RFC 1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) and RFC 4193 (FC00::/7). Also called LAN/Intranet.
// reservedIPNets are special-purpose ranges that net.IP.IsPrivate omits but that must not be
// treated as public/external destinations (CGNAT, cloud metadata, IPv6 transition, etc.). We layer
// these on top of net.IP.IsPrivate (RFC 1918 / RFC 4193) so future additions to Go's IsPrivate are
// picked up automatically, while still covering the ranges it leaves out; otherwise the default
// allow-list would let authenticated users reach cloud metadata, internal, and IPv6 transition
// endpoints (SSRF), and a "private" block-list would fail to catch them.
var reservedIPNets = sync.OnceValue(func() []*net.IPNet {
var nets []*net.IPNet
for _, cidr := range []string{
// IPv4
"100.64.0.0/10", // RFC 6598 Carrier-Grade NAT
"168.63.129.16/32", // Azure WireServer metadata endpoint
"192.0.0.0/24", // RFC 6890 IETF protocol assignments
"192.0.2.0/24", // RFC 5737 TEST-NET-1
"192.88.99.0/24", // RFC 7526 6to4 relay anycast (deprecated)
"198.18.0.0/15", // RFC 2544 benchmarking
"198.51.100.0/24", // RFC 5737 TEST-NET-2
"203.0.113.0/24", // RFC 5737 TEST-NET-3
// IPv6
"100::/64", // RFC 6666 discard-only
"64:ff9b::/96", // RFC 6052 NAT64 (can embed IPv4 such as 169.254.169.254)
"64:ff9b:1::/48", // RFC 8215 local-use NAT64
"2001::/32", // RFC 4380 Teredo tunneling (embeds IPv4)
"2001:10::/28", // RFC 4843 ORCHID (deprecated)
"2001:20::/28", // RFC 7343 ORCHIDv2
"2001:db8::/32", // RFC 3849 documentation
"2002::/16", // RFC 3056 6to4 (embeds IPv4)
} {
_, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
panic("hostmatcher: invalid reserved CIDR " + cidr + ": " + err.Error())
}
nets = append(nets, ipNet)
}
return nets
})
// isPrivateIP reports whether ip falls in a private (net.IP.IsPrivate) or reserved special-purpose
// range (see reservedIPNets) that must not be considered a public/external destination.
func isPrivateIP(ip net.IP) bool {
if ip.IsPrivate() {
return true
}
for _, ipNet := range reservedIPNets() {
if ipNet.Contains(ip) {
return true
}
}
return false
}
// MatchBuiltinPrivate RFC 1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) and RFC 4193 (FC00::/7),
// plus the reserved special-purpose ranges in reservedIPNets (CGNAT, NAT64, cloud metadata, etc.).
// Also called LAN/Intranet.
const MatchBuiltinPrivate = "private"
// MatchBuiltinLoopback 127.0.0.0/8 for IPv4 and ::1/128 for IPv6, localhost is included.
@@ -105,11 +160,11 @@ func (hl *HostMatchList) checkIP(ip net.IP) bool {
for _, builtin := range hl.builtins {
switch builtin {
case MatchBuiltinExternal:
if ip.IsGlobalUnicast() && !ip.IsPrivate() {
if ip.IsGlobalUnicast() && !isPrivateIP(ip) {
return true
}
case MatchBuiltinPrivate:
if ip.IsPrivate() {
if isPrivateIP(ip) {
return true
}
case MatchBuiltinLoopback:
+55
View File
@@ -159,3 +159,58 @@ func TestHostOrIPMatchesList(t *testing.T) {
}
test(cases)
}
// TestReservedRanges ensures special-purpose ranges that net.IP.IsPrivate misses are kept out of the
// "external" allow-list (the default for webhook delivery and repository migrations) and folded into
// the "private" block-list, so they cannot be used for SSRF to metadata/internal endpoints.
func TestReservedRanges(t *testing.T) {
external := ParseHostMatchList("", "external")
private := ParseHostMatchList("", "private")
// legitimate public destinations: external, not private
for _, ip := range []string{"8.8.8.8", "1.1.1.1", "2001:4860:4860::8888", "1000::1"} {
addr := net.ParseIP(ip)
assert.Truef(t, external.MatchIPAddr(addr), "public ip %s should be external", ip)
assert.Falsef(t, private.MatchIPAddr(addr), "public ip %s should not be private", ip)
}
// RFC 1918 / RFC 4193 private ranges (now folded into privateIPNets instead of net.IP.IsPrivate):
// not external, blockable as private. Includes range edges to guard the CIDR boundaries.
for _, ip := range []string{
"10.0.0.0", "10.255.255.255", // 10.0.0.0/8
"172.16.0.0", "172.31.255.255", // 172.16.0.0/12
"192.168.0.0", "192.168.255.255", // 192.168.0.0/16
"fc00::", "fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", // fc00::/7
} {
addr := net.ParseIP(ip)
assert.Falsef(t, external.MatchIPAddr(addr), "private ip %s must not be external", ip)
assert.Truef(t, private.MatchIPAddr(addr), "private ip %s should match private block-list", ip)
}
// 172.32.0.0 is just outside 172.16.0.0/12: a public destination, not private
if addr := net.ParseIP("172.32.0.0"); assert.NotNil(t, addr) {
assert.True(t, external.MatchIPAddr(addr), "172.32.0.0 should be external")
assert.False(t, private.MatchIPAddr(addr), "172.32.0.0 should not be private")
}
// reserved ranges that IsPrivate does not cover: not external, but blockable as private
for _, ip := range []string{
"100.64.0.1", // CGNAT
"100.127.255.254", // CGNAT
"168.63.129.16", // Azure WireServer
"192.0.2.1", // TEST-NET-1
"198.18.0.1", // benchmarking
"198.51.100.1", // TEST-NET-2
"203.0.113.1", // TEST-NET-3
"192.88.99.1", // 6to4 relay anycast
"64:ff9b::1", // NAT64
"64:ff9b::a9fe:a9fe", // NAT64 embedding 169.254.169.254
"2001::1", // Teredo
"2002::1", // 6to4
"2001:db8::1", // documentation
} {
addr := net.ParseIP(ip)
assert.Falsef(t, external.MatchIPAddr(addr), "reserved ip %s must not be external", ip)
assert.Truef(t, private.MatchIPAddr(addr), "reserved ip %s should match private block-list", ip)
}
}
+40 -16
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")
}
+35
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"))
}
+21
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())
}
+7
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"))
}
+21 -3
View File
@@ -173,16 +173,25 @@ var emojiProcessors = []processor{
emojiProcessor,
}
// isBareURLSubject reports whether the (HTML-escaped) commit subject content
// is entirely a single URL, ignoring leading/trailing whitespace.
func isBareURLSubject(content string) bool {
s := strings.TrimSpace(html.UnescapeString(content))
if s == "" {
return false
}
m := common.GlobalVars().LinkRegex.FindStringIndex(s)
return m != nil && m[0] == 0 && m[1] == len(s)
}
// PostProcessCommitMessageSubject will use the same logic as PostProcess and
// PostProcessCommitMessage, but will disable the shortLinkProcessor and
// emailAddressProcessor, will add a defaultLinkProcessor if defaultLink is set,
// which changes every text node into a link to the passed default link.
// emailAddressProcessor, and wraps the whole subject in defaultLink.
func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content string) (string, error) {
procs := []processor{
fullIssuePatternProcessor,
comparePatternProcessor,
fullHashPatternProcessor,
linkProcessor,
mentionProcessor,
issueIndexPatternProcessor,
commitCrossReferencePatternProcessor,
@@ -190,6 +199,15 @@ func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content st
emojiShortCodeProcessor,
emojiProcessor,
}
// When the whole subject is a bare URL, linkProcessor would turn it into
// a competing anchor and hijack the surrounding defaultLink wrapper, leaving
// the subject visually unclickable. Match GitHub: render such subjects as
// plain text inside defaultLink. Partial URLs inside larger text still become
// their own links (nested anchors aren't legal HTML, so the outer defaultLink
// naturally breaks on that span, same as on GitHub).
if !isBareURLSubject(content) {
procs = append(procs, linkProcessor)
}
procs = append(procs, func(ctx *RenderContext, node *html.Node) {
ch := &html.Node{Parent: node, Type: html.TextNode, Data: node.Data}
node.Type = html.ElementNode
+3 -1
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
}
+3 -1
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"`
}
+13 -2
View File
@@ -146,15 +146,26 @@ func ParseControlFile(r io.Reader) (*Package, error) {
var depends strings.Builder
var control strings.Builder
s := bufio.NewScanner(io.TeeReader(r, &control))
// https://www.debian.org/doc/debian-policy/ch-controlfields.html#syntax-of-control-files
s := bufio.NewScanner(r)
for s.Scan() {
line := s.Text()
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
// A binary package control file holds exactly one stanza. Stop at the
// blank line that terminates it, otherwise a crafted control file could
// smuggle additional stanzas (with attacker-chosen Filename/Package
// fields) into the generated repository "Packages" index.
if control.Len() == 0 {
continue
}
break
}
control.WriteString(line)
control.WriteByte('\n')
if line[0] == ' ' || line[0] == '\t' {
switch key {
case "Description":
+15
View File
@@ -184,4 +184,19 @@ func TestParseControlFile(t *testing.T) {
assert.NotNil(t, p)
}
})
t.Run("SingleStanzaOnly", func(t *testing.T) {
// A control file with a trailing stanza must not leak the extra fields into
// p.Control, otherwise buildPackagesIndices would emit a second package entry
// with an attacker-chosen Filename into the repository "Packages" index.
content := bytes.NewBufferString("Package: realpkg\nVersion: 1.0.0\nArchitecture: amd64\nMaintainer: a <a@b.c>\nDescription: real\n\nPackage: openssl\nVersion: 99.0\nArchitecture: amd64\nFilename: pool/main/o/openssl/evil.deb\nDescription: spoofed\n")
p, err := ParseControlFile(content)
assert.NoError(t, err)
assert.NotNil(t, p)
assert.Equal(t, "realpkg", p.Name)
assert.Equal(t, "1.0.0", p.Version)
assert.NotContains(t, p.Control, "openssl")
assert.NotContains(t, p.Control, "evil.deb")
})
}
+6 -15
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
}
+9
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
+27 -20
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.
+16 -3
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))
}
+12
View File
@@ -140,6 +140,18 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject(testInput(), "https://example.com/link", mockRepo))
})
t.Run("RenderCommitMessageLinkSubjectURLOnly", func(t *testing.T) {
// a bare URL in the subject must not hijack the default link
expected := `<a href="https://example.com/link" class="muted">https://example.com/file.bin</a>`
assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject("https://example.com/file.bin", "https://example.com/link", mockRepo))
})
t.Run("RenderCommitMessageLinkSubjectPartialURL", func(t *testing.T) {
// a URL embedded in larger subject text still becomes its own link
expected := `<a href="https://example.com/link" class="muted">see </a><a href="https://example.com/x" data-markdown-generated-content="">https://example.com/x</a><a href="https://example.com/link" class="muted"> here</a>`
assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject("see https://example.com/x here", "https://example.com/link", mockRepo))
})
t.Run("RenderIssueTitle", func(t *testing.T) {
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
expected := ` space @mention-user<SPACE><SPACE>
+89 -33
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)
}
+43 -10
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",
},
}
+6 -4
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)))
}
+10 -3
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"))
}
+10 -9
View File
@@ -637,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:",
@@ -1787,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>",
@@ -3624,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.",
+2 -2
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,7 +54,7 @@
"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",
+748 -121
View File
File diff suppressed because it is too large Load Diff
+1 -7
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 {
+20 -4
View File
@@ -264,16 +264,32 @@ 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:]
}
// Ack a re-sent finalize idempotently. Appending new rows past the seal errors.
if task.LogInStorage {
if len(rows) > 0 {
return nil, status.Errorf(codes.AlreadyExists, "log file has been archived")
}
res.Msg.AckIndex = ack
return res, nil
}
if task.LogInStorage {
return nil, status.Errorf(codes.AlreadyExists, "log file has been archived")
// 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
}
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)
+13 -5
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,
}
}
}
@@ -146,6 +146,7 @@ func PackageMetadata(ctx *context.Context) {
}
resp := createPackageMetadataResponse(
ctx,
setting.AppURL+"api/packages/"+ctx.Package.Owner.Name+"/composer",
pds,
)
+10 -2
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)
}
+30 -21
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)
}
+84 -61
View File
@@ -212,6 +212,11 @@ func repoAssignment() func(ctx *context.APIContext) {
ctx.APIErrorNotFound()
return
}
if !ctx.TokenCanAccessRepo(repo) {
ctx.APIErrorNotFound()
return
}
}
}
@@ -249,51 +254,66 @@ func checkTokenPublicOnly() func(ctx *context.APIContext) {
return
}
// public Only permission check
switch {
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryRepository):
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
ctx.APIError(http.StatusForbidden, "token scope is limited to public repos")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryIssue):
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
ctx.APIError(http.StatusForbidden, "token scope is limited to public issues")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryOrganization):
if ctx.Org.Organization != nil && ctx.Org.Organization.Visibility != api.VisibleTypePublic {
ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs")
return
}
if ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryUser):
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
ctx.APIError(http.StatusForbidden, "token scope is limited to public users")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryActivityPub):
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
ctx.APIError(http.StatusForbidden, "token scope is limited to public activitypub")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryNotification):
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
ctx.APIError(http.StatusForbidden, "token scope is limited to public notifications")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryPackage):
if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
ctx.APIError(http.StatusForbidden, "token scope is limited to public packages")
return
for _, category := range requiredScopeCategories {
switch category {
case auth_model.AccessTokenScopeCategoryRepository:
if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) {
ctx.APIError(http.StatusForbidden, "token scope is limited to public repos")
return
}
case auth_model.AccessTokenScopeCategoryIssue:
if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) {
ctx.APIError(http.StatusForbidden, "token scope is limited to public issues")
return
}
case auth_model.AccessTokenScopeCategoryOrganization:
orgPrivate := ctx.Org.Organization != nil && !ctx.Org.Organization.Visibility.IsPublic()
userOrgPrivate := ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() && !ctx.ContextUser.Visibility.IsPublic()
if orgPrivate || userOrgPrivate {
ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs")
return
}
case auth_model.AccessTokenScopeCategoryUser:
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && !ctx.ContextUser.Visibility.IsPublic() {
ctx.APIError(http.StatusForbidden, "token scope is limited to public users")
return
}
case auth_model.AccessTokenScopeCategoryActivityPub:
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && !ctx.ContextUser.Visibility.IsPublic() {
ctx.APIError(http.StatusForbidden, "token scope is limited to public activitypub")
return
}
case auth_model.AccessTokenScopeCategoryNotification:
if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) {
ctx.APIError(http.StatusForbidden, "token scope is limited to public notifications")
return
}
case auth_model.AccessTokenScopeCategoryPackage:
if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
ctx.APIError(http.StatusForbidden, "token scope is limited to public packages")
return
}
}
}
}
}
func rejectPublicOnly() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !ctx.PublicOnly {
return
}
ctx.APIError(http.StatusForbidden, "this endpoint is not available for public-only tokens")
}
}
func contextAuthenticatedUser() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
ctx.ContextUser = ctx.Doer
}
}
// if a token is being used for auth, we check that it contains the required scope
// if a token is not being used, reqToken will enforce other sign in methods
func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeCategory) func(ctx *context.APIContext) {
@@ -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)
+8 -3
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 {
+3 -7
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"
@@ -1076,6 +1074,8 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) {
ctx.APIError(http.StatusNotFound, err)
} else if errors.Is(err, util.ErrPermissionDenied) {
ctx.APIError(http.StatusForbidden, err)
} else if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
@@ -1770,11 +1770,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 {
+3 -1
View File
@@ -55,7 +55,8 @@ func ListForks(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
forks, total, err := repo_service.FindForks(ctx, ctx.Repo.Repository, ctx.Doer, utils.GetListOptions(ctx))
listOptions := utils.GetListOptions(ctx)
forks, total, err := repo_service.FindForks(ctx, ctx.Repo.Repository, ctx.Doer, listOptions)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -79,6 +80,7 @@ func ListForks(ctx *context.APIContext) {
apiForks[i] = convert.ToRepo(ctx, fork, permission)
}
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, apiForks)
}
+2 -1
View File
@@ -47,9 +47,10 @@ func buildSearchIssuesRepoIDs(ctx *context.APIContext) (repoIDs []int64, allPubl
Actor: ctx.Doer,
}
if ctx.IsSigned {
opts.Private = !ctx.PublicOnly
opts.Private = true
opts.AllLimited = true
}
opts.ApplyPublicOnly(ctx.PublicOnly)
if ctx.FormString("owner") != "" {
owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
if err != nil {
+4 -2
View File
@@ -95,7 +95,8 @@ func ListTrackedTimes(ctx *context.APIContext) {
if qUser != "" {
user, err := user_model.GetUserByName(ctx, qUser)
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
return
} else if err != nil {
ctx.APIErrorInternal(err)
return
@@ -523,7 +524,8 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
if qUser != "" {
user, err := user_model.GetUserByName(ctx, qUser)
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
return
} else if err != nil {
ctx.APIErrorInternal(err)
return
+1 -1
View File
@@ -994,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)
+6 -3
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 {
+7 -4
View File
@@ -19,12 +19,15 @@ import (
func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
opts := utils.GetListOptions(ctx)
repos, count, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
searchOpts := repo_model.SearchRepoOptions{
Actor: u,
Private: private,
ListOptions: opts,
OrderBy: "id ASC",
})
}
searchOpts.ApplyPublicOnly(ctx.PublicOnly)
repos, count, err := repo_model.GetUserRepositories(ctx, searchOpts)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -79,8 +82,7 @@ func ListUserRepos(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
private := ctx.IsSigned
listUserRepos(ctx, ctx.ContextUser, private)
listUserRepos(ctx, ctx.ContextUser, ctx.IsSigned)
}
// ListMyRepos - list the repositories you own or have access to.
@@ -110,6 +112,7 @@ func ListMyRepos(ctx *context.APIContext) {
Private: ctx.IsSigned,
IncludeDescription: true,
}
opts.ApplyPublicOnly(ctx.PublicOnly)
repos, count, err := repo_model.SearchRepository(ctx, opts)
if err != nil {
+5 -2
View File
@@ -20,11 +20,14 @@ import (
// getStarredRepos returns the repos that the user with the specified userID has
// starred
func getStarredRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, error) {
starredRepos, err := repo_model.GetStarredRepos(ctx, &repo_model.StarredReposOptions{
opts := &repo_model.StarredReposOptions{
ListOptions: utils.GetListOptions(ctx),
StarrerID: user.ID,
IncludePrivate: private,
})
}
opts.ApplyPublicOnly(ctx.PublicOnly)
starredRepos, err := repo_model.GetStarredRepos(ctx, opts)
if err != nil {
return nil, err
}
+5 -8
View File
@@ -9,7 +9,6 @@ import (
activities_model "code.gitea.io/gitea/models/activities"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
@@ -69,19 +68,16 @@ func Search(ctx *context.APIContext) {
maxResults = 1
users = []*user_model.User{user_model.NewActionsUser()}
default:
var visible []structs.VisibleType
if ctx.PublicOnly {
visible = []structs.VisibleType{structs.VisibleTypePublic}
}
users, maxResults, err = user_model.SearchUsers(ctx, user_model.SearchUserOptions{
opts := user_model.SearchUserOptions{
Actor: ctx.Doer,
Keyword: ctx.FormTrim("q"),
UID: uid,
Types: []user_model.UserType{user_model.UserTypeIndividual},
SearchByEmail: true,
Visible: visible,
ListOptions: listOptions,
})
}
opts.ApplyPublicOnly(ctx.PublicOnly)
users, maxResults, err = user_model.SearchUsers(ctx, opts)
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]any{
"ok": false,
@@ -214,6 +210,7 @@ func ListUserActivityFeeds(ctx *context.APIContext) {
Date: ctx.FormString("date"),
ListOptions: listOptions,
}
opts.ApplyPublicOnly(ctx.PublicOnly)
feeds, count, err := feed_service.GetFeeds(ctx, opts)
if err != nil {

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