Compare commits

..

92 Commits

Author SHA1 Message Date
Shudhanshu Singh
a61ef7aa14 fix(markup): keep list items on separate lines in quote replies (#40329)
Keep each list item on its own line when quoting a comment. Items
containing bold text or links could run together in the reply editor,
breaking the quoted list.
2026-11-11 12:55:13 +00:00
silverwind
03fc2bdcc8 fix(repo): require the unit in AccessibleRepositoryCondition (#40059)
`AccessibleRepositoryCondition(user, unitType)` now requires the repo to
have the requested unit enabled and not globally disabled, consistent
with the per-repo permission checks. This affects code search, Actions
run listings, LFS object reuse, recently pushed branches and org project
milestones.

Written by Claude.
2026-11-11 12:55:04 +00:00
silverwind
6eb5efb6bb fix(ui): consistent spacing between labels (#40261)
The issue reference popup showed its labels without any spacing, and the
other label lists each used their own gap, with wrapped rows spaced
differently from columns.

`.labels-list` is now a wrapping flex list with `--gap-inline` by
default, giving the popup, issue sidebar and project cards equal gaps in
both directions. Lists inline with text, in issue list titles and
timeline events, stay inline so their labels still wrap one by one.

<img width="600" alt="Issue reference popup, issue sidebar and issue
list labels, before and after"
src="https://github.com/user-attachments/assets/fc4f0b9e-2b9d-4060-a2fb-c1bd3b7391db"
/>
2026-11-11 10:33:15 +00:00
GiteaBot
c62bcf19f6 [skip ci] Updated translations via Crowdin 2026-11-11 00:33:48 +00:00
silverwind
d399f844e6 test: fix the login tab order e2e test in macOS WebKit (#40316)
The login tab order test fails in WebKit on macOS, so `make test-e2e`
fails locally on every Mac. CI on Linux is unaffected.

- macOS WebKit moves Tab focus only between text fields, plus buttons
and checkboxes when the system keyboard navigation setting is on, and
never to links, like Safari. Option+Tab reaches every element there and
acts as Tab in Linux WebKit, so the test presses Alt+Tab in WebKit.
- Headless WebKit applies the username `autofocus` late, which pulled
focus back from the password field. The test now waits for the autofocus
first, which also covers it.

Follow-up to https://github.com/go-gitea/gitea/pull/40081

Assisted-by: Claude Code:claude-opus-5-5
2026-11-10 20:07:28 +00:00
Shudhanshu Singh
db23b5a192 fix(api): keep webhook settings that are omitted in an edit request (#40282)
`PATCH .../hooks/{id}` previously reset webhook events to push-only and
cleared the branch filter and authorization header whenever they were
not included in the payload. Disabling a hook with `{"active": false}`
silently destroyed its configuration.

`BranchFilter` and `AuthorizationHeader` are now pointers so omitted
fields remain untouched, while empty strings can still be passed to
clear them explicitly.

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-10 19:26:22 +00:00
Gaurav Dubey
d5ce1cad7f fix(indexer): reindex issues when their projects change (#40299)
With the bleve, elasticsearch or meilisearch issue indexer, a keyword
search filtered by project matched issues by the projects they had when
they were last indexed, so newly added issues were missing and removed
ones still showed up.

- Reindex an issue when its projects change, using `IssueChangeProjects`
from https://github.com/go-gitea/gitea/pull/30205.
- Reindex the issues of a deleted project, so the "No project" filter
finds them again.

Entries that are already stale are corrected on the issue's next change
or an index rebuild.

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

Assisted-by: Claude Code:claude-opus-5-5

---
🤖 Opened with [Superhuman](https://github.com/gaurav0107/superhuman), an
open-source contribution agent.

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-10 19:26:12 +00:00
Giteabot
a61ad4876a chore(deps): update go toolchain directive to v1.27.4 (#40315)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [go](https://go.dev/)
([source](https://redirect.github.com/golang/go)) | toolchain | patch |
`1.27.3` → `1.27.4` |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Mend Renovate
CLI](https://redirect.github.com/renovatebot/renovate).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4yMTAuMCIsInVwZGF0ZWRJblZlciI6IjQ0LjIxMC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2026-11-10 18:08:53 +00:00
silverwind
f5a54d6169 chore(css): remove dead CSS (#40314)
Remove CSS rules that no longer match anything because earlier changes
removed or restructured the markup they styled.

- Fomantic card styles, unused since the migrate page and profile card
moved off cards
- List item image, table cell and grid column width rules whose last
users are gone
- Dashboard filter menu and actions panel button overrides

Assisted-by: Claude Code:claude-opus-5-5
2026-11-10 17:35:57 +00:00
Yonghun Shin
2ef5b7623b fix(packages): generic package deletion ignores terraform state lock and latest version (#40302)
`IsLocked` and `IsLatest` in `services/packages/terraform/state.go`
return early when the package **is** a terraform state (`==` instead of
`!=`), so the `OnBeforeRemovePackageAll` /
`OnBeforeRemovePackageVersion` guards never fire. As a result, deleting
through the generic package API (`DELETE
/api/v1/packages/{owner}/terraform/{name}[/{version}]`) or the web UI
ignores the state lock and allows removing the latest state version,
while the terraform state endpoints reject both.

Changes:

- Flip the type check in `IsLocked` and `IsLatest`.
- `DeleteState` (terraform state `DELETE` endpoint) relied on the bug:
after checking the lock it removed every version one by one with
`RemovePackageVersion`, including the latest. With the guard fixed, it
failed with 500 partway through and left a half-deleted state (the
existing `TestPackageTerraform` subtests catch this). It now calls
`RemovePackage`, which checks the lock and deletes everything in one
transaction.
- The generic package delete API used `APIErrorInternal` for every
error, so the guard errors became 500. It now uses `APIErrorAuto`, which
returns 422 for these.
- Added a `GenericPackageAPI` subtest to `TestPackageTerraform`:
deleting an older version works, deleting the latest version or a locked
state returns 422, and deleting an unlocked state works. It fails on
current `main`.

Tested locally with `go test -run '^TestPackage' ./tests/integration`
(SQLite): all pass.

AI disclosure: I used an AI assistant (Claude) to find the bug, trace
the code paths, and draft the change and this description. I reviewed
the change and ran the tests myself.

Signed-off-by: choam2426 <choam2426@gmail.com>
2026-11-10 17:10:59 +00:00
Gaurav Dubey
1fcb6b4cd6 fix(packages): rebuild repository index after removing a package (#40306)
Deleting a package or a package version from the web UI, the admin panel
or `DELETE /api/v1/packages/...` removed the database rows but never
rebuilt the Debian, Alpine, RPM or Arch repository index, so clients
kept seeing the deleted version until something else triggered a
rebuild. Only the registries' own delete endpoints and the cleanup task
rebuilt it.

https://github.com/go-gitea/gitea/pull/22810 was closed in favor of
`OnBeforeRemovePackageVersion`, but that hook runs before the version is
gone, so it can't refresh the index. This adds an `OnAfterRemovePackage`
hook and rebuilds the owner's repository files there, the same way the
cleanup task does.

Deleting the last RPM of a group also removed the repodata of all other
groups of the owner. Now only the emptied group's files are removed.

Fixes: https://github.com/go-gitea/gitea/issues/32830
Fixes: https://github.com/go-gitea/gitea/issues/40304
Fixes: https://github.com/go-gitea/gitea/issues/40305

---
🤖 Opened with [Superhuman](https://github.com/gaurav0107/superhuman), an
open-source contribution agent.

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-10 16:29:39 +00:00
wxiaoguang
d1d702bea9 refactor: user error system (#40308)
Introduce "FlashErrorAuto" for "XxxErrorAuto" family and simplify the
error handling logic.

* Use ErrorUnwrapForHTML for web error messages 
* Use ErrorUnwrapForUser for api error messages
* Make Flash message can take up to about 3000 bytes

Next step: completely refactor "ctx.ServerError"

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-10 16:29:36 +00:00
Toni
518e08c2c7 fix: discard stale markdown preview and diff responses (#40289)
Preview and diff tabs render whichever response arrives last, so a slow
earlier request can overwrite the result of a newer click. Introduce
ExclusiveFetch class to handle duplicate requests.

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

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-11-10 14:22:53 +00:00
Basavaraj S m
25c5b6ab37 fix(issues): hide commit references the viewer cannot read (#40216)
Pushing a commit whose message mentions `owner/repo#1` adds a
"referenced this issue from a commit" event to that issue, with the
commit subject and a link into the pushed repository. The event never
recorded that repository, so a commit in a private repository
referencing a public issue showed its subject and repository path to
anonymous visitors of the issue page and the timeline API.

- The event now records the pushed repository in `RefRepoID`, and is
only shown to viewers who can read that repository's code, including the
issue's own. Older events have no recorded source and stay as they are.
- As on GitHub, the event stays attributed to the repository the commit
was first pushed to, even after the commit reaches another repository,
and is removed when that repository is deleted.

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-10 06:13:50 +00:00
Shivansh Garg
640cd615c4 feat: restrict pull/issue creation on access basis (#40069)
Adds a repository-level **"Restrict to collaborators"** option under the
Issues and Pull Request unit settings. When enabled, only users with
collaborator access to the base repository (owner, team members, or
explicit collaborators) can open issues or pull requests against it.

Public repositories often need to be visible/cloneable by everyone, but
owners may want to limit who can open pull requests (e.g. restrict to an
internal team while still keeping the code public)

* Fixes: https://github.com/go-gitea/gitea/issues/40015
* Fixes: https://github.com/go-gitea/gitea/issues/31890
* Fixes: https://github.com/go-gitea/gitea/issues/28021
* Fixes: https://github.com/go-gitea/gitea/issues/14801

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-11-10 00:53:49 +00:00
GiteaBot
c01f93b5a7 [skip ci] Updated translations via Crowdin 2026-11-10 00:32:42 +00:00
silverwind
3c41728017 test(e2e): assert server-rendered biography after profile update (#40176)
Since Playwright 1.56, a WebKit click on a native submit button can
return before the form navigation starts, so `toHaveValue` passed on the
old page with the value `fill()` just set, and the next step raced the
reload.

Assert the textarea's text instead, its server-rendered default, which
only the reloaded page has.

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-11-09 21:50:27 +00:00
Trung Hehe
06b393e402 feat(ldap): support multiple comma-separated LDAP hosts for failover (#6898) (#40201)
Allows an LDAP auth source to list multiple comma-separated servers in
the Host field so logins continue working when one server is
unreachable. Entries can be plain hosts using the configured Port and
Security Protocol fields or `ldap://`, `ldaps://`, or `ldap+starttls://`
URLs with their own port and protocol (as proposed in
https://github.com/go-gitea/gitea/pull/31649#issuecomment-2505555133),
with TLS protocol consistency enforced so an entry cannot silently
downgrade encryption.

Hosts are tried in order with a 250 ms head start each so an
unresponsive server does not block logins for the full 60 s connect
timeout, and only connection failures fail over to the next host so
failed binds are not retried across servers. Existing single-host
configurations continue to work unchanged without a migration.

Docs: https://gitea.com/gitea/docs/pulls/635
Fixes https://github.com/go-gitea/gitea/issues/6898
/claim #6898

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-09 21:45:12 +00:00
Lunny Xiao
3561478109 fix(projects): move only the dragged card on project boards (#40182)
The board reported every card of the target column on each drag, and the
server wrote all of them into that column. A stale board in another
browser therefore pulled cards moved by someone else back to their old
column.

The client now sends only the dragged issue and its predecessor, and the
server computes the position in one transaction:

- Only the dragged issue changes column, the others are just renumbered.
- A predecessor that is no longer in the column puts the card at the
end.

Added an e2e test.

Fixes https://github.com/go-gitea/gitea/issues/21243
Fixes https://github.com/go-gitea/gitea/issues/32070
Fixes https://github.com/go-gitea/gitea/issues/13498

---
Assisted-by: Codet:claude

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-09 21:39:22 +00:00
Shudhanshu Singh
2d6dca9abb fix(ui): improve user filter dropdown when no remote search is configured (#40296)
On the dashboard issue and pull request pages, author and assignee
filters only accept direct username input since candidate lists across
all accessible repositories cannot be pre-fetched. However, the dropdown
inherited several confusing artifacts from the repository-level user
search:

- Displayed a misleading "Shows a maximum of 30 users" tooltip even
though no remote user list was fetched.
- Used "Search users" as placeholder rather than indicating direct
username input.
- Pressing Enter in the input had no effect, requiring mouse selection.
- Any active username filter was not reflected in the dropdown menu on
open.

This change:
- Only renders the 30-user tooltip when a remote search URL is provided.
- Uses "Filter by username…" as placeholder when no remote search URL
exists.
- Applies the filter on Enter keypress.
- Ensures the active username is displayed and selected when opening the
menu.

Fixes: https://github.com/go-gitea/gitea/issues/40275
Fixes: https://github.com/go-gitea/gitea/issues/33426

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-11-09 21:34:43 +00:00
Lunny Xiao
bccc90d654 fix(api): lock and unlock issues as the signed-in user, not the repository owner (#40291)
`PUT/DELETE /repos/{owner}/{repo}/issues/{index}/lock` passed
`ctx.ContextUser` as the doer of `LockIssue` / `UnlockIssue`. In the
repo routes that is the owner of the repository, so on a repository of
an organization the lock and unlock timeline comments were created by
the organization instead of the user who did it. The web handler already
uses the signed-in user; this uses `ctx.Doer` as well.

Adds a subtest `DoerOnOrganizationRepo` to the existing
`TestAPILockIssue`: a member of an organization locks and unlocks an
issue of an organization repository, and the comments must be posted by
that member.

Found while testing audit logging of the issue lock events downstream.

_Authored by Codet (Claude)._
2026-11-09 18:00:06 +00:00
wxiaoguang
772f365254 refactor: use RunWithStderr for git commands (#40295)
If the stderr output is not handled, then it needs to use RunWithStderr
to make sure the git error message is included in the returned error.
Otherwise, when the git command fails, the error log won't show useful
information.

Ref:

* #39795 : no useful information in log messages
*
https://github.com/go-gitea/gitea/pull/40160#pullrequestreview-5752720866
2026-11-09 17:59:52 +00:00
bircni
e0b2f3425e fix(actions): run pull_request workflows on the merge commit (#40076)
`pull_request` workflow runs checked out the PR head, so a PR could pass
CI and still break the base branch once merged.

Gitea now maintains `refs/pull/<index>/merge`, a test merge of the head
into the base branch, for open pull requests. It is updated on creation
and mergeability checks and removed on close or merge.

- `pull_request` runs read their workflows from the test merge and
expose it as `github.ref`, `github.sha` and
`github.event.pull_request.merge_commit_sha`. Commit statuses, the API's
`head_sha`, `workflow_run` payloads and `paths` filters stay on the
head.
- Runs fall back to the head when the merge conflicts, the pull request
is closed or git is older than 2.40.
- Hidden refs keep each run's test merge (`refs/gitea/run/<id>`) and
merged commits (`refs/gitea/pull/<index>/merged`) from `git gc`, for
reruns and after the base branch is deleted or force-pushed.

Fixes: https://github.com/go-gitea/gitea/issues/27039
Fixes: https://github.com/go-gitea/gitea/issues/13134
Fixes: https://github.com/go-gitea/gitea/issues/30972
Docs: https://gitea.com/gitea/docs/pulls/617

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-09 12:42:40 +00:00
wxiaoguang
0ab0bfc375 fix: don't log network error in console when loading issue popup (#40288)
* fix #40286

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
2026-11-09 05:59:03 +00:00
Giteabot
f3354aa902 chore(deps): update dependencies (#40292)
Updates JS, Go, Python and Actions dependencies, including the stylelint
18 and stylelint-config-recommended 19 majors.

- Update clippie to 4.3.1, which fixes copy buttons in browsers without
`ClipboardItem` such as Pale Moon
- Exempt the markup `tt` selectors from the new `selector-no-deprecated`
rule, the sanitizer allows `<tt>`
- Replace the `stylelint-value-no-unknown-custom-properties` plugin with
stylelint's `referenceFiles` and the core `no-unknown-*` rules
- Remove dead dropdown loading icon styles that referenced deleted
keyframes

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

Assisted-by: Claude Code:claude-opus-5-5

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-09 05:32:54 +00:00
Shudhanshu Singh
7b1cde99d0 perf(charset): skip plain ASCII text when escaping control characters (#39937)
Every line of file, blame, diff and code preview views goes through
`EscapeControlHTML`, which streamed it through a 32 KiB read buffer and
copied every tag and text part. Views render the `EscapeStatus` before
the content, so every caller holds the complete HTML and streaming can't
help. It is now escaped in one pass over the string and returned
unchanged when nothing needs escaping.

- Skip text of printable ASCII, tabs and newlines, such lines return
immediately
- Render markup and wiki pages into a buffer instead of a pipe and
goroutine, and remove `EscapeControlReader`
- Analyze long text parts in windows overlapping by the detection
context, which bounds memory without changing results
- Escape invalid bytes before a tag, they were moved into the tag part
unescaped and could hide a `<math>` tag

| | before | after |
|---|---|---|
| ASCII line | 5.1 µs, 59 allocs | 0.08 µs, 1 alloc |
| line with one `é` | 3.9 µs, 30 allocs | 0.29 µs, 3 allocs |
| 150 KB document | 2.5 ms, 5.6 MB | 0.27 ms, 0.6 KB |

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-11-09 03:42:30 +00:00
GiteaBot
2bbac68f45 [skip ci] Updated translations via Crowdin 2026-11-09 00:36:42 +00:00
silverwind
d715a5ae75 test(api): cover null milestone due_on (#40172)
Removing a milestone deadline with `"due_on": null` was untested, only
`""` was. On release/v28 the null case returned HTTP 500
(https://github.com/go-gitea/gitea/pull/40171). main already handles it,
this adds the case to the existing test so it stays that way.
2026-11-08 20:53:59 +00:00
Lunny Xiao
1554b6c939 feat!: remove repository adopt feature (#40181)
Remove the "adopt unadopted repositories" feature. Adopting assumes a
fixed repository storage path on disk, which makes the storage path hard
to change. It is mostly used once for migrations, and the [Gitea
importer](https://gitea.com/gitea/importer) covers that after the
repositories are moved out of the repository root. The docs are updated
in https://gitea.com/gitea/docs/pulls/639.

- Remove the admin and user adopt pages, the `/admin/unadopted` API
endpoints and the adopt notifier hook
- Remove the `ALLOW_ADOPTION_OF_UNADOPTED_REPOSITORIES` and
`ALLOW_DELETION_OF_UNADOPTED_REPOSITORIES` settings
- The user "Repositories" settings page only lists the user's
repositories
- Repository files without a database record must now be removed from
disk by the administrator

Assisted-by: Codet:claude

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-08 18:38:23 +00:00
wxiaoguang
3699309edd feat: support jpeg-xl (jxl) (#40283)
Major browsers (Chrome/Firefox/Safari) all support it now,
https://developer.chrome.com/blog/jpeg-xl-in-chrome

* Fix #16629
2026-11-08 15:24:46 +00:00
silverwind
8cb6fb2001 fix: add missing checks to more handlers and services (#40131)
Several code paths skipped checks that their siblings already perform.

- Code comment replies are rejected when they target a review of another
pull request or another user's pending review
- OAuth2 refuses tokens for users who can't sign in, and introspection
reports their tokens and already used refresh tokens as inactive
- AGit proc-receive runs the same pull request permission and wiki
checks as pre-receive
- Repository transfer drops the repository's owner-level scoped workflow
source registrations, and registrations whose source the owner no longer
owns are ignored
- The web editor's base branch copy into a fork and the fork's upstream
divergence info require read access to the base repository
- LFS downloads require sign-in under strict `REQUIRE_SIGNIN_VIEW`, like
git smart HTTP
- Unknown AGit target branches are reported as an unexpected ref again
2026-11-08 09:02:51 +00:00
silverwind
cd92c6a008 fix: paste copied code as text, not as a markdown table (#40053)
Copying lines from a diff, file view, blame, code search or code preview
and pasting them into a comment on Gitea or GitHub produced a markdown
table with escaped pipes and stripped indentation. Both use
`@github/paste-markdown`, which converts copied HTML tables unless they
carry `data-paste-markdown-skip`, as GitHub's own code tables do.
Gitea's code tables now set it too.

The test is a bit more complicated because of a upstream bug which
causes `paste-markdown` to insert pasted content twice, once
https://github.com/github/paste-markdown/pull/128 is merged and
released, the negative assertion could be removed.

---------

Signed-off-by: silverwind <me@silverwind.io>
2026-11-08 09:01:50 +00:00
GiteaBot
edebfaa125 [skip ci] Updated translations via Crowdin 2026-11-08 00:36:44 +00:00
silverwind
bd8fd71bc1 enhance(theme): derive theme colors with color-mix and unify shades (#40113)
Derive theme colors from a few base colors with `color-mix(in srgb, …)`
and make the colorful palette coherent, so custom themes that import a
Gitea theme can override a base color and get matching shades.

- Every `-light`, `-dark-1` and `-dark-2` shade follows one rule for all
hues, fixing dark orange shades copied from the light theme, black
shades that barely moved and neon tints
- Base colors move at most 4 OKLab units toward a common lightness, the
median in dark and red in light, yellow stays unchanged
- Text, secondary, console, message, surface and alpha colors derive
from their own base, alpha variants now match their names
- Added and removed diff colors share one lightness per level in all
themes, including colorblind ones
- Chart.js and the external render iframe resolve `color-mix()` values
to hex through `getComputedStyle`
- Remove unused `secondary-dark-12`, `-13`, `shadow-opaque` and
`workflow-edge-hover`

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

<img width="880" alt="Components with the largest color changes, dark
and light, before and after"
src="https://github.com/user-attachments/assets/a9d27636-e3bb-46fa-a3ac-3fec33fd9806"
/>
<img width="880" alt="All theme colors per family, before and after,
unchanged ones dimmed"
src="https://github.com/user-attachments/assets/9977dde3-0749-4926-b110-18c0dc31d632"
/>

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-11-07 23:39:15 +00:00
Sandiyo Christan
c54ec52f0f fix(auth): limit failed TOTP attempts per account (#39993)
Gitea accepted unlimited wrong TOTP passcodes, so anyone holding an
account's password could enumerate the 6-digit code in minutes. This
allows 5 failed attempts per account, then refuses TOTP until 15 minutes
after the last one.

- The count lives in the cache, and the global lock spans the check,
validation and counting, so new sessions and concurrent requests cannot
widen it. With Redis, both are shared across instances
- Applies to every TOTP check: the web 2FA page, account recovery and
Basic auth with `X-Gitea-OTP` (API, git, packages)
- Only wrong or replayed passcodes count, server errors do not
- Scratch codes and WebAuthn stay usable as the recovery path while
locked
- Failures log `Failed authentication attempt for <user> from <ip>`,
which the fail2ban filter already matches

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-11-07 23:01:49 +00:00
silverwind
51879941dc enhance: improve commit and compare link rendering (#40154)
Commit, compare and SHA links render the SHA in code with the rest of
the URL after it, and links to other repositories get an `owner/repo@`
prefix like references do.

- Abbreviated SHAs, branch compares and `<url>` autolinks now render as
commit links
- Pull request commit URLs no longer break into an issue reference plus
stray text
- Only routes that take a SHA are shortened, so file and API URLs stay
plain

Replaces https://github.com/go-gitea/gitea/pull/36725

<img width="620" alt="Commit links before and after"
src="https://github.com/user-attachments/assets/5919c8a0-94e3-4148-ad11-6944bf790572"
/>
2026-11-07 22:43:07 +00:00
William Desportes
87a703819b fix(queue): bump levelqueue to v0.5.0 and remove workarounds (#40219)
levelqueue v0.5.1 fixes this upstream
(https://gitea.com/lunny/levelqueue/pulls/8), so this now only bumps the
dependency.

A push wrote "high" before the payload, so an interrupted push left an
id counted with no key. `LPop` then returned `ErrNotFound` without
advancing "low" and the wrapper retried forever: nothing popped, no
worker started, later items unreachable. All were only fixable by
clearing the queue, losing the backlog:
- https://github.com/go-gitea/gitea/issues/32006 — mail, 0 workers
- https://github.com/go-gitea/gitea/issues/28919 — mirror, "already in
queue"
- https://github.com/go-gitea/gitea/issues/33862 — archives hanging
- https://github.com/go-gitea/gitea/issues/33882 — self-healing request

v0.5.1 writes counter and payload in one batch, moves the counters past
missing keys, and repairs leftover unique-queue set entries, so affected
instances recover on their next pop.

No test: the behaviour lives in the dependency, which covers it in its
own `recover_test.go`.

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-11-07 22:26:53 +00:00
silverwind
e09813f472 docs: update AGENTS.md (#40270)
Some useful rules from my past experiences, feel free to suggest more.
Dark theme screenshots is subjective of course, but I prefer them.

---------

Signed-off-by: silverwind <me@silverwind.io>
2026-11-07 16:09:25 +00:00
Lazizbek Ergashev
884884308d fix(pull): don't add a timeline entry for pending API reviews (#40260)
Pending reviews created through the API went through `SubmitReview`,
which added a timeline entry and sent notifications before the review
was submitted, so submitting it later showed the review twice.

- Pending reviews skip the timeline entry and notifications until
submitted, like on GitHub
- `CreatePullReview` rejects comments without a `path` or a positive
line with 422, like GitHub
- Already duplicated reviews stay as they are

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

Assisted-by: Claude Code:claude-opus-5-5

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-07 14:19:50 +00:00
Renox
462ab3801a fix(web): don't block README images with REQUIRE_SIGNIN_VIEW = expensive (#40264)
Relax expensive endpoint list, then README can render its images.

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-11-07 13:34:01 +00:00
Mitrahsoft
3aca9c8fb7 enhance(actions): create commit status for workflow_dispatch runs (#39961)
Runs triggered by `workflow_dispatch` never created a commit status, so
the commit showed no sign that a workflow ran on it.

- Post the status on the run's commit, e.g. `ci / e2e
(workflow_dispatch)`. `schedule` runs stay unchanged.
- These statuses don't count for pull requests. They don't show in a
PR's checks and can't satisfy or fail its required checks, even through
broad globs like `ci / e2e*`.

Closes https://github.com/go-gitea/gitea/issues/39639

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-07 12:55:19 +00:00
Basavaraj S m
4f69599981 fix(repo): do not reuse pull mirror credentials for a new host (#39991)
Changing a pull mirror address with an empty password field carried the
stored credentials over to the new address. The password is write-only,
so a repo admin who did not set up the mirror could send the owner's
token to a host they control, or pull another repository on the same
host that the token can read.

Stored credentials are now reused only when the address is unchanged,
like a changed username already drops the stored password. The API
cannot change the address and push mirrors never reuse credentials, so
both are unaffected.

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-07 12:35:10 +00:00
TheFox0x7
dd4672607b chore: add report requirements (#40262)
Explicitly require version, reproduction and settings

---------

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: silverwind <me@silverwind.io>
2026-11-07 12:09:23 +00:00
wxiaoguang
a165fdb63c refactor: lazy init global regexp variables (#40269)
Gitea binary is an all-in-one program.

It doesn't make sense to compile dozens or even hundreds of regexps for
git hook sub commands.
2026-11-07 11:56:56 +00:00
Trung Hehe
6bc92f464a fix(org): enforce unique team names in the database (#40228)
Concurrent requests could create teams with the same name in one
organization, because only the service checked the name. Editing such a
team, or renaming a team to a taken name, then failed with a 500.

- Add a unique index on `team (org_id, lower_name)`. Migration 368
renames every duplicate except the oldest to a free `<name>-<n>` and
logs each rename. IDs, members, repositories and permissions are kept,
but lookups by the old name (CODEOWNERS, group-to-team sync, mentions,
URLs) now reach only the kept team.
- Team writes report a name rejected by the index as
`ErrTeamAlreadyExist`, so duplicates, concurrent ones included, return
409 from `POST /orgs/{org}/teams` (previously 422) and `PATCH
/teams/{id}`, and a form error in the web UI.

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

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-07 11:16:11 +00:00
silverwind
baccc2d7cd fix: truncate strings on character boundaries (#40267)
Byte slicing like `t.Description[:255]` can split a multi-byte character
and store invalid UTF-8. Team descriptions were also cut to 255 bytes
although their `MaxSize(255)` validation allows 255 characters.

Team descriptions now truncate by runes. Signup user agents, Actions log
lines and access log request IDs use `util.TruncateStringBytes`, which
now returns short strings untouched.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-11-07 11:15:28 +00:00
Michael Lopez
5d9bfa1e14 feat(packages): add private Terraform module registry (#38040)
Adds a Terraform module registry package type implementing the [module
registry
protocol](https://developer.hashicorp.com/terraform/internals/module-registry-protocol),
so `terraform init` and Terragrunt `tfr://` can resolve modules from
Gitea:

```hcl
module "vpc" {
  source  = "gitea.example.com/owner/vpc/aws"
  version = "~> 1.0"
}
```

- Modules are uploaded with `PUT
/api/packages/-/terraform/modules/{owner}/{name}/{provider}/{version}`
as a `.tar.gz` with the module at its root, stored verbatim and
immutable.
- `X-Terraform-Get` is a short-lived signed URL because Terraform sends
no credentials when downloading the archive.
- The package page shows install snippets, README, the root module's
inputs, outputs and providers, and `modules/*` submodules.
- Metadata is read by a small in-tree HCL scanner instead of
`hashicorp/hcl/v2`, which would add 5 modules and ~1.4 MB to the binary.

Docs: https://gitea.com/gitea/docs/pulls/433

---------

Signed-off-by: Michael Lopez <mickael.lopez@gmail.com>
Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-11-07 10:45:05 +00:00
Wei-Lun Li
7efb5c055f fix(markup): recognize issue references next to CJK punctuation (#40097)
Issue references next to non-ASCII characters, common in Chinese and
Japanese text, are ignored: `修复#1`, `(#2)` or `Closes #3,` create no
link or cross reference.

Like on GitHub, any non-ASCII character may now precede `#123`, `!123`
or `owner/repo#123`, and anything but a word character may follow it.
Dead trailing-space handling in the reference and mention search is
removed.

I used Claude Code to write this change.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-11-07 10:14:47 +00:00
silverwind
b23c70d439 fix: add missing checks to several handlers and services (#40120)
Several code paths skipped checks their siblings perform, or cleaned up
more than they validated.

- Official team review requests now count, and removing one no longer
marks an unrelated review official
- Release edits only delete files of validated attachments
- Rejected container uploads no longer delete an existing blob
- Attachment removal only removes the uploader's own pending uploads
- OpenID sign-in checks the allow and block lists against the normalized
identifier
- HTTP git access is rejected while a repository is being created, like
SSH

---------

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-11-07 09:43:39 +00:00
Mitrahsoft
b6c291f87a fix(api): honor branch protection force push and allowlist options on create and edit (#39990)
The branch protection API handled push and force push options
inconsistently. Create dropped force push unless push was enabled, and
edit ignored allowlist fields unless their parent was set to true in the
same request.

- Create treats force push independently of push, like edit and the web
UI. Enforcement is unchanged.
- Edit applies `enable_push_whitelist`, `enable_force_push_allowlist`
and both deploy key flags against the rule's resulting state, like the
deletion fields. `{"enable_push_whitelist": false}` now disables the
whitelist instead of being ignored.
- Documents that `enable_force_push_allowlist` requires
`enable_force_push` on create.

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

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-07 09:34:21 +00:00
Zettat123
c74f2f9216 fix(repo): mark repository as non-empty before git push returns (#40102)
The post-receive hook syncs pushed branches to the database, but
`is_empty` and the default branch of an empty repository were only
updated later by the `push_update` queue. Until then the API treated the
repository as empty (branch list `null`, `GET /branches/{branch}` 500),
and the queue overwrote the default branch chosen by the hook or set via
the API.

`SyncBranchesToDB` now initializes an empty repository and repairs a
default branch missing in git in its locked transaction, so the state is
final when `git push` returns. The push queue and web file edits retry
it while the repository is still empty.

- The default branch is the configured one if it exists, else `main`,
else `master`, else the first branch by name, like GitLab. HEAD follows
it.
- A first push of several branches no longer depends on push order. The
push that initializes a repository still prints no pull request hints,
like GitHub.

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-07 03:20:25 +00:00
GiteaBot
b8b270bac9 [skip ci] Updated translations via Crowdin 2026-11-07 00:31:59 +00:00
Alan Hsu
df4544a58b fix(web): handle TypeCastError when building form validation messages (#40212)
Fixes https://github.com/go-gitea/gitea/issues/40210

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-11-06 23:27:06 +00:00
silverwind
ede9ec3e10 fix: stop discarding file close and test setup errors (#39987)
- Fix
https://github.com/go-gitea/gitea/security/quality/rules/go%2Funhandled-writable-file-close,
ignored close errors hide failed flushes, which could lose rotated logs
and truncate authorized_keys
- Fix `TestProjects` fixture reset, from
https://github.com/go-gitea/gitea/security/quality/ai-findings,
`TruncateBeans` rejected the table names and the ignored error let the
test run against fixture data
2026-11-06 20:45:17 +00:00
silverwind
24700dca2a test(e2e): retry clipboard reads rejected by Firefox (#40155)
The Firefox mermaid e2e test fails intermittently with `Clipboard read
operation is not allowed.`, for example in
https://github.com/go-gitea/gitea/actions/runs/41281998298/job/124776072700.

Firefox rejects `readText()` when it overlaps the page's still pending
clipboard write, and `expect.poll` does not retry errors thrown by its
callback, so one overlapping read fails the test.
https://github.com/go-gitea/gitea/pull/39838 only removed the paste
prompt, not this rejection.

- A new `assertClipboardText` helper treats a rejected read as a
mismatch so it is polled again
- The bot token copy test had the same race and uses it too

_Written by Claude (Opus 5.5)._
2026-11-06 20:44:00 +00:00
silverwind
7e18e81c7d refactor: remove duplicated and dead Go code (#40244)
Pure refactors with no behavior change, mostly reusing existing helpers
and dropping duplicated or dead code. Net −222 lines.

- Reuse existing helpers instead of inlined copies: `getIssueIndex`,
`NewLabel`, `ToCommitUser`, `GetPackageFileDescriptors`,
`util.SplitTrimSpace`
- Merge copy-pasted blocks in review comment deletion, project deletion
and the pre/post-receive hook templates
- Drop dead code: an error `toStringKeys` never returned, impossible
`bytes.Buffer` write errors, redundant nil-map branches, a `base64`
wrapper
- Use stdlib where it fits: `strings.Cut`, `bytes.IndexAny`,
`cmp.Compare`, `min`/`max`, `slices.Concat`, map lookups instead of key
scans
2026-11-06 11:59:49 +00:00
Shivansh Garg
a5201dc427 feat(release): allow setting a release as the latest (#40214)
Gitea picks the latest release by commit date, while GitHub keeps an
explicit pointer, so repositories migrated from GitHub could show a
different latest release than their source. Fixes
https://github.com/go-gitea/gitea/issues/35273.

- Add a "Set as Latest Release" checkbox and GitHub's `make_latest` API
option. Without them, the newest stable release by commit date stays the
latest.
- Migrations from GitHub and Gitea carry over the source's latest
release.
- Migrated releases are dated by their commit like native ones, and
equally dated ones keep the source's order.

<img width="355" height="122" alt="Set as Latest Release checkbox"
src="https://github.com/user-attachments/assets/d35f5324-7ae0-4e9f-b73c-d3c90496fb00"
/>

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-06 08:23:13 +00:00
Lazizbek Ergashev
98c937170f fix(repo): reject unparsable required approvals in branch protection (#40213)
Saving a branch protection rule with a "Required approvals" value that
can't be parsed as an integer (like `99999999999999999999` or `abc`)
showed the success message and stored 0, because the binding error was
never checked. The form now uses `form-fetch-action` with
`GetFetchActionForm`, so invalid input is rejected and nothing is saved.
Dev mode still answers unparsable numbers with a 500 until
`TypeCastError` gets a message, see
https://github.com/go-gitea/gitea/pull/40212.

- Validation errors show on the page and highlight the offending field,
keeping the form input
- Negative approvals are rejected before any rule lookup, and the input
gets `min="0"`

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

Assisted-by: Claude Code:claude-opus-5-5

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-06 08:08:20 +00:00
silverwind
09922e4ae1 ci: update giteabot to v1.5.1, label conflict-free backports (#40247)
Bump `giteabot` to
[`v1.5.1`](https://github.com/go-gitea/giteabot/releases/tag/v1.5.1) to
label PRs with a `backport/<version>` label that would cherry-pick
cleanly onto the latest release branch with `backport/conflict-free`.

The backport workflow's concurrency group is now per ref, so release
branch pushes can't cancel a pending backport run from `main`.

_Written by Claude (Opus 5.5)._
2026-11-06 07:55:55 +00:00
Basavaraj S m
8d14c9af20 fix(pull): enforce user blocks on pull request reviews (#40194)
Code review never checked user blocks, so a user blocked by the poster
or repository owner could still post reviews, code comments and replies
through the web, API and email.

- `CreateCodeComment` and `SubmitReview` now check blocks, with the repo
admin exemption issue comments have.
- Official reviewers blocked only by the author can still review, so an
author can't keep a maintainer from requesting changes.
- Review requests are handled in
https://github.com/go-gitea/gitea/pull/39296.

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-06 07:31:58 +00:00
silverwind
87ebf3b1ba feat(auth)!: remove support for obsolete OpenID 2.0 draft (#40231)
Remove OpenID 2.0 authentication. The spec is an obsolete draft, major
providers dropped it years ago, and the code keeps needing security
fixes like https://github.com/go-gitea/gitea/pull/39642. OpenID Connect
and OAuth2 are unaffected. Docs: https://gitea.com/gitea/docs/pulls/633.

- The `[openid]` settings, OpenID sign-in, sign-up and account
association are gone
- A migration drops the `user_open_id` table with all user OpenID URIs
- Accounts registered via OpenID have a random password and regain
access via password reset

Fixes: https://github.com/go-gitea/gitea/issues/1310
Fixes: https://github.com/go-gitea/gitea/issues/3521
2026-11-06 07:21:19 +00:00
wxiaoguang
fd98ab5472 enhance(codeowners): support section / glob / file view display / skip review request (#40243)
Document: https://gitea.com/gitea/docs/pulls/634 ([doc PR
preview](955ae7d75c/docs/usage/repository/code-owners.md))

1. If the pattern starts with `*` or contains `/*`, use our glob package
to parse it as glob to regexp
* Make it easier to use. Otherwise it's really ugly to keep writing a
lot of backslash escapes
2. Add GitLab-style "section" support
   * Fix #28926

```codeowners
[Section] @user
*.js
```

3. Add our annotation support, at the moment only need to support
review-request=true/false
    * Fix #40241

```codeowners
## review-request=false # this is global

*.js $user ## review-request=true # this is line-level

[Section] @user ## review-request=true # this is section-level
*.md
```

4. Add GitHub/GitLab-style "show codeowner when viewing a file"
    * Fix #28137
2026-11-06 07:19:51 +00:00
silverwind
0d59b737aa fix: upgrade Alpine base packages in Docker images (#40252)
The `alpine:3.24` base image lags behind fixes published to the Alpine
repository.

Both images now run `apk upgrade` before `apk add`. Packages from `apk
add` are already current, so this only affects the preinstalled base
packages.

_Written by Claude (Opus 5.5)._
2026-11-06 07:19:07 +00:00
Giteabot
917badaae6 fix(deps): update module github.com/andybalholm/brotli to v1.2.7 [security] (#40246)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
|
[github.com/andybalholm/brotli](https://redirect.github.com/andybalholm/brotli)
| `v1.2.1` → `v1.2.7` |
![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fandybalholm%2fbrotli/v1.2.7?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fandybalholm%2fbrotli/v1.2.1/v1.2.7?slim=true)
|

---

### Excessive memory allocation when decompressing crafted streams in
github.com/andybalholm/brotli
BIT-brotli-2026-111497 / BIT-dotnet-2026-111497 /
BIT-dotnet-sdk-2026-111497 / BIT-node-2026-111497 /
BIT-node-min-2026-111497 / BIT-powershell-2026-111497 /
[CVE-2026-111497](https://nvd.nist.gov/vuln/detail/CVE-2026-111497) /
[GHSA-9v8h-qqgm-4m35](https://redirect.github.com/advisories/GHSA-9v8h-qqgm-4m35)
/
[GHSA-c9gr-gmg4-2vqv](https://redirect.github.com/advisories/GHSA-c9gr-gmg4-2vqv)
/ [GO-2026-6888](https://pkg.go.dev/vuln/GO-2026-6888) / PYSEC-2026-4322
/ PYSEC-2026-4323

<details>
<summary>More information</summary>

#### Details
In github.com/andybalholm/brotli before v1.2.7, the decompressor
allocates memory for Huffman tables and block-type trees on every
meta-block header without bounding the cumulative allocations relative
to the input size. A small crafted brotli stream containing many empty
meta-blocks with new Huffman trees can trigger hundreds of megabytes of
allocations per kilobyte of input, leading to denial of service.

#### Severity
Unknown

#### References
-
[https://github.com/andybalholm/brotli/security/advisories/GHSA-c9gr-gmg4-2vqv](https://redirect.github.com/andybalholm/brotli/security/advisories/GHSA-c9gr-gmg4-2vqv)
-
[33c2a98842)
-
[https://github.com/andybalholm/brotli/releases/tag/v1.2.7](https://redirect.github.com/andybalholm/brotli/releases/tag/v1.2.7)
-
[0ed4268a44)

This data is provided by
[OSV](https://osv.dev/vulnerability/GO-2026-6888) and the [Go
Vulnerability Database](https://redirect.github.com/golang/vulndb)
([CC-BY 4.0](https://redirect.github.com/golang/vulndb#license)).
</details>

---

### Release Notes

<details>
<summary>andybalholm/brotli (github.com/andybalholm/brotli)</summary>

###
[`v1.2.7`](https://redirect.github.com/andybalholm/brotli/compare/v1.2.6...v1.2.7)

[Compare
Source](https://redirect.github.com/andybalholm/brotli/compare/v1.2.6...v1.2.7)

###
[`v1.2.6`](https://redirect.github.com/andybalholm/brotli/compare/v1.2.5...v1.2.6)

[Compare
Source](https://redirect.github.com/andybalholm/brotli/compare/v1.2.5...v1.2.6)

###
[`v1.2.5`](https://redirect.github.com/andybalholm/brotli/compare/v1.2.4...v1.2.5)

[Compare
Source](https://redirect.github.com/andybalholm/brotli/compare/v1.2.4...v1.2.5)

###
[`v1.2.4`](https://redirect.github.com/andybalholm/brotli/compare/v1.2.3...v1.2.4)

[Compare
Source](https://redirect.github.com/andybalholm/brotli/compare/v1.2.3...v1.2.4)

###
[`v1.2.3`](https://redirect.github.com/andybalholm/brotli/compare/v1.2.2...v1.2.3)

[Compare
Source](https://redirect.github.com/andybalholm/brotli/compare/v1.2.2...v1.2.3)

###
[`v1.2.2`](https://redirect.github.com/andybalholm/brotli/compare/v1.2.1...v1.2.2)

[Compare
Source](https://redirect.github.com/andybalholm/brotli/compare/v1.2.1...v1.2.2)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Mend Renovate
CLI](https://redirect.github.com/renovatebot/renovate).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xOTAuMSIsInVwZGF0ZWRJblZlciI6IjQ0LjE5MC4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2026-11-06 01:55:15 +00:00
Lazizbek Ergashev
288d54951e fix(perm): keep read access for restricted collaborators on public repos (#40234)
Adding a restricted user as a read collaborator to a public repo gave
them no access when the owner isn't fully public, like a limited org.
Restricted users get no default read there, and `RecalculateUserAccess`
dropped their read access row.

- `RecalculateUserAccess` now keeps the same read rows as
`refreshAccesses`, on org-owned repos and for restricted users.
- Re-saving an unchanged collaborator permission recalculates access,
repairing collaborators affected before this fix.

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

Assisted-by: Claude Code:claude-opus-5-5

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-05 23:48:55 +00:00
silverwind
b81b50be55 fix(build): pass # test patterns to go test -run unchanged (#39977)
The `test-backend#` and `test-integration#` targets rewrote every `.`
into `/`, because GNU make pattern rules never match goals containing
`/`. That broke regexes like `make 'test-backend#TestA.*'`, which
silently selected subtests instead.

Static pattern rules built from the goals accept `/` directly, so the
pattern reaches `go test -run` unchanged. Subtests are now selected like
`make 'test-backend#TestA/sub'`, on GNU make 3.81 and 4.x.
2026-11-05 23:44:33 +00:00
silverwind
c62db5b2a3 perf: reduce git processes and queries on pull request view (#40224)
Removes redundant work from the pull request conversation tab.

- Base and head refs resolve through the `cat-file` batch of the git
repos the callers already hold, instead of two `git rev-parse`
processes, reducing the number of git processes from 9 to 7 per page,
about 17 ms faster locally.
- Sidebar reviewers are batch-loaded and code-review comments reuse
their loaded poster, reducing the number of SQL queries from 387 to 358
on a PR with 169 timeline entries and 15 reviews.

Also fixes the commit list of merged pull requests, which resolved the
merge base as a branch name and returned 500.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-11-05 21:58:17 +00:00
Andrew Barnes
4a9d49bb46 fix(dump): --file extension is ignored when --type is not set (#40235)
`gitea dump -f backup.tar.gz` without `--type` wrote a zip archive under
that name. Detecting the type from the file name was requested in
https://github.com/go-gitea/gitea/issues/24622 and intended in
https://github.com/go-gitea/gitea/pull/30240, but the check read the
still-empty result instead of `--file`, so every name fell back to zip.

A supported extension now selects the type, other names like `backup` or
`gitea.bak` still default to zip. An explicit `--type` still wins, and
its help text now mentions the detection.

AI disclosure: written with Claude Code (claude-opus-5-5).

---------

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: silverwind <me@silverwind.io>
2026-11-05 18:30:31 +00:00
silverwind
b7c1fb2a39 enhance: enable DEFAULT_DELETE_BRANCH_AFTER_MERGE by default for new repos (#39975)
Switch the `DEFAULT_DELETE_BRANCH_AFTER_MERGE` default from
https://github.com/go-gitea/gitea/pull/36917 to `true`, saving users a
click on merge for the most common action that deletes the PR branch on
merge.

Docs: https://gitea.com/gitea/docs/pulls/608
2026-11-05 18:26:32 +00:00
rafał przypek
3b9b8a8976 fix(packages): return empty Chef dependencies instead of null (#40237)
Fixes #40230

Cookbooks without dependencies are served with `"dependencies": null`
from the universe and version metadata endpoints. This happens because
`omitempty` drops the empty map when the metadata is stored. Chef
clients iterate over this value and Policyfile resolution crashes.

Supermarket returns `{}` for these cookbooks, Gitea now does too. The
fix checks whether dependencies are `nil` while building response,
returns an empty map if true. This way cookbooks that are already stored
are covered without a re-upload.

Tested with the Chef integration test and `chef-cli install`.

Assisted by Claude Code (claude-opus-5-5).
2026-11-05 15:19:09 +00:00
Lunny Xiao
565f1b7d52 fix(migrations): Do not fail Bitbucket Server migration when pull request activities cannot be loaded (#40205)
Bitbucket Server can answer 500 for `/pull-requests/{id}/activities` of
some pull requests. That error aborted the whole repository migration:

`bitbucket server API .../pull-requests/128/activities returned status
500`

Now a 500 is logged as a warning and that pull request's comments,
reviews and merge commit are skipped, cached so the endpoint is not hit
again. Other errors are still returned, so the migration retries them as
before.

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-05 07:28:12 +00:00
silverwind
e87ac3a0f4 chore(deps): update go.yaml.in/yaml/v4 to v4.0.0 (#40222)
Update go-yaml from the rc.6 pseudo-version to the stable v4.0.0
release, and actionslib to v1.5.2, which does the same in
https://gitea.com/gitea/actionslib/pulls/32. The bump also brings in
actionslib v1.5.1.

v4 double-quotes the `on` key like yaml.v3 did, so one jobparser fixture
changes. Runner counterpart: https://gitea.com/gitea/runner/pulls/1381.

Fixes: https://github.com/go-gitea/gitea/issues/40145
2026-11-05 07:25:10 +00:00
GiteaBot
4477d47d07 [skip ci] Updated translations via Crowdin 2026-11-04 20:23:49 +00:00
Mark
bfbf8cd366 fix: fix missing accessible name on static Fomantic combobox dropdowns (#40163)
Fixes https://github.com/go-gitea/gitea/issues/40107

Static Fomantic dropdowns get `role="combobox"` or `role="menu"`, and
neither role takes its accessible name from child contents (`nameFrom:
author` only). Dropdowns like Filter and Sort on `/explore/repos`
without `data-tooltip-content`, `aria-label` or `aria-labelledby`
therefore had an empty accessible name.

If the focusable element still has no `aria-label` or `aria-labelledby`
after the tooltip check, link `aria-labelledby` to the dropdown's direct
`.text` or `span` child. Selection dropdowns are skipped because their
`.text` holds the current value, not a label.

---------

Co-authored-by: Mark <14371969+malfunctionAI@users.noreply.github.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-11-04 19:55:29 +00:00
silverwind
dfe6db6149 fix(web): build issue popup request from parsed link (#40207)
The issue reference popup built its `/info` request from the raw link
path, so a link ending in `/owner/repo/issues/N` queried an unrelated
location.

It now builds the request from the owner, repo, type and index parsed
from the link, with `parseIssueHref` anchored to the pathname under
`appSubUrl`.
2026-11-04 19:40:27 +00:00
Lazizbek Ergashev
d1c980cf8b fix(web): inline the site manifest (#40198)
Inline the web app manifest as a `data:` URL instead of serving it from
`/assets/site-manifest.json`. That endpoint has been linked through
`STATIC_URL_PREFIX` since https://github.com/go-gitea/gitea/pull/37405
and fails to load when:

1. a reverse proxy checks auth on every request, because browsers fetch
manifests without cookies
2. `STATIC_URL_PREFIX` serves the `public/` directory from disk, which
has no such file
3. `STATIC_URL_PREFIX` is cross-origin, because the endpoint sends no
CORS headers

The manifest is built per request so `start_url` follows
`PUBLIC_URL_DETECTION`. A reverse proxy that adds a stricter CSP must
allow `manifest-src data:`.

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

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-04 18:11:25 +00:00
silverwind
1152bc0cf2 fix(actions): evaluate job if: against needs finished in the same pass (#40126)
The job emitter resolves a dependency chain in several rounds in memory,
but a job's `if:` reads its needs' results from the database, where a
need skipped or failed earlier in the same pass was still blocked. A job
with `if: always() && needs.build.result == 'skipped'` was therefore
skipped instead of started.

Finished statuses are now persisted at the end of each round, which also
replaces the separate handling of skipped and failed reusable workflow
callers.
2026-11-04 18:10:56 +00:00
silverwind
159e6d7e75 test(actions): run repo Actions API tests as a non-owner repo admin (#40183)
The repo-level runner, secret and variable API tests ran as the repo
owner, who bypasses the repo admin check. That's how the regression
fixed in https://github.com/go-gitea/gitea/pull/40174 slipped through.
As suggested in
https://github.com/go-gitea/gitea/pull/40174#issuecomment-6591991824,
they now run as an admin collaborator, which replaces the duplicate
subtest added there.

The tests are also trimmed to one case per status code, because
`services/secrets/validation_test.go` already unit tests name
validation.
2026-11-04 17:34:29 +00:00
Paul van der lei
f29eedad83 fix(pull): code owner team is requested again after a member reviewed (#40187)
When a member of a CODEOWNERS team approves or rejects a pull request,
`SubmitReview` removes the team's review request. On the next push,
`PullRequestCodeOwnersReview` finds no request for that team and adds it
again, so every team member except the author gets a new "Requested to
review" mail. This happens even when the push doesn't change the PR's
diff (for example, merging the base branch into the PR branch) and the
approval is still valid.

Individual code owners don't have this problem: they are skipped when
they already have a review. This PR applies the same rule to teams. A
team is not requested again while one of its members has an approve or
reject review on the PR that hasn't been dismissed. If that review is
dismissed (for example by "Dismiss stale approvals"), the team is
requested again as before.

Testing: `TestPullView_CodeOwnerTeamAnsweredByMember` creates an org
repo with `README.md @org3/team1`, opens a PR, and checks three things:
the team is requested; after a member approves, a code owner pass (what
a push runs) doesn't request the team again; after the approval is
dismissed, the team is requested again. The test fails without the
change. `make lint-backend` and the `TestPull*` integration tests pass
locally on SQLite.

AI disclosure: I used Claude to help trace the cause and draft the
change, test and description. I reviewed and tested it myself and can
explain every line.

Fixes #34884.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-11-04 17:34:13 +00:00
Basavaraj S m
e9c5804075 fix(issues): hide unreadable issue types in search and milestone views (#40192)
The repository issue search JSON route sits behind the issues reader
check only, and the milestone page is open to anyone who can read either
issues or pull requests, since the issue routes have to tolerate pull
request permissions. Neither handler narrows the issue type to what the
viewer may read: the search route takes `type=pulls` straight from the
query or lists both types when it is absent, and the milestone page
always lists both. A team member granted only the Issues unit on a
private repository can therefore fetch every pull request's title and
body as JSON and see pull requests on milestone pages, while a
pull-requests-only reader sees issues there. I noticed it while
comparing these routes with the API `ListIssues` handler and the web
issue suggestions endpoint, which both already narrow the type. The
narrowing now lives in one small helper used by all three handlers, with
the search route answering 404 for a requested type the viewer cannot
read, like the API does. A unit test covers both handlers with an
issues-only permission.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-11-04 17:31:24 +00:00
Shivansh Garg
be297b7e05 fix(webhook): fire repository event on repository transfer (#40200)
Transferring a repository fired no `repository` webhook. It now sends a
`transferred` action shaped like GitHub's, with the previous owner in
`changes.owner.from.user` or `changes.owner.from.organization`. Like
GitHub, it goes to the new owner's webhooks, plus the repository's own
and system webhooks.

- Chat webhooks render "Repository transferred from <owner>".
- Actions workflows are unaffected, no trigger matches `repository`
events.

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

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-04 17:25:51 +00:00
Lunny Xiao
fca64e7330 perf: reuse request-scoped git repo in pull request conversion (#40208)
Reuse the request-scoped base git repository in `ToAPIPullRequest`,
`ToPayloadPullRequest` and `ToAPIPullRequests` instead of reopening it
for each conversion, so conversions share the request's `cat-file`
process.

- Same-repo PRs reuse the base repository for the head instead of
opening it a second time.
- Drop a redundant second open of the base repository.
- Fork head repositories stay opened per call. A request-cached fork
keeps its `cat-file` running until the request ends, which blocks
deleting the fork on Windows and keeps one process per fork alive in
batch status changes.

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-04 17:24:49 +00:00
Myers Carpenter
58279e39fd fix(actions): match pull_request unlabeled on label removal (#40215)
Fixes https://github.com/go-gitea/gitea/issues/34317

Removing a label from a pull request still started workflows with
`types: [labeled]` and never matched `types: [unlabeled]`.
https://github.com/go-gitea/gitea/pull/34316 derived
`labeled`/`unlabeled` from the added and removed labels for issues only,
while pull requests kept mapping every `label_updated` to `labeled`.
Both events now share one conversion, so they cannot drift apart again.

Two other label changes also triggered the wrong workflows:

- Clearing all labels started no `unlabeled` workflow, because the
Actions notifier ignored it
- Replacing labels reported every old label as removed, so a replace
that kept a label also matched `unlabeled`

AI disclosure: written with Claude Code (claude-opus-5-5), session
https://claude.ai/code/session_01Kw9ySGNd4ynmyX9GcVdUfw

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-04 17:24:33 +00:00
Nico Schlömer
ab295e62e4 fix(markup): make heading anchors match GitHub for whitespace and combining marks (#40195)
Heading anchors now keep the same characters as GitHub: letters, marks,
decimal and letter numbers, connector punctuation and alphabetic
symbols, with ASCII spaces turned into `-`. The rule follows
[github-slugger](https://github.com/Flet/github-slugger), whose fixtures
are generated from real GitHub renders.

- Other whitespace, including newlines in multi-line headings, is
dropped instead of becoming `-`, so `1<em space>ab` gets `#1ab`
- Combining marks are kept, so `हिन्दी` no longer becomes `हनद`
- Other numbers like `²` and `①` are dropped

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

This PR was written with Claude Code.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-11-04 05:51:12 +00:00
wxiaoguang
d919301d36 refactor: remove UnmarshalHandleDoubleEncode patch and fix repo unit TODO (#39998)
Remove the `UnmarshalHandleDoubleEncode` runtime workaround for an old
xorm bug, and normalize legacy double-encoded JSON in `repo_unit.config`
and `login_source.cfg` with a migration instead.

- Config `FromDB` no longer fails on corrupted values. A broken auth
source is deactivated, so admins can still fix it in the admin panel
- Remove `ProjectsConfig.GetProjectsMode`, `FromDB` already defaults
`ProjectsMode`

Actually it doesn't need so many changes, just because AI thinks it
should handle "corrupted database value" for auth sources.

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-11-04 05:28:49 +00:00
Alan Hsu
666e6e2d17 fix(web): allow clearing the full name in profile settings (#40186)
Clearing "Full Name" in the profile settings shows success but keeps the
old value: since #31959 it is only updated when non-empty, because the
input is disabled (not submitted) when `change_full_name` is disabled.

Use `*string` for the field, so a missing field keeps the value and an
empty one clears it. With `change_full_name` disabled, any submitted
value (including empty) is now rejected. Not `optional.Option[string]`,
because `MaxSize` would count slice elements instead of characters.

Fixes #40185

---
AI disclosure: written with Claude Code (claude-opus-5-5) and tested
locally.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-11-04 01:42:41 +00:00
Jon Fuller
4d00ed2a1e fix(notifications): avoid deadlock when creating issue notifications concurrently (#40166)
Concurrent `notification-service` workers can deadlock in the
transaction for-loop.

The transaction is useless, so it is removed to avoid deadlock.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-11-03 16:33:39 +00:00
silverwind
ce0da8a378 build: cross-compile in docker (#40136)
Building the backend under QEMU takes 46 to 54 minutes per foreign
platform in the nightly image build, against 7 minutes natively. The
backend stage now runs on the build platform and cross-compiles with
`GOOS`/`GOARCH`/`GOARM`, so every platform builds at native speed.

- `go generate` runs once instead of once per platform.
- The PR docker dry run builds all three platforms in a single job, on
any docker-related change.

---------

Signed-off-by: silverwind <me@silverwind.io>
2026-11-03 16:20:48 +00:00
Gaurav Dubey
f26a314636 fix(api): keep wiki page name when editing with an empty title (#40153)
Match `PATCH /repos/{owner}/{repo}/wiki/page/{pageName}` document "leave
empty to keep unchanged"

Fixes #39179

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-11-03 16:20:39 +00:00
Jon Fuller
5c68ba3e0a fix(repo): avoid deadlock when updating language stats concurrently (#40167)
The `repo_stats_update` queue only deduplicates waiting items, so
several workers can index the same repository at once, then
`UpdateLanguageStats` can lead to transaction deadlock.

Use a global lock to avoid concurrent updates, and also helps
https://github.com/go-gitea/gitea/pull/39342 to avoid duplicate updates.

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-11-03 16:18:27 +00:00
silverwind
78eb95ab19 fix: enable more djlint rules and improve keyboard navigation (#40081)
Enable djlint's [H018](https://djlint.com/docs/linter/#h018),
[H046](https://djlint.com/docs/linter/#h046) and
[H054](https://djlint.com/docs/linter/#h054) rules, fix their findings
and the keyboard navigation problems around them. H054 found links
nested in `<button>` dropdowns and radio inputs nested in links, both
invalid HTML.

- "Add File" and "More Operations" in the repo file list become `div`
dropdowns, the disabled state stays unfocusable
- The diff whitespace menu marks the current option with a check and
`aria-current` instead of radios, which made our dropdown aria patch
announce it as a combobox
- Sign-in moves "Forgot password?" below "Sign In" and drops the
positive tabindexes from https://github.com/go-gitea/gitea/pull/31689,
so the tab order follows the DOM: username, password, remember, sign in,
forgot
- The focus ring on primary, red and green buttons is near-white on dark
themes and near-black on light ones, the primary-colored ring was
invisible on primary buttons

<img width="500" alt="Whitespace menu, before and after"
src="https://github.com/user-attachments/assets/d22a423d-95e5-434c-aec4-59264c7132b7"
/>

<img width="600" alt="Sign-in form, main and this PR"
src="https://github.com/user-attachments/assets/d6780690-d906-4791-8a2b-8d80fb03067a"
/>

<img width="215" height="132" alt="Keyboard-focused primary and red
buttons, dark and light theme"
src="https://github.com/user-attachments/assets/a2400b18-4802-4383-8560-7e8267a7f02f"
/>

---------

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-11-03 15:30:59 +00:00
silverwind
5c723ee0fd refactor(markdown): remove goldmark-highlighting (#39751)
Render fenced code blocks with Gitea's own code block renderer instead
of `goldmark-highlighting`, using the managed lexer lookup from
`modules/highlight`. Indented and fenced blocks now share one rendering
path.

Rendered output is unchanged. The trailing newline is stripped on copy
in the frontend since https://github.com/go-gitea/gitea/pull/39755.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-11-03 12:25:31 +00:00
642 changed files with 9269 additions and 10292 deletions

View File

@@ -1,31 +0,0 @@
name: docker-dryrun
description: Composite action that performs the container build steps for a single platform.
inputs:
platform:
description: "The target platform: linux/amd64, linux/arm64, linux/riscv64."
required: true
runs:
using: composite
steps:
- uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
with:
cache-image: false
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Build regular image
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
platforms: ${{ inputs.platform }}
push: false
file: Dockerfile
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
- name: Build rootless image
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
platforms: ${{ inputs.platform }}
push: false
file: Dockerfile.rootless
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless

View File

@@ -9,7 +9,7 @@ inputs:
runs:
using: composite
steps:
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7
with:
node-version: 26

View File

@@ -4,7 +4,7 @@ description: Set up uv and python and restore the uv cache
runs:
using: composite
steps:
- uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
- uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
with:
python-version: 3.15 # renovate: datasource=python-version depName=python versioning=python
save-cache: ${{ github.workflow == 'cache-seeder' }}

View File

@@ -9,7 +9,7 @@ concurrency:
group: cron-renovate
env:
RENOVATE_VERSION: 44.190.1 # renovate: datasource=npm depName=renovate
RENOVATE_VERSION: 44.210.0 # renovate: datasource=npm depName=renovate
permissions:
contents: read
@@ -21,7 +21,7 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: renovatebot/github-action@2804199d260ad00c1fae0e9e842888369391c776 # v46.3.10
- uses: renovatebot/github-action@1c0cf989c02653fba0efe7ca5772338860ca71dc # v46.3.12
with:
renovate-version: ${{ env.RENOVATE_VERSION }}
configurationFile: renovate.json5

View File

@@ -15,7 +15,7 @@ jobs:
contents: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: crowdin/github-action@959dc74ac104447731cba99a016f85d0ae719788 # v3.5.0
- uses: crowdin/github-action@5b9790df740c85af5412a3a3a91b24988d5d324f # v3.5.1
with:
upload_sources: true
upload_translations: false

View File

@@ -15,8 +15,6 @@ on:
value: ${{ jobs.detect.outputs.templates }}
docker:
value: ${{ jobs.detect.outputs.docker }}
dockerfile:
value: ${{ jobs.detect.outputs.dockerfile }}
swagger:
value: ${{ jobs.detect.outputs.swagger }}
yaml:
@@ -42,7 +40,6 @@ jobs:
actions: ${{ steps.changes.outputs.actions }}
templates: ${{ steps.changes.outputs.templates }}
docker: ${{ steps.changes.outputs.docker }}
dockerfile: ${{ steps.changes.outputs.dockerfile }}
swagger: ${{ steps.changes.outputs.swagger }}
yaml: ${{ steps.changes.outputs.yaml }}
json: ${{ steps.changes.outputs.json }}
@@ -103,16 +100,11 @@ jobs:
docker:
- ".github/workflows/pull-docker-dryrun.yml"
- ".github/actions/docker-dryrun/**"
- "Dockerfile"
- "Dockerfile.rootless"
- "docker/**"
- "Makefile"
dockerfile:
- "Dockerfile"
- "Dockerfile.rootless"
swagger:
- "templates/swagger/*.json"
- "Makefile"

View File

@@ -4,13 +4,14 @@ on:
push:
branches:
- main
- release/v*
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
@@ -19,8 +20,8 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: go-gitea/giteabot@b830f7aa810e43afc1b2f8e8968a30754e6ff5d3 # v1.4.1
- uses: go-gitea/giteabot@15b18fdedd98a80d0968d0bd9db3a9428814b808 # v1.5.1
with:
github_token: ${{ secrets.GITEABOT_TOKEN }}
gitea_fork: giteabot/gitea
checks: backport
checks: backport,backport_conflict_free

View File

@@ -31,7 +31,7 @@ on:
checks:
description: Comma-separated list of non-backport checks to run
required: false
default: backport_branches,labels,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions
default: backport_branches,backport_conflict_free,labels,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions
permissions: {}
@@ -50,7 +50,7 @@ jobs:
pull-requests: write
statuses: write
steps:
- uses: go-gitea/giteabot@b830f7aa810e43afc1b2f8e8968a30754e6ff5d3 # v1.4.1
- uses: go-gitea/giteabot@15b18fdedd98a80d0968d0bd9db3a9428814b808 # v1.5.1
with:
github_token: ${{ secrets.GITEABOT_TOKEN }}
checks: ${{ github.event.inputs.checks || 'backport_branches,labels,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }}
checks: ${{ github.event.inputs.checks || 'backport_branches,backport_conflict_free,labels,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }}

View File

@@ -128,7 +128,7 @@ jobs:
ports:
- "9200:9200"
meilisearch:
image: getmeili/meilisearch:v1@sha256:1f578fa865f8251894b3bf38d6668d6209a3f451f8f65ab953e129d4c1db5ef4
image: getmeili/meilisearch:v1@sha256:88f2a08eb9532cc54a60d8fd014a23bf5cfda33d0c01ecbbaca9b4b463e17466
env:
MEILI_ENV: development # disable auth
ports:

View File

@@ -14,34 +14,26 @@ jobs:
files-changed:
uses: ./.github/workflows/files-changed.yml
# QEMU-based build is slow (40-50 minutes), so run arm64 and riscv64 when dockerfile changes.
# Run amd64 when any docker-related files change, which is fast (4 minutes).
container-amd64:
container:
if: needs.files-changed.outputs.docker == 'true'
needs: [files-changed]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: ./.github/actions/docker-dryrun
- uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
with:
platform: linux/amd64
container-arm64:
if: needs.files-changed.outputs.dockerfile == 'true'
needs: [files-changed]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: ./.github/actions/docker-dryrun
cache-image: false
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Build regular image
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
platform: linux/arm64
container-riscv64:
if: needs.files-changed.outputs.dockerfile == 'true'
needs: [files-changed]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: ./.github/actions/docker-dryrun
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
- name: Build rootless image
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
platform: linux/riscv64
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
file: Dockerfile.rootless
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless

View File

@@ -1,10 +1,14 @@
- Never assume, verify before claiming
- List development targets with `make help`
- Before working on code, read the `docs` guides for every area touched: `guidelines-backend.md` for Go, `guidelines-frontend.md` for TS, CSS and Vue, both for templates, `testing.md` for tests, `development.md` for config options and migrations, `guidelines-refactoring.md` for refactors
- PR descriptions: minimal, only what and why, no task or file listings. Include screenshots for UI changes, before and after when modifying existing UI. Aim for less than 1000 characters
- Before designing a feature or behavior, check prior art in GitHub and GitLab
- Don't add code for theoretical edge cases, only for ones that can realistically occur. Ask humans to make decisions if the edge case matters, comment the edge case for why it can be ignored if it doesn't matter
- Write docs concisely and user-facing, skip implementation details
- PR descriptions: minimal, only what and why, no task or file listings, no headings. Aim for less than 1000 characters. References go at the end of the description like `Fixes: <url>`.
- PR descriptions and update comments: Include screenshots for UI changes, before and after side by side. Take them at 4x scale in dark theme, cropped to the changes with some padding, and embed them at half CSS size
- Reference issues and PRs by full URL, not by number
- Use Conventional Commits for commit messages and PR titles, plus Gitea's `enhance` type for user-facing enhancements
- Add an `Assisted-by: AGENT_NAME:MODEL_VERSION` trailer to commit messages, never `Co-Authored-By` or `Signed-off-by`
- Add an `Assisted-by: AGENT_NAME:MODEL_VERSION` trailer to commit messages and PR descriptions, never `Co-Authored-By` or `Signed-off-by`
- Attribute agent authorship on one trailing line in issue and PR comments, never as a PR description section
- Never rewrite git history unless asked, update PRs with new commits and normal push
- Comments: write almost none, short and preferably same-line, explaining why for a future reader. Never narrate code, the change or the prompt. Preserve existing ones that still apply. If you need to write a paragraph-long comment, rethink your implementation, it is likely too complicated

View File

@@ -8,8 +8,8 @@ RUN --mount=type=cache,target=/root/.local/share/pnpm/store pnpm install --froze
COPY --exclude=.git/ . .
RUN make frontend
# Build backend for each target platform
FROM docker.io/library/golang:1.27-alpine3.24 AS build-env
# Cross-compile backend on the native platform to avoid slow QEMU emulation
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.27-alpine3.24 AS build-env
ARG GITEA_VERSION
ARG TAGS=""
@@ -28,11 +28,15 @@ RUN go mod download
# ".git" directory is mounted separately later only for version data extraction.
COPY --exclude=.git/ . .
COPY --from=frontend-build /src/public/assets public/assets
RUN --mount=type=cache,target="/root/.cache/go-build" make generate-backend
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
# Build gitea, .git mount is required for version data
RUN --mount=type=cache,target="/root/.cache/go-build" \
--mount=type=bind,source=".git/",target=".git/" \
make backend
GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#v} make gitea
COPY docker/root /tmp/local
@@ -48,7 +52,7 @@ FROM docker.io/library/alpine:3.24 AS gitea
EXPOSE 22 3000
RUN apk --no-cache add \
RUN apk --no-cache upgrade && apk --no-cache add \
bash \
ca-certificates \
curl \

View File

@@ -8,8 +8,8 @@ RUN --mount=type=cache,target=/root/.local/share/pnpm/store pnpm install --froze
COPY --exclude=.git/ . .
RUN make frontend
# Build backend for each target platform
FROM docker.io/library/golang:1.27-alpine3.24 AS build-env
# Cross-compile backend on the native platform
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.27-alpine3.24 AS build-env
ARG GITEA_VERSION
ARG TAGS=""
@@ -27,11 +27,15 @@ RUN go mod download
# See the comments in Dockerfile
COPY --exclude=.git/ . .
COPY --from=frontend-build /src/public/assets public/assets
RUN --mount=type=cache,target="/root/.cache/go-build" make generate-backend
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
# Build gitea, .git mount is required for version data
RUN --mount=type=cache,target="/root/.cache/go-build" \
--mount=type=bind,source=".git/",target=".git/" \
make backend
GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#v} make gitea
COPY docker/rootless /tmp/local
@@ -43,7 +47,7 @@ FROM docker.io/library/alpine:3.24 AS gitea-rootless
EXPOSE 2222 3000
RUN apk --no-cache add \
RUN apk --no-cache upgrade && apk --no-cache add \
bash \
ca-certificates \
dumb-init \

View File

@@ -1,6 +1,6 @@
DIST := dist
DIST_DIRS := $(DIST)/binaries $(DIST)/release
HASH := \#
GO ?= go
SHASUM ?= shasum -a 256
@@ -8,7 +8,7 @@ AIR_PACKAGE ?= github.com/air-verse/air@v1.67.4 # renovate: datasource=go
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v4/cmd/editorconfig-checker@v4.0.2 # renovate: datasource=go
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.15.0 # renovate: datasource=go
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.17 # renovate: datasource=go
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.36.6 # renovate: datasource=go
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.36.7 # renovate: datasource=go
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.9.0 # renovate: datasource=go
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 # renovate: datasource=go
SHELLCHECK_IMAGE ?= docker.io/koalaman/shellcheck:v0.11.0@sha256:61862eba1fcf09a484ebcc6feea46f1782532571a34ed51fedf90dd25f925a8d # renovate: datasource=docker
@@ -385,10 +385,11 @@ test-check:
exit 1; \
fi
.PHONY: test-backend\#%
test-backend\#%:
TEST_BACKEND_GOALS := $(filter test-backend$(HASH)%,$(MAKECMDGOALS))
.PHONY: $(TEST_BACKEND_GOALS)
$(TEST_BACKEND_GOALS): test-backend$(HASH)%:
@echo "Running go test with -tags '$(TAGS)'..."
@$(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' -run '$(subst .,/,$*)' $(GO_TEST_PACKAGES)
@$(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' -run '$*' $(GO_TEST_PACKAGES)
.PHONY: coverage
coverage:
@@ -443,9 +444,10 @@ test-integration: $(EXECUTABLE)
test-integration-compile:
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -c -o /dev/null gitea.dev/tests/integration
.PHONY: test-integration\#%
test-integration\#%: $(EXECUTABLE)
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -run '$(subst .,/,$*)' gitea.dev/tests/integration
TEST_INTEGRATION_GOALS := $(filter test-integration$(HASH)%,$(MAKECMDGOALS))
.PHONY: $(TEST_INTEGRATION_GOALS)
$(TEST_INTEGRATION_GOALS): test-integration$(HASH)%: $(EXECUTABLE)
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -run '$*' gitea.dev/tests/integration
.PHONY: test-migration
test-migration: migrations.integration.test migrations.individual.test

View File

@@ -14,12 +14,28 @@ Reports must be filed against a supported version. Before reporting, verify that
## Reporting a Vulnerability
Please **DO NOT** file a public issue. Instead, report the vulnerability privately by opening a [GitHub security advisory](https://github.com/go-gitea/gitea/security/advisories/new), or optionally by sending your report to `security@gitea.io`.
Please **DO NOT** file a public issue. Instead, report the vulnerability privately by opening a [GitHub security advisory](https://github.com/go-gitea/gitea/security/advisories/new), or optionally by sending your report to `security@gitea.io`. Former is preferred if given a choice.
If reporting via GitHub, open an advisory per found issue. You can group the found issues if they relate to a single area (same feature, same fault present in multiple routes) but do not group unrelated reports into one as it makes it much harder to assess and track for duplicates.
Do not use LLM to write the reports. It's meant for people to read so take your time and use your own words to write it. If you have used LLM to find the bug, be transparent about it and verify that it is not hallucination before submitting the report.
If your report turns out to be a duplicate, you will be credited in the original report.
### Report requirements
When filing the report please document it. Reports must include:
- Version or a range of versions it was successfully tested against. If reproducible against the `main` branch, a commit hash is also acceptable. Ambiguous versions like `latest` are not acceptable.
- Settings used, especially when they diverge from the defaults
- Reproduction steps from freshly installed instance. If the fault is only reproducible through upgrade from older version, reproduction should state that.
Take your time to format it properly as it'll be much easier to read. Prefer a concise writing style.
When submitting an advisory, it should have the `gitea.dev` go package as package `name.cd`
### Triage process
Github does not have any search or tagging in GHSA so in most cases your report will leave triage state when we have a fix for it in works for next release.
This helps us keep track of items fixed and pending release and separate them from things that we either haven't looked at yet or have not fixed.
## Protecting Security Information
Due to the sensitive nature of security information, you can use the below GPG public key to encrypt your mail body.

View File

@@ -94,11 +94,6 @@
"path": "github.com/andybalholm/brotli/LICENSE",
"licenseText": "Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
},
{
"name": "github.com/andybalholm/brotli/flate",
"path": "github.com/andybalholm/brotli/flate/LICENSE",
"licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
},
{
"name": "github.com/andybalholm/cascadia",
"path": "github.com/andybalholm/cascadia/LICENSE",
@@ -959,16 +954,6 @@
"path": "github.com/x448/float16/LICENSE",
"licenseText": "MIT License\n\nCopyright (c) 2019 Montgomery Edwards⁴⁴⁸ and Faye Amacker\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
},
{
"name": "github.com/yohcop/openid-go",
"path": "github.com/yohcop/openid-go/LICENSE",
"licenseText": "Copyright 2015 Yohann Coppel\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
},
{
"name": "github.com/yuin/goldmark-highlighting/v2",
"path": "github.com/yuin/goldmark-highlighting/v2/LICENSE",
"licenseText": "MIT License\n\nCopyright (c) 2019 Yusuke Inuzuka\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
},
{
"name": "github.com/yuin/goldmark",
"path": "github.com/yuin/goldmark/LICENSE",

View File

@@ -16,6 +16,7 @@ import (
"regexp"
"slices"
"strings"
"sync"
"gitea.dev/modules/json"
)
@@ -31,7 +32,9 @@ type emoji struct {
description string
}
var slugRe = regexp.MustCompile(`[^a-z0-9]+`)
var slugRe = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`[^a-z0-9]+`)
})
func main() {
if err := generate(); err != nil {
@@ -108,7 +111,7 @@ func fetchEmojis(existingAliases map[string][]string) ([]*emoji, error) {
for _, e := range emojis {
e.Aliases = existingAliases[e.Emoji]
if e.Aliases == nil {
e.Aliases = []string{strings.Trim(slugRe.ReplaceAllString(strings.ReplaceAll(strings.ToLower(e.description), "’", ""), "_"), "_")}
e.Aliases = []string{strings.Trim(slugRe().ReplaceAllString(strings.ReplaceAll(strings.ToLower(e.description), "’", ""), "_"), "_")}
}
delete(existingAliases, e.Emoji)
for _, alias := range e.Aliases {

View File

@@ -15,17 +15,24 @@ import (
"slices"
"sort"
"strings"
"sync"
)
// regexp is based on go-license, excluding README and NOTICE
// https://github.com/google/go-licenses/blob/master/licenses/find.go
// also defined in vite.config.ts
var licenseRe = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING).*$`)
// primaryLicenseRe matches exact primary license filenames without suffixes.
// When a directory has both primary and variant files (e.g. LICENSE and
// LICENSE.docs), only the primary files are kept.
var primaryLicenseRe = regexp.MustCompile(`^(?i)(LICEN[SC]E|COPYING)$`)
var globalVars = sync.OnceValue(func() (ret struct {
licenseRe *regexp.Regexp
primaryLicenseRe *regexp.Regexp
},
) {
// regexp is based on go-license, excluding README and NOTICE
// https://github.com/google/go-licenses/blob/master/licenses/find.go
// also defined in vite.config.ts
ret.licenseRe = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING).*$`)
// primaryLicenseRe matches exact primary license filenames without suffixes.
// When a directory has both primary and variant files (e.g. LICENSE and
// LICENSE.docs), only the primary files are kept.
ret.primaryLicenseRe = regexp.MustCompile(`^(?i)(LICEN[SC]E|COPYING)$`)
return ret
})
// ignoredNames are LicenseEntry.Name values to exclude from the output.
var ignoredNames = map[string]bool{
@@ -144,7 +151,7 @@ func scanDirForLicenses(dir, modulePath, moduleRoot string) []LicenseEntry {
continue
}
name := entry.Name()
if !licenseRe.MatchString(name) {
if !globalVars().licenseRe.MatchString(name) {
continue
}
if excludedExt[strings.ToLower(filepath.Ext(name))] {
@@ -181,7 +188,7 @@ func scanDirForLicenses(dir, modulePath, moduleRoot string) []LicenseEntry {
var primary []LicenseEntry
for _, e := range entries {
fileName := e.Path[strings.LastIndex(e.Path, "/")+1:]
if primaryLicenseRe.MatchString(fileName) {
if globalVars().primaryLicenseRe.MatchString(fileName) {
primary = append(primary, e)
}
}

View File

@@ -8,6 +8,7 @@ import (
"regexp"
"sort"
"strings"
"sync"
"gitea.dev/modules/json"
@@ -16,11 +17,19 @@ import (
"github.com/getkin/kin-openapi/openapi3"
)
// rxDeprecated matches "deprecated" as a word at the start of a description
// or preceded by whitespace/punctuation that indicates a leading marker (e.g.
// "Deprecated: true", "deprecated (use X instead)"). Rejects negated phrases
// like "not deprecated" or "previously deprecated, now supported".
var rxDeprecated = regexp.MustCompile(`(?i)(?:^|[\n.;])\s*deprecated\b`)
var globalVars = sync.OnceValue(func() (ret struct {
rxDeprecated *regexp.Regexp
rxSwaggerEnum *regexp.Regexp
},
) {
// rxDeprecated matches "deprecated" as a word at the start of a description
// or preceded by whitespace/punctuation that indicates a leading marker (e.g.
// "Deprecated: true", "deprecated (use X instead)"). Rejects negated phrases
// like "not deprecated" or "previously deprecated, now supported".
ret.rxDeprecated = regexp.MustCompile(`(?i)(?:^|[\n.;])\s*deprecated\b`)
ret.rxSwaggerEnum = regexp.MustCompile(`swagger:enum\s+(\w+)`)
return ret
})
// Convert parses a Swagger 2.0 spec and returns an OAS3 spec, applying
// Gitea-specific post-processing: file-schema fixups, URI formats,
@@ -158,7 +167,7 @@ func addDeprecatedFlags(doc *openapi3.T) {
if propRef == nil || propRef.Value == nil || propRef.Ref != "" {
continue
}
if rxDeprecated.MatchString(propRef.Value.Description) {
if globalVars().rxDeprecated.MatchString(propRef.Value.Description) {
propRef.Value.Deprecated = true
}
}

View File

@@ -13,7 +13,6 @@ import (
"go/token"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
@@ -31,8 +30,6 @@ func EnumKey(values []any) string {
return strings.Join(strs, "|")
}
var rxSwaggerEnum = regexp.MustCompile(`swagger:enum\s+(\w+)`)
// ScanSwaggerEnumTypes walks .go files under each dir and returns a map from
// a canonical value-set key (see EnumKey) to the Go type names declared with
// // swagger:enum TypeName. Multiple type names per key are allowed (e.g.
@@ -146,7 +143,7 @@ func registerEnumAnnotation(doc *ast.CommentGroup, specs []ast.Spec, enumTypes m
if doc == nil {
return nil
}
matches := rxSwaggerEnum.FindStringSubmatch(doc.Text())
matches := globalVars().rxSwaggerEnum.FindStringSubmatch(doc.Text())
if len(matches) < 2 {
return nil
}

View File

@@ -49,11 +49,11 @@ func commonLdapCLIFlags() []cli.Flag {
},
&cli.StringFlag{
Name: "host",
Usage: "The address where the LDAP server can be reached.",
Usage: "Comma-separated LDAP hosts or ldap://, ldaps:// or ldap+starttls:// URLs, tried in order.",
},
&cli.IntFlag{
Name: "port",
Usage: "The port to use when connecting to the LDAP server.",
Usage: "The port to use when connecting to hosts given without a URL.",
},
&cli.StringFlag{
Name: "user-search-base",

View File

@@ -90,7 +90,7 @@ func newDumpCommand() *cli.Command {
},
&cli.StringFlag{
Name: "type",
Usage: `Dump output format, default to "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "),
Usage: `Dump output format, default to the --file extension if supported, else "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "),
},
},
}

View File

@@ -261,13 +261,7 @@ func extractAsset(c *cli.Command, d string, a assetFile, overwrite, rename bool)
perms = fi.Mode()
}
file, err := os.OpenFile(dest, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, perms)
if err != nil {
return fmt.Errorf("%s: %w", dest, err)
}
defer file.Close()
if _, err = file.Write(data); err != nil {
if err := os.WriteFile(dest, data, perms); err != nil {
return fmt.Errorf("%s: %w", dest, err)
}

View File

@@ -1145,11 +1145,6 @@ LEVEL = Info
;; The default Git object format of new repositories. Available values: sha1, sha256.
;DEFAULT_OBJECT_FORMAT = sha1
;;
;; Allow adoption of unadopted repositories
;ALLOW_ADOPTION_OF_UNADOPTED_REPOSITORIES = false
;;
;; Allow deletion of unadopted repositories
;ALLOW_DELETION_OF_UNADOPTED_REPOSITORIES = false
;; Don't allow download source archive files from UI
;DISABLE_DOWNLOAD_SOURCE_ARCHIVES = false
@@ -1242,7 +1237,7 @@ LEVEL = Info
;DELAY_CHECK_FOR_INACTIVE_DAYS = 7
;;
;; Set the default value for "Delete pull request branch after merge by default" for new repositories
;DEFAULT_DELETE_BRANCH_AFTER_MERGE = false
;DEFAULT_DELETE_BRANCH_AFTER_MERGE = true
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -1721,58 +1716,18 @@ LEVEL = Info
;; - manage_ssh_keys: a user cannot configure ssh keys
;; - manage_gpg_keys: a user cannot configure gpg keys
;; - manage_mfa: a user cannot configure mfa devices
;; - manage_credentials: a user cannot configure emails, passwords, or openid
;; - manage_credentials: a user cannot configure emails or passwords
;USER_DISABLED_FEATURES =
;; Comma separated list of disabled features ONLY if the user has an external login type (eg. LDAP, Oauth, etc.), could be "deletion", "manage_ssh_keys", "manage_gpg_keys", "manage_mfa", "manage_credentials". This setting is independent from `USER_DISABLED_FEATURES` and supplements its behavior.
;; - deletion: a user cannot delete their own account
;; - manage_ssh_keys: a user cannot configure ssh keys
;; - manage_gpg_keys: a user cannot configure gpg keys
;; - manage_mfa: a user cannot configure mfa devices
;; - manage_credentials: a user cannot configure emails, passwords, or openid
;; - manage_credentials: a user cannot configure emails or passwords
;; - change_username: a user cannot change their username
;; - change_full_name: a user cannot change their full name
;;EXTERNAL_USER_DISABLE_FEATURES =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;[openid]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; OpenID is an open, standard and decentralized authentication protocol.
;; Your identity is the address of a webpage you provide, which describes
;; how to prove you are in control of that page.
;;
;; For more info: https://en.wikipedia.org/wiki/OpenID
;;
;; Current implementation supports OpenID-2.0
;;
;; Tested to work providers at the time of writing:
;; - Any GNUSocial node (your.hostname.tld/username)
;; - Any SimpleID provider (http://simpleid.koinic.net)
;; - http://openid.org.cn/
;; - openid.stackexchange.com
;; - login.launchpad.net
;; - <username>.livejournal.com
;;
;; Whether to allow sign-in via OpenID
;ENABLE_OPENID_SIGNIN = false
;;
;; Whether to allow registering via OpenID
;;ENABLE_OPENID_SIGNUP = false
;;
;; Allowed URI patterns (POSIX regexp).
;; Space separated.
;; Only these would be allowed if non-blank.
;; Example value: trusted.domain.org trusted.domain.net
;WHITELISTED_URIS =
;;
;; Forbidden URI patterns (POSIX regexp).
;; Space separated.
;; Only used if WHITELISTED_URIS is blank.
;; Example value: loadaverage.org/badguy stackexchange.com/.*spammer
;BLACKLISTED_URIS =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;[oauth2_client]
@@ -2098,7 +2053,7 @@ LEVEL = Info
;ENABLED = true
;;
;; Comma-separated list of allowed issue/pull-request attachment file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
;ALLOWED_TYPES = .avif,.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.webp,.xls,.xlsx,.zip
;ALLOWED_TYPES = .avif,.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.jxl,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.webp,.xls,.xlsx,.zip
;;
;; Max size of each issue/pull-request attachment file. Defaults to 100MB
;MAX_SIZE = 100
@@ -2925,6 +2880,8 @@ LEVEL = Info
;LIMIT_SIZE_VAGRANT = -1
;; Maximum size of a Terraform state upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
;LIMIT_SIZE_TERRAFORM_STATE = -1
;; Maximum size of a Terraform module archive upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
;LIMIT_SIZE_TERRAFORM_MODULE = -1
;; Enable RPM re-signing by default. (It will overwrite the old signature ,using v4 format, not compatible with CentOS 6 or older)
;DEFAULT_RPM_SIGN_ENABLED = false
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -88,6 +88,13 @@ conditional `UPDATE` or a lock.
struct conditions and inserts of more than 1000 rows.
- Paginated queries need a unique tiebreaker like `id` for a stable order.
### Git
- Pass already opened repositories down or use `git.RepositoryFromContextOrOpen`
rather than reopening them or spawning extra `git` processes.
- If git command stderr output is not handled by the caller, then it needs to use RunWithStderr
to make sure the git error message is included in the returned error and displayed in the logs.
## Dependencies
Go dependencies are managed with [Go Modules](https://go.dev/ref/mod).

View File

@@ -24,6 +24,8 @@ Now, that's not a requirement anymore as our [backport bot](https://github.com/G
The `backport/manual` label signifies either that you want to backport the change yourself, or that there were conflicts when backporting, thus you **must** do it yourself.
The bot labels open PRs targeting `main` that have a `backport/<version>` label with `backport/conflict-free` when they would cherry-pick onto the latest release branch without conflicts.
### Format of backport PRs
The title of backport PRs should be

View File

@@ -211,7 +211,7 @@ export default defineConfig([
'@typescript-eslint/no-magic-numbers': [0],
'@typescript-eslint/no-meaningless-void-operator': [0],
'@typescript-eslint/no-misused-new': [2],
'@typescript-eslint/no-misused-promises': [2, {checksVoidReturn: {attributes: false, arguments: false}}],
'@typescript-eslint/no-misused-promises': [2, {checksVoidReturn: {attributes: false, arguments: false, properties: false}}],
'@typescript-eslint/no-misused-spread': [2],
'@typescript-eslint/no-mixed-enums': [0],
'@typescript-eslint/no-namespace': [2],

34
go.mod
View File

@@ -2,14 +2,14 @@ module gitea.dev
go 1.27
toolchain go1.27.3
toolchain go1.27.4
require (
connectrpc.com/connect/v2 v2.0.2
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
gitea.dev/actionslib v1.5.0
gitea.dev/sdk v1.5.0
gitea.com/lunny/levelqueue v0.5.1
gitea.dev/actionslib v1.5.2
gitea.dev/sdk v1.5.1
github.com/42wim/httpsig v1.3.0
github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432
github.com/Azure/go-ntlmssp v0.1.1
@@ -19,14 +19,14 @@ require (
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0
github.com/alecthomas/chroma/v2 v2.27.0
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
github.com/blevesearch/bleve/v2 v2.6.1
github.com/blevesearch/bleve/v2 v2.6.2
github.com/bohde/codel v0.2.0
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c
github.com/buildkite/terminal-to-html/v3 v3.17.3
github.com/caddyserver/certmagic v0.25.6
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20260812203852-971c0284dc33
github.com/coder/websocket v1.8.15
github.com/dlclark/regexp2/v2 v2.11.0
github.com/dlclark/regexp2/v2 v2.12.0
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
github.com/dustin/go-humanize v1.1.0
github.com/editorconfig/editorconfig-core-go/v2 v2.6.5
@@ -79,14 +79,12 @@ require (
github.com/stretchr/testify v1.12.1
github.com/syndtr/goleveldb v1.0.0
github.com/ulikunitz/xz v0.5.17
github.com/urfave/cli-docs/v3 v3.1.0
github.com/urfave/cli-docs/v3 v3.2.0
github.com/urfave/cli/v3 v3.14.0
github.com/wneessen/go-mail v0.8.1
github.com/yohcop/openid-go v1.0.1
github.com/yuin/goldmark v1.8.6
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
gitlab.com/gitlab-org/api/client-go/v3 v3.39.1
go.yaml.in/yaml/v4 v4.0.0-rc.6.0.20261013184558-04dd63131525
github.com/yuin/goldmark v1.8.7
gitlab.com/gitlab-org/api/client-go/v3 v3.43.0
go.yaml.in/yaml/v4 v4.0.0
golang.org/x/crypto v0.58.0
golang.org/x/image v0.47.0
golang.org/x/mod v0.42.0
@@ -109,13 +107,13 @@ require (
filippo.io/edwards25519 v1.2.0 // indirect
github.com/RoaringBitmap/roaring/v2 v2.18.2 // indirect
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/andybalholm/brotli v1.2.7 // indirect
github.com/andybalholm/cascadia v1.3.4 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.5 // indirect
github.com/blevesearch/bleve_index_api v1.4.1 // indirect
github.com/blevesearch/geo v0.2.6 // indirect
github.com/blevesearch/geo v0.2.7-0.20260831141219-9c3e3a4d84bc // indirect
github.com/blevesearch/go-faiss v1.1.5 // indirect
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
github.com/blevesearch/gtreap v0.1.1 // indirect
@@ -124,15 +122,15 @@ require (
github.com/blevesearch/segment v0.9.1 // indirect
github.com/blevesearch/snowballstem v0.9.0 // indirect
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
github.com/blevesearch/vellum v1.2.0 // indirect
github.com/blevesearch/vellum v1.2.1 // indirect
github.com/blevesearch/zapx/v11 v11.4.3 // indirect
github.com/blevesearch/zapx/v12 v12.4.3 // indirect
github.com/blevesearch/zapx/v13 v13.4.3 // indirect
github.com/blevesearch/zapx/v14 v14.4.3 // indirect
github.com/blevesearch/zapx/v15 v15.4.3 // indirect
github.com/blevesearch/zapx/v16 v16.3.4 // indirect
github.com/blevesearch/zapx/v17 v17.2.3 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.3 // indirect
github.com/blevesearch/zapx/v17 v17.2.4 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.4 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.4 // indirect
github.com/bodgit/windows v1.0.1 // indirect
@@ -223,7 +221,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.28.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
go.yaml.in/yaml/v3 v3.0.6 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/time v0.16.0 // indirect

84
go.sum
View File

@@ -8,14 +8,14 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:+wWBi6Qfruqu7xJgjOIrKVQGiLUZdpKYCZewJ4clqhw=
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY=
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o=
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4/go.mod h1:HBqmLbz56JWpfEGG0prskAV97ATNRoj5LDmPicD22hU=
gitea.com/lunny/levelqueue v0.5.1 h1:15q1oBWaO2Aw9t3rDthQSTX+vBNgjYNkCgRJgES9gA8=
gitea.com/lunny/levelqueue v0.5.1/go.mod h1:HBqmLbz56JWpfEGG0prskAV97ATNRoj5LDmPicD22hU=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
gitea.dev/actionslib v1.5.0 h1:dC3v2HTWbbvyNBSdg2W3kd4WNC/p+FnZaKpl1MdzM00=
gitea.dev/actionslib v1.5.0/go.mod h1:C9wmOFgDBCXyRvg4A3KTX8Vszs8v9+elH4QLGVDolqU=
gitea.dev/sdk v1.5.0 h1:qxtIHpkid4pva3id7S+UlJIGJAAQ8/riQidqD3jzErc=
gitea.dev/sdk v1.5.0/go.mod h1:cM7S3Y9TY3LraJoRHHDLlY3YnVTu4bTc9TEBGuFa2Jw=
gitea.dev/actionslib v1.5.2 h1:PSSUd+4jIjI+ycqo62+BRgAWkBnzjgeGQAcI3ZqH7LY=
gitea.dev/actionslib v1.5.2/go.mod h1:i8wUQMnvGwT13f2re2ZTMCFL/TyauQLeOb1L4ngjtPk=
gitea.dev/sdk v1.5.1 h1:bMBU3rPoU87a8OOgtfu5w4sZn2x/ydwNAkqFLfIN8c8=
gitea.dev/sdk v1.5.1/go.mod h1:cM7S3Y9TY3LraJoRHHDLlY3YnVTu4bTc9TEBGuFa2Jw=
github.com/42wim/httpsig v1.3.0 h1:HK2bieeZ99X9IvOoc1ST6yrhLek0f4MqlEGES2JCP3g=
github.com/42wim/httpsig v1.3.0/go.mod h1:lgC3Yrvp4lxi1odEF1ge3kQBu/VM+SRcZQu5YwiR3dU=
github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432 h1:3Fcz1QzlS7Jv4FT2KI3cHNSZL+KPN3dXxurn9f3YL/Y=
@@ -48,16 +48,14 @@ github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0 h1:LvK7+C6qgz8BPnmn7x
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0/go.mod h1:I28hc9eaiqKCoOB+9Wh/P1IOScfm0xCgyWC6DAo0lmo=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/brotli v1.2.7 h1:UgafRPGMam5ySHfRkhthMqOFA6IFoeF3D7mWIDLjG5A=
github.com/andybalholm/brotli v1.2.7/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/cascadia v1.3.4 h1:vM2lgh0Vru9Vwyfm4cQqWP2HHMW0u0+2PAW7Q38Qufg=
github.com/andybalholm/cascadia v1.3.4/go.mod h1:BLRmbRjpEtNKieZOCCvYj4RqN+KRA41GBe/5O+G93kM=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
@@ -68,12 +66,12 @@ github.com/bits-and-blooms/bitset v1.24.5 h1:654xBVHc23gJMAgOTkPNoCVfiRxuIOAUnAZ
github.com/bits-and-blooms/bitset v1.24.5/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
github.com/blevesearch/bleve/v2 v2.6.1 h1:47vLskRTqxvQEtxVPYHjf5KpOgzD2msslXFjvUQCgWQ=
github.com/blevesearch/bleve/v2 v2.6.1/go.mod h1:Dvvx6ZoEBTOj6RSzfk0lEz0wce/qhe2yOUubXeuzd2c=
github.com/blevesearch/bleve/v2 v2.6.2 h1:CaPS1fjYgp+ejW5Nd7OTmVTCkuFzzqyjrADi/XCizdU=
github.com/blevesearch/bleve/v2 v2.6.2/go.mod h1:w86Ae7GPU0r5IW2S6OSrdUojZY3z7WmOb2pgAnfg8fQ=
github.com/blevesearch/bleve_index_api v1.4.1 h1:CYIyecFlI+/RYjzUm+NmDjYbSvk870Bb7f+Vl4b12q8=
github.com/blevesearch/bleve_index_api v1.4.1/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko=
github.com/blevesearch/geo v0.2.6 h1:7K1oyQKYlauC+mJuo2AfNPyjN/4mihEoJMfyClVH1Mo=
github.com/blevesearch/geo v0.2.6/go.mod h1:6qzVUiB4BK47QkSZcRqiXEP2W3EeXuzM5XFTF8AdZ8A=
github.com/blevesearch/geo v0.2.7-0.20260831141219-9c3e3a4d84bc h1:2nCheDGYfGpD+w6d1czqyzuLXUgQpuk8tTi66si470c=
github.com/blevesearch/geo v0.2.7-0.20260831141219-9c3e3a4d84bc/go.mod h1:6qzVUiB4BK47QkSZcRqiXEP2W3EeXuzM5XFTF8AdZ8A=
github.com/blevesearch/go-faiss v1.1.5 h1:/IU5lkOahH9Ghfk9n3F6N0XD7PYVXZJWmNDc9TtXuco=
github.com/blevesearch/go-faiss v1.1.5/go.mod h1:w3W9AiWsFRGVaMG+/cmJi7iHEAuGyC6blsgO1EzCK/M=
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
@@ -90,8 +88,8 @@ github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A=
github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
github.com/blevesearch/vellum v1.2.0 h1:xkDiOEsHc2t3Cp0NsNZZ36pvc130sCzcGKOPMzXe+e0=
github.com/blevesearch/vellum v1.2.0/go.mod h1:uEcfBJz7mAOf0Kvq6qoEKQQkLODBF46SINYNkZNae4k=
github.com/blevesearch/vellum v1.2.1 h1:PYryJDqBHjLM6n3YindZSQcrseaMs8QpcsrrAOpwwj8=
github.com/blevesearch/vellum v1.2.1/go.mod h1:uEcfBJz7mAOf0Kvq6qoEKQQkLODBF46SINYNkZNae4k=
github.com/blevesearch/zapx/v11 v11.4.3 h1:PTZOO5loKpHC/x/GzmPZNa9cw7GZIQxd5qRjwij9tHY=
github.com/blevesearch/zapx/v11 v11.4.3/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc=
github.com/blevesearch/zapx/v12 v12.4.3 h1:eElXvAaAX4m04t//CGBQAtHNPA+Q6A1hHZVrN3LSFYo=
@@ -104,10 +102,10 @@ github.com/blevesearch/zapx/v15 v15.4.3 h1:iJiMJOHrz216jyO6lS0m9RTCEkprUnzvqAI2l
github.com/blevesearch/zapx/v15 v15.4.3/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
github.com/blevesearch/zapx/v16 v16.3.4 h1:hDAqA8qusZTNbPEL7//w5P65UZ2de6yhSeUaTbp0Po0=
github.com/blevesearch/zapx/v16 v16.3.4/go.mod h1:zqkPPqs9GS9FzVWzCO3Wf1X044yWAV17+4zb+FTiEHg=
github.com/blevesearch/zapx/v17 v17.2.3 h1:UYYJPAt5b2tVxldx5h0jmv23RMsg8/UZKFVya7v92po=
github.com/blevesearch/zapx/v17 v17.2.3/go.mod h1:r7mb4QWbDQSkbAnOjCb9iCfkcrzajB4yBdJpuBIo/fE=
github.com/bmatcuk/doublestar/v4 v4.10.3 h1:1UIv1+fqH3++ViVkpnaPTB+Pi1UHycnPFAq14zgK+gI=
github.com/bmatcuk/doublestar/v4 v4.10.3/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/blevesearch/zapx/v17 v17.2.4 h1:6BekwKXVubfivq87E91dqJgRDhmguA0Hvm1JTyqNzWM=
github.com/blevesearch/zapx/v17 v17.2.4/go.mod h1:8gLqU0Hr+MuFCBbL/lDS3O9MiTFjyoTcLXkiVCjwqMs=
github.com/bmatcuk/doublestar/v4 v4.10.4 h1:oTKkRmRuOsKMqFe1AQazd9gGBFNEpOmgKp/xzB97dnA=
github.com/bmatcuk/doublestar/v4 v4.10.4/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q=
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
@@ -162,12 +160,10 @@ github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454Wv
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2/v2 v2.11.0 h1:z5uRPHav+eTVpoffDSuUP2KPS4f1ONZaPDK73S8HA0w=
github.com/dlclark/regexp2/v2 v2.11.0/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dlclark/regexp2/v2 v2.12.0 h1:GsCgDZZeHluUDHASr2yqQonZk4Tma5LFmLH9CSsFFEE=
github.com/dlclark/regexp2/v2 v2.12.0/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
@@ -528,7 +524,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
@@ -545,8 +540,8 @@ github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77ro
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.17 h1:flR0y/x1hgM8EGV1AW3Xll6T413G0glV8UfBwR617V4=
github.com/ulikunitz/xz v0.5.17/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw=
github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw=
github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to=
github.com/urfave/cli-docs/v3 v3.2.0 h1:1J2pmtDuMRConzYb/QHyPbeFB6HvMtYm3xre9uhs81c=
github.com/urfave/cli-docs/v3 v3.2.0/go.mod h1:cjSVza4yCaqQet06zO6QhYqXQYjGRqbUj8zok6mHDRU=
github.com/urfave/cli/v3 v3.14.0 h1:a8414NQlHJs0c/iBsulKLzlES0n/lEAskbL2LKpU4/s=
github.com/urfave/cli/v3 v3.14.0/go.mod h1:vXn6HxPNccJSzQr2QvwVncOKrgYGIHU0HY5h8B2nQj4=
github.com/wneessen/go-mail v0.8.1 h1:tVcncj02/QySVFw3zr/kXOzZcuFQqBNT6K+Rbgm/pcM=
@@ -555,15 +550,9 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yohcop/openid-go v1.0.1 h1:DPRd3iPO5F6O5zX2e62XpVAbPT6wV51cuucH0z9g3js=
github.com/yohcop/openid-go v1.0.1/go.mod h1:b/AvD03P0KHj4yuihb+VtLD6bYYgsy0zqBzPCRjkCNs=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4=
github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
github.com/yuin/goldmark v1.8.7 h1:XT/tNqCIbabh0ln675w4x3dv04kQBKi8NwYqi9IJaxI=
github.com/yuin/goldmark v1.8.7/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
@@ -572,8 +561,8 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
gitlab.com/gitlab-org/api/client-go/v3 v3.39.1 h1:yBmlSUi5P6L354Aq3PJCWvW24zz9Ru3WnAXgNMwA9g8=
gitlab.com/gitlab-org/api/client-go/v3 v3.39.1/go.mod h1:jBFhGkTHLKkgeHEdS/A8yA0tQ6UHq96QVsS5muu6flM=
gitlab.com/gitlab-org/api/client-go/v3 v3.43.0 h1:X7qHPPt6If/5f0bgBpeT8GYWx2zDnuUzRsfnzJa9/cc=
gitlab.com/gitlab-org/api/client-go/v3 v3.43.0/go.mod h1:8cuzgeUJzkVvaRvw57iZ8foGmL5q8GZXi4lHTiF5edY=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.uber.org/atomic v1.12.0 h1:BvcXdFKuviU4fTL/f+SxdQ5qJX/Jix8pAkgdUcb3XOE=
@@ -590,17 +579,16 @@ go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
go.yaml.in/yaml/v4 v4.0.0-rc.6.0.20261013184558-04dd63131525 h1:FtzHH3s777Nx50YcPAcodAPm1mNakc228nMYbr3f+90=
go.yaml.in/yaml/v4 v4.0.0-rc.6.0.20261013184558-04dd63131525/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
go.yaml.in/yaml/v3 v3.0.6 h1:zcDfvjXEqTQmpjniUBLc91xYJMB2c4QOFxEF/LCEckM=
go.yaml.in/yaml/v3 v3.0.6/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
go.yaml.in/yaml/v4 v4.0.0 h1:/LG1pNO84Z4l603j/xMn69CcS5OYhMQ/f7l2LoQUGuw=
go.yaml.in/yaml/v4 v4.0.0/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
@@ -608,7 +596,6 @@ golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aI
golang.org/x/image v0.47.0 h1:za/8gVz++PPZqmZJBNSISICN5Rrj5zixP/8K++fgmqo=
golang.org/x/image v0.47.0/go.mod h1:IkN5o4A7vq55vXN8FWjjnhg9Rs2GOtffqsSi9mWAKYE=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg=
golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -617,8 +604,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY=
golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
@@ -627,7 +612,6 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -641,24 +625,17 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
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=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
@@ -666,7 +643,6 @@ golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs=
golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -441,6 +441,10 @@ func prepareMigrationTasks() []*migration {
newMigration(362, "Add reusable_workflow_ref column to action_run_job", v29.AddReusableWorkflowRefToActionRunJob),
newMigration(363, "Add release_id to reaction table", v29.AddReleaseIDToReaction),
newMigration(364, "Add new table project_workflow", v29.AddProjectWorkflow),
newMigration(365, "Normalize legacy repository unit and login source JSON", v29.NormalizeLegacyConfigJSON),
newMigration(366, "Drop user_open_id table", v29.DropUserOpenIDTable),
newMigration(367, "Add latest_release_id to repository table", v29.AddLatestReleaseIDToRepository),
newMigration(368, "Add unique index for team name", v29.AddUniqueTeamNameIndex),
}
return preparedMigrations
}

View File

@@ -0,0 +1,95 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v29
import (
"context"
"encoding/binary"
"gitea.dev/modelmigration/base"
"gitea.dev/modules/json"
"gitea.dev/modules/util"
)
// unmarshalHandleDoubleEncode - due to a bug in xorm (see https://gitea.com/xorm/xorm/pulls/1957) - it's
// possible that a Blob may be double encoded or gain an unwanted prefix of 0xff 0xfe.
func unmarshalHandleDoubleEncode(bs []byte, v any) error {
if len(bs) == 0 {
// json.Unmarshal will report errors if input is empty (nil or zero-length)
// It seems that XORM ignores the nil but still passes zero-length string into this function
// To be consistent, we should treat all empty inputs as success
return nil
}
err := json.Unmarshal(bs, v)
if err != nil {
ok := true
rs := []byte{}
temp := make([]byte, 2)
for _, rn := range string(bs) {
if rn > 0xffff {
ok = false
break
}
binary.LittleEndian.PutUint16(temp, uint16(rn))
rs = append(rs, temp...)
}
if ok {
if len(rs) > 1 && rs[0] == 0xff && rs[1] == 0xfe {
rs = rs[2:]
}
err = json.Unmarshal(rs, v)
}
}
if err != nil && len(bs) > 2 && bs[0] == 0xff && bs[1] == 0xfe {
err = json.Unmarshal(bs[2:], v)
}
return err
}
func normalizeLegacyConfigJSON(config string) string {
if config == "" || json.Valid(util.UnsafeStringToBytes(config)) {
return config
}
m := map[any]any{}
err := unmarshalHandleDoubleEncode(util.UnsafeStringToBytes(config), &m)
if err != nil {
return config
}
if bs, err := json.Marshal(m); err == nil {
return string(bs)
}
return config
}
func NormalizeLegacyConfigJSON(ctx context.Context, x base.EngineMigration) error {
type RepoUnit struct {
ID int64
Config string
}
type LoginSource struct {
ID int64
Cfg string
}
return base.EngineTransaction(x, func(sess base.Session) error {
if err := base.IterateTableByColumn(ctx, x, sess, "id", nil, func(_ context.Context, unit *RepoUnit) error {
config := normalizeLegacyConfigJSON(unit.Config)
if config == unit.Config {
return nil
}
_, err := sess.ID(unit.ID).Cols("config").Update(&RepoUnit{Config: config})
return err
}); err != nil {
return err
}
return base.IterateTableByColumn(ctx, x, sess, "id", nil, func(_ context.Context, source *LoginSource) error {
config := normalizeLegacyConfigJSON(source.Cfg)
if config == source.Cfg {
return nil
}
_, err := sess.ID(source.ID).Cols("cfg").Update(&LoginSource{Cfg: config})
return err
})
})
}

View File

@@ -0,0 +1,95 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v29
import (
"testing"
"gitea.dev/modelmigration/migrationtest"
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNormalizeLegacyConfigJSONValue(t *testing.T) {
const (
config = `{"Host":"smtp.example.com"}`
packed = "≻潈瑳㨢猢瑭⹰硥浡汰⹥潣≭⁽"
)
for _, tc := range []struct {
name, input, want string
}{
{"empty", "", ""},
{"valid", config + " ", config + " "},
{"unicode and large number", ` {"name":"例😀","id":9007199254740993} `, ` {"name":"例😀","id":9007199254740993} `},
{"double encoded", packed, config},
{"double encoded with BOM", "\ufeff" + packed, config},
{"BOM", "\xff\xfe" + config, config},
{"BOM with non-BMP rune", "\xff\xfe" + `{"name":"😀"}`, `{"name":"😀"}`},
{"corrupt", "invalid", "invalid"},
{"corrupt non-BMP rune", "invalid😀", "invalid😀"},
} {
t.Run(tc.name, func(t *testing.T) {
got := normalizeLegacyConfigJSON(tc.input)
assert.Equal(t, tc.want, got)
assert.Equal(t, got, normalizeLegacyConfigJSON(got))
})
}
}
func TestNormalizeLegacyConfigJSON(t *testing.T) {
type RepoUnit struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64
Config string `xorm:"TEXT"`
}
type LoginSource struct {
ID int64 `xorm:"pk autoincr"`
Name string
Cfg string `xorm:"TEXT"`
}
x, cleanup := migrationtest.PrepareTestEnv(t, 0, new(RepoUnit), new(LoginSource))
defer cleanup()
if x == nil || t.Failed() {
return
}
defer test.MockVariableValue(&setting.Database.IterateBufferSize, 2)()
inputs := []string{
` {"id":9007199254740993,"name":"例😀"} `,
"≻潈瑳㨢猢瑭⹰硥浡汰⹥潣≭⁽",
"\ufeff≻潈瑳㨢猢瑭⹰硥浡汰⹥潣≭⁽",
"",
"invalid",
}
want := []string{inputs[0], `{"Host":"smtp.example.com"}`, `{"Host":"smtp.example.com"}`, "", "invalid", ""}
for _, input := range inputs {
_, err := x.Insert(&RepoUnit{RepoID: 42, Config: input})
require.NoError(t, err)
_, err = x.Insert(&LoginSource{Name: "test", Cfg: input})
require.NoError(t, err)
}
_, err := x.Exec("INSERT INTO `repo_unit` (`repo_id`, `config`) VALUES (?, NULL)", 42)
require.NoError(t, err)
_, err = x.Exec("INSERT INTO `login_source` (`name`, `cfg`) VALUES (?, NULL)", "test")
require.NoError(t, err)
for range 2 {
require.NoError(t, NormalizeLegacyConfigJSON(t.Context(), x))
var units []RepoUnit
require.NoError(t, x.Asc("id").Find(&units))
require.Len(t, units, len(want))
var sources []LoginSource
require.NoError(t, x.Asc("id").Find(&sources))
require.Len(t, sources, len(want))
for i, config := range want {
assert.Equal(t, config, units[i].Config)
assert.EqualValues(t, 42, units[i].RepoID)
assert.Equal(t, config, sources[i].Cfg)
assert.Equal(t, "test", sources[i].Name)
}
}
}

View File

@@ -0,0 +1,14 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v29
import (
"context"
"gitea.dev/modelmigration/base"
)
func DropUserOpenIDTable(_ctx context.Context, x base.EngineMigration) error {
return x.DropTables("user_open_id")
}

View File

@@ -0,0 +1,33 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v29
import (
"testing"
"gitea.dev/modelmigration/migrationtest"
"github.com/stretchr/testify/require"
)
func TestDropUserOpenIDTable(t *testing.T) {
type UserOpenID struct {
ID int64 `xorm:"pk autoincr"`
}
x, cleanup := migrationtest.PrepareTestEnv(t, 0, new(UserOpenID))
defer cleanup()
if x == nil || t.Failed() {
return
}
exists, err := x.IsTableExist("user_open_id")
require.NoError(t, err)
require.True(t, exists)
require.NoError(t, DropUserOpenIDTable(t.Context(), x))
exists, err = x.IsTableExist("user_open_id")
require.NoError(t, err)
require.False(t, exists)
require.NoError(t, DropUserOpenIDTable(t.Context(), x))
}

View File

@@ -0,0 +1,23 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v29
import (
"context"
"gitea.dev/modelmigration/base"
"xorm.io/xorm"
)
func AddLatestReleaseIDToRepository(_ctx context.Context, engine base.EngineMigration) error {
type Repository struct {
LatestReleaseID int64 `xorm:"NOT NULL DEFAULT 0"`
}
_, err := engine.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
IgnoreConstrains: true,
}, new(Repository))
return err
}

View File

@@ -0,0 +1,50 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v29
import (
"context"
"fmt"
"strings"
"gitea.dev/modelmigration/base"
"gitea.dev/modules/log"
"gitea.dev/modules/util"
"xorm.io/xorm"
)
func AddUniqueTeamNameIndex(_ctx context.Context, x base.EngineMigration) error {
type Team struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"INDEX UNIQUE(s)"`
LowerName string `xorm:"UNIQUE(s)"`
Name string
}
var teams []Team
if err := x.Where("id NOT IN (SELECT MIN(id) FROM team GROUP BY org_id, lower_name)").Asc("id").Find(&teams); err != nil {
return err
}
for _, team := range teams {
for suffix := team.ID; ; suffix++ {
ending := fmt.Sprintf("-%d", suffix)
name := util.TruncateStringBytes(team.Name, 255-len(ending)) + ending
exists, err := x.Where("org_id = ? AND lower_name = ?", team.OrgID, strings.ToLower(name)).Exist(new(Team))
if err != nil {
return err
}
if !exists {
if _, err := x.ID(team.ID).Cols("name", "lower_name").Update(&Team{Name: name, LowerName: strings.ToLower(name)}); err != nil {
return err
}
log.Warn("renamed duplicate team id=%d in org id=%d from %q to %q", team.ID, team.OrgID, team.Name, name)
break
}
}
}
_, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(Team))
return err
}

View File

@@ -0,0 +1,52 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v29
import (
"strings"
"testing"
"gitea.dev/modelmigration/migrationtest"
"github.com/stretchr/testify/require"
)
func TestAddUniqueTeamNameIndex(t *testing.T) {
type Team struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"INDEX"`
LowerName, Name string
}
x, cleanup := migrationtest.PrepareTestEnv(t, 0, new(Team))
defer cleanup()
if x == nil || t.Failed() {
return
}
_, err := x.Insert(
&Team{OrgID: 1, Name: "Team", LowerName: "team"},
&Team{OrgID: 1, Name: "Team", LowerName: "team"},
&Team{OrgID: 1, Name: "Team-2", LowerName: "team-2"},
&Team{OrgID: 2, Name: "Team", LowerName: "team"},
&Team{OrgID: 1, Name: strings.Repeat("A", 255), LowerName: strings.Repeat("a", 255)},
&Team{OrgID: 1, Name: strings.Repeat("A", 255), LowerName: strings.Repeat("a", 255)},
)
require.NoError(t, err)
for range 2 {
require.NoError(t, AddUniqueTeamNameIndex(t.Context(), x))
var teams []Team
require.NoError(t, x.Asc("id").Find(&teams))
require.Equal(t, []Team{
{ID: 1, OrgID: 1, Name: "Team", LowerName: "team"},
{ID: 2, OrgID: 1, Name: "Team-3", LowerName: "team-3"},
{ID: 3, OrgID: 1, Name: "Team-2", LowerName: "team-2"},
{ID: 4, OrgID: 2, Name: "Team", LowerName: "team"},
{ID: 5, OrgID: 1, Name: strings.Repeat("A", 255), LowerName: strings.Repeat("a", 255)},
{ID: 6, OrgID: 1, Name: strings.Repeat("A", 253) + "-6", LowerName: strings.Repeat("a", 253) + "-6"},
}, teams)
}
_, err = x.Insert(&Team{OrgID: 1, Name: "TEAM", LowerName: "team"})
require.Error(t, err)
}

View File

@@ -6,10 +6,10 @@ package actions
import (
"context"
"gitea.dev/models/db"
"gitea.dev/models/perm"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/json"
"gitea.dev/modules/util"
"xorm.io/xorm/convert"
@@ -30,7 +30,7 @@ type OwnerActionsConfig struct {
var _ convert.ConversionFrom = (*OwnerActionsConfig)(nil)
func (cfg *OwnerActionsConfig) FromDB(bytes []byte) error {
_ = json.Unmarshal(bytes, cfg)
db.UnmarshalFromDB(bytes, cfg)
cfg.TokenPermissionMode, _ = util.EnumValue(cfg.TokenPermissionMode)
return nil
}

View File

@@ -65,26 +65,22 @@ func (opts FindScopedWorkflowSourceOpts) ToConds() builder.Cond {
if opts.SourceRepoID != 0 {
cond = cond.And(builder.Eq{"source_repo_id": opts.SourceRepoID})
}
return cond
return cond.And(scopedWorkflowSourceOwnedCond())
}
func scopedWorkflowSourceOwnedCond() builder.Cond {
return builder.Eq{"owner_id": 0}.Or(builder.Expr("EXISTS (SELECT 1 FROM repository WHERE repository.id = action_scoped_workflow_source.source_repo_id AND repository.owner_id = action_scoped_workflow_source.owner_id)"))
}
// GetEffectiveScopedWorkflowSources returns the scoped-workflow sources effective for a repo owned by repoOwnerID:
// the owner's own sources plus instance-level (owner_id=0) sources.
func GetEffectiveScopedWorkflowSources(ctx context.Context, repoOwnerID int64) ([]*ActionScopedWorkflowSource, error) {
owners := []int64{0}
if repoOwnerID != 0 {
owners = append(owners, repoOwnerID)
}
return db.Find[ActionScopedWorkflowSource](ctx, FindScopedWorkflowSourceOpts{OwnerIDs: owners})
return db.Find[ActionScopedWorkflowSource](ctx, FindScopedWorkflowSourceOpts{OwnerIDs: []int64{0, repoOwnerID}})
}
// IsScopedWorkflowSourceEffective reports whether sourceRepoID is a scoped-workflow source effective for a repo owned by repoOwnerID.
func IsScopedWorkflowSourceEffective(ctx context.Context, repoOwnerID, sourceRepoID int64) (bool, error) {
owners := []int64{0}
if repoOwnerID != 0 {
owners = append(owners, repoOwnerID)
}
return db.Exist[ActionScopedWorkflowSource](ctx, FindScopedWorkflowSourceOpts{OwnerIDs: owners, SourceRepoID: sourceRepoID}.ToConds())
return db.Exist[ActionScopedWorkflowSource](ctx, FindScopedWorkflowSourceOpts{OwnerIDs: []int64{0, repoOwnerID}, SourceRepoID: sourceRepoID}.ToConds())
}
// IsWorkflowRequiredInSources reports whether workflowID from sourceRepoID is required by any of the given sources.
@@ -138,12 +134,10 @@ func GetScopedWorkflowSourcesByOwner(ctx context.Context, ownerID int64) ([]*Act
// GetScopedWorkflowSource returns the (owner, repo) source registration or a NotExist error.
func GetScopedWorkflowSource(ctx context.Context, ownerID, repoID int64) (*ActionScopedWorkflowSource, error) {
src := &ActionScopedWorkflowSource{}
has, err := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID).Get(src)
src, has, err := db.Get[ActionScopedWorkflowSource](ctx, builder.Eq{"owner_id": ownerID, "source_repo_id": repoID}.And(scopedWorkflowSourceOwnedCond()))
if err != nil {
return nil, err
}
if !has {
} else if !has {
return nil, util.NewNotExistErrorf("scoped workflow source (owner %d, repo %d) does not exist", ownerID, repoID)
}
return src, nil
@@ -181,3 +175,9 @@ func RemoveScopedWorkflowSource(ctx context.Context, ownerID, repoID int64) erro
_, err := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID).Delete(new(ActionScopedWorkflowSource))
return err
}
// RemoveOwnerScopedWorkflowSources removes every owner-level registration of repoID, as only its current owner may hold one.
func RemoveOwnerScopedWorkflowSources(ctx context.Context, repoID int64) error {
_, err := db.GetEngine(ctx).Where("owner_id <> 0 AND source_repo_id = ?", repoID).Delete(new(ActionScopedWorkflowSource))
return err
}

View File

@@ -48,26 +48,28 @@ func TestGetEffectiveScopedWorkflowSources(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
rows := []*ActionScopedWorkflowSource{
{OwnerID: 2, SourceRepoID: 100, WorkflowConfigs: nil}, // org 2 registers repo 100 (optional)
{OwnerID: 0, SourceRepoID: 100, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"a.yml": {Required: true, Patterns: []string{"p"}}}}, // instance also registers repo 100 (required)
{OwnerID: 0, SourceRepoID: 200, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"b.yml": {Required: true, Patterns: []string{"p"}}}}, // instance source 200
{OwnerID: 3, SourceRepoID: 300, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"c.yml": {Required: true, Patterns: []string{"p"}}}}, // a different owner's source
}
for _, r := range rows {
require.NoError(t, db.Insert(ctx, r))
for _, source := range []*ActionScopedWorkflowSource{
{OwnerID: 2, SourceRepoID: 1},
{OwnerID: 0, SourceRepoID: 1, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"a.yml": {Required: true}}},
{OwnerID: 0, SourceRepoID: 200, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"b.yml": {Required: true}}},
{OwnerID: 3, SourceRepoID: 3, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"c.yml": {Required: true}}},
{OwnerID: 2, SourceRepoID: 3, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"c.yml": {Required: true}}},
} {
require.NoError(t, db.Insert(ctx, source))
}
// owner 2 sees its own sources plus instance-level ones, but not owner 3's.
owner2, err := GetEffectiveScopedWorkflowSources(ctx, 2)
require.NoError(t, err)
assert.Len(t, owner2, 3)
_, err = GetScopedWorkflowSource(ctx, 2, 3)
assert.ErrorIs(t, err, util.ErrNotExist)
required, err := IsScopedWorkflowRequired(ctx, 2, 100, "a.yml")
required, err := IsScopedWorkflowRequired(ctx, 2, 1, "a.yml")
require.NoError(t, err)
assert.True(t, required, "instance marks a.yml required → required for owner 2 even though org left it optional")
required, err = IsScopedWorkflowRequired(ctx, 2, 100, "x.yml")
required, err = IsScopedWorkflowRequired(ctx, 2, 1, "x.yml")
require.NoError(t, err)
assert.False(t, required)
@@ -76,12 +78,12 @@ func TestGetEffectiveScopedWorkflowSources(t *testing.T) {
assert.True(t, required)
// owner 3's source must not be effective for owner 2.
required, err = IsScopedWorkflowRequired(ctx, 2, 300, "c.yml")
required, err = IsScopedWorkflowRequired(ctx, 2, 3, "c.yml")
require.NoError(t, err)
assert.False(t, required)
// IsScopedWorkflowSourceEffective: owner-level and instance-level sources are effective; another owner's is not.
effective, err := IsScopedWorkflowSourceEffective(ctx, 2, 100)
effective, err := IsScopedWorkflowSourceEffective(ctx, 2, 1)
require.NoError(t, err)
assert.True(t, effective, "owner 2's own source")
@@ -89,7 +91,7 @@ func TestGetEffectiveScopedWorkflowSources(t *testing.T) {
require.NoError(t, err)
assert.True(t, effective, "instance-level source is effective for any owner")
effective, err = IsScopedWorkflowSourceEffective(ctx, 2, 300)
effective, err = IsScopedWorkflowSourceEffective(ctx, 2, 3)
require.NoError(t, err)
assert.False(t, effective, "owner 3's source is not effective for owner 2")
@@ -97,7 +99,7 @@ func TestGetEffectiveScopedWorkflowSources(t *testing.T) {
require.NoError(t, err)
assert.False(t, effective, "unknown source repo")
effective, err = IsScopedWorkflowSourceEffective(ctx, 3, 300)
effective, err = IsScopedWorkflowSourceEffective(ctx, 3, 3)
require.NoError(t, err)
assert.True(t, effective, "owner 3's own source is effective for owner 3")
}
@@ -107,8 +109,8 @@ func TestScopedWorkflowSourceCRUD(t *testing.T) {
ctx := t.Context()
// add is idempotent
require.NoError(t, AddScopedWorkflowSource(ctx, 5, 10))
require.NoError(t, AddScopedWorkflowSource(ctx, 5, 10))
require.NoError(t, AddScopedWorkflowSource(ctx, 5, 4))
require.NoError(t, AddScopedWorkflowSource(ctx, 5, 4))
sources, err := GetScopedWorkflowSourcesByOwner(ctx, 5)
require.NoError(t, err)
assert.Len(t, sources, 1)
@@ -118,20 +120,20 @@ func TestScopedWorkflowSourceCRUD(t *testing.T) {
"a.yml": {Required: true, Patterns: []string{"src: a.yml / *"}},
"b.yml": {Required: false, Patterns: []string{"src: b.yml / build (push)"}},
}
require.NoError(t, SetScopedWorkflowSourceConfigs(ctx, 5, 10, configs))
src, err := GetScopedWorkflowSource(ctx, 5, 10)
require.NoError(t, SetScopedWorkflowSourceConfigs(ctx, 5, 4, configs))
src, err := GetScopedWorkflowSource(ctx, 5, 4)
require.NoError(t, err)
assert.Equal(t, configs, src.WorkflowConfigs)
// clearing the configs works
require.NoError(t, SetScopedWorkflowSourceConfigs(ctx, 5, 10, nil))
src, err = GetScopedWorkflowSource(ctx, 5, 10)
require.NoError(t, SetScopedWorkflowSourceConfigs(ctx, 5, 4, nil))
src, err = GetScopedWorkflowSource(ctx, 5, 4)
require.NoError(t, err)
assert.Empty(t, src.WorkflowConfigs)
// remove
require.NoError(t, RemoveScopedWorkflowSource(ctx, 5, 10))
_, err = GetScopedWorkflowSource(ctx, 5, 10)
require.NoError(t, RemoveScopedWorkflowSource(ctx, 5, 4))
_, err = GetScopedWorkflowSource(ctx, 5, 4)
assert.ErrorIs(t, err, util.ErrNotExist)
sources, err = GetScopedWorkflowSourcesByOwner(ctx, 5)
require.NoError(t, err)

View File

@@ -7,7 +7,6 @@ import (
"context"
"errors"
"fmt"
"strconv"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
@@ -162,12 +161,7 @@ func (actions ActionList) LoadIssues(ctx context.Context) error {
issueNum := 0
for _, action := range actions {
if action.IsIssueEvent() {
infos := action.GetIssueInfos()
if len(infos) == 0 {
continue
}
index, _ := strconv.ParseInt(infos[0], 10, 64)
if index > 0 {
if index := action.getIssueIndex(); index > 0 {
conditions = conditions.Or(builder.Eq{
"repo_id": action.RepoID,
"`index`": index,

View File

@@ -180,15 +180,14 @@ func updateIssueNotification(ctx context.Context, userID, issueID, commentID, up
}
// NOTICE: Only update comment id when the before notification on this issue is read, otherwise you may miss some old comments.
// But we need update update_by so that the notification will be reorder
var cols []string
// Always update so that updated_unix moves the notification to the top of the list
// Actually, it's unclear what UpdatedBy is used for, or maybe not used at all, or maybe it should just be dropped.
notification.UpdatedBy = updatedByID
cols := []string{"updated_by"}
if notification.Status == NotificationStatusRead {
notification.Status = NotificationStatusUnread
notification.CommentID = commentID
cols = []string{"status", "update_by", "comment_id"}
} else {
notification.UpdatedBy = updatedByID
cols = []string{"update_by"}
cols = append(cols, "status", "comment_id")
}
_, err = db.GetEngine(ctx).ID(notification.ID).Cols(cols...).Update(notification)

View File

@@ -71,16 +71,6 @@ func (opts FindNotificationOptions) ToOrders() string {
// receiverID > 0 just send to receiver, else send to all watcher
// Returns the set of user IDs whose notification rows were created or updated.
func CreateOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) ([]int64, error) {
var notifiedIDs []int64
err := db.WithTx(ctx, func(ctx context.Context) error {
var innerErr error
notifiedIDs, innerErr = createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID)
return innerErr
})
return notifiedIDs, err
}
func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) ([]int64, error) {
// init
var toNotify container.Set[int64]
notifications, err := db.Find[Notification](ctx, FindNotificationOptions{
@@ -181,7 +171,11 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n
if notificationExists(notifications, issue.ID, userID) {
err = updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID)
} else {
err = createIssueNotification(ctx, userID, issue, commentID, notificationAuthorID)
if err = createIssueNotification(ctx, userID, issue, commentID, notificationAuthorID); err != nil {
if errUpdate := updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID); errUpdate == nil {
err = nil
}
}
}
if err != nil {
return nil, err

View File

@@ -32,6 +32,18 @@ func TestCreateOrUpdateIssueNotifications(t *testing.T) {
notf = unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{UserID: 4, IssueID: issue.ID})
assert.Equal(t, activities_model.NotificationStatusUnread, notf.Status)
// unread notification should get latest updated_unix when re-notified
_, err = db.GetEngine(t.Context()).ID(notf.ID).NoAutoTime().Cols("updated_unix").Update(&activities_model.Notification{UpdatedUnix: 123})
assert.NoError(t, err)
notf = unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: notf.ID})
assert.EqualValues(t, 123, notf.UpdatedUnix)
_, err = activities_model.CreateOrUpdateIssueNotifications(t.Context(), issue.ID, 0, 3, 0)
assert.NoError(t, err)
notf = unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: notf.ID})
assert.NotEqualValues(t, 123, notf.UpdatedUnix)
assert.EqualValues(t, 3, notf.UpdatedBy)
}
func TestCreateOrUpdateIssueNotificationsForAssigneeAndReviewer(t *testing.T) {

View File

@@ -6,6 +6,7 @@ package asymkey
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"os"
@@ -107,7 +108,7 @@ func writeAuthorizedStringForKey(key *PublicKey, w io.Writer) (keyValid bool, er
}
// appendAuthorizedKeysToFile appends new SSH keys' content to authorized_keys file.
func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
func appendAuthorizedKeysToFile(keys ...*PublicKey) (err error) {
// Don't need to rewrite this file if builtin SSH server is enabled.
if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
return nil
@@ -133,7 +134,7 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
if err != nil {
return err
}
defer f.Close()
defer func() { err = errors.Join(err, f.Close()) }()
// Note: chmod command does not support in Windows.
if !consts.IsWindows {

View File

@@ -88,8 +88,6 @@ var (
UserWebAuthRemove = define("user:webauth:remove", "Removed WebAuthn key {credential} from user {scope}.")
UserExternalLoginAdd = define("user:externallogin:add", "Added external login {external_id} for user {scope} using provider {provider}.")
UserExternalLoginRemove = define("user:externallogin:remove", "Removed external login from authentication source {auth_source_id} for user {scope}.")
UserOpenIDAdd = define("user:openid:add", "Associated OpenID {openid} to user {scope}.")
UserOpenIDRemove = define("user:openid:remove", "Removed OpenID {openid} from user {scope}.")
UserAccessTokenAdd = define("user:accesstoken:add", "Added access token {token} for user {scope} with scope {token_scope}.")
UserAccessTokenRegenerate = define("user:accesstoken:regenerate", "Regenerated access token {token} of user {scope}.")
UserAccessTokenRemove = define("user:accesstoken:remove", "Removed access token {token} from user {scope}.")

View File

@@ -251,18 +251,8 @@ func (s AccessTokenScope) parse() (accessTokenScopeBitmap, error) {
// The following is the more performant equivalent of 'for _, v := range strings.Split(remainingScope, ",")' as this is hot code
remainingScopes := string(s)
for len(remainingScopes) > 0 {
i := strings.IndexByte(remainingScopes, ',')
var v string
if i < 0 {
v = remainingScopes
remainingScopes = ""
} else if i+1 >= len(remainingScopes) {
v = remainingScopes[:i]
remainingScopes = ""
} else {
v = remainingScopes[:i]
remainingScopes = remainingScopes[i+1:]
}
v, remainingScopes, _ = strings.Cut(remainingScopes, ",")
singleScope := AccessTokenScope(v)
if singleScope == "" {
continue

View File

@@ -10,6 +10,7 @@ import (
"reflect"
"gitea.dev/models/db"
"gitea.dev/modules/graceful"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
@@ -60,14 +61,40 @@ var Names = map[Type]string{
type Config interface {
convert.Conversion
SetAuthSource(*Source)
GetAuthSource() *Source
}
type ConfigBase struct {
AuthSource *Source
AuthSource *Source `json:"-"`
}
func (p *ConfigBase) SetAuthSource(s *Source) {
p.AuthSource = s
func (c *ConfigBase) SetAuthSource(source *Source) {
c.AuthSource = source
}
func (c *ConfigBase) GetAuthSource() *Source {
return c.AuthSource
}
func UnmarshalAuthSourceConfig(bs []byte, v Config) {
if db.UnmarshalFromDB(bs, &v) {
return
}
// This is only a workaround because we don't know whether this unmarshal happens on sign-in page or admin panel.
// * Admin should have the chance to edit the auth source in admin panel, so FromDB should not return an error
// * The sign-in page should not use the auth source in the next login attempt, but we don't have a clear way to disable it immediately (acceptable)
// Actually, corrupted database value is a really rare case, just because AI thinks it should be handled.
authSourceID := v.GetAuthSource().ID
log.Error("Unable to unmarshal login source id=%d, will mark it as inactive to avoid unexpected sign-in issues", authSourceID)
go graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) {
// A small time window to avoid race condition with admin's editing because there is no other way.
// The UpdateUnix field is not filled when this function is called.
_, err := db.GetEngine(ctx).ID(authSourceID).And("is_active = ? AND updated_unix < ?", true, timeutil.TimeStampNow()-5).
Cols("is_active").Update(&Source{IsActive: false})
if err != nil {
log.Error("Unable to set login source id=%d to inactive: %v", authSourceID, err)
}
})
}
// SkipVerifiable configurations provide a IsSkipVerify to check if SkipVerify is set

View File

@@ -6,10 +6,15 @@ package auth_test
import (
"strings"
"testing"
"time"
auth_model "gitea.dev/models/auth"
"gitea.dev/models/db"
"gitea.dev/models/unittest"
"gitea.dev/modules/json"
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
"gitea.dev/modules/timeutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -24,7 +29,8 @@ type TestSource struct {
// FromDB fills up a LDAPConfig from serialized format.
func (source *TestSource) FromDB(bs []byte) error {
return json.Unmarshal(bs, &source)
auth_model.UnmarshalAuthSourceConfig(bs, source)
return nil
}
// ToDB exports a LDAPConfig to a serialized format.
@@ -53,3 +59,37 @@ func TestDumpAuthSource(t *testing.T) {
// INSERT INTO `login_source` (`id`, `type`, `name`, `is_active`, `is_sync_enabled`, `two_factor_policy`, `cfg`, `created_unix`, `updated_unix`) VALUES (1,6,'TestSource',0,0,'','{"TestField":"TestValue"}',1774179784,1774179784);
assert.Contains(t, sb.String(), `'{"TestField":"TestValue"}'`)
}
func TestUnmarshalAuthSourceConfig(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
defer test.MockVariableValue(&setting.IsProd, true)()
defer test.MockVariableValue(&setting.IsInTesting, false)()
cfg := &TestSource{}
source := &auth_model.Source{Type: auth_model.OAuth2, Name: t.Name(), IsActive: true, Cfg: cfg}
err := db.Insert(t.Context(), source)
require.NoError(t, err)
cfg.SetAuthSource(source)
t.Run("ValidConfig", func(t *testing.T) {
for _, data := range []string{`{"TestField":"TestValue"}`, "", "null"} {
auth_model.UnmarshalAuthSourceConfig([]byte(data), cfg)
assert.Equal(t, "TestValue", cfg.TestField)
assert.Same(t, source, cfg.GetAuthSource())
}
})
t.Run("InvalidConfigDeactivatesSource", func(t *testing.T) {
_, err = db.GetEngine(t.Context()).ID(source.ID).NoAutoTime().Cols("updated_unix").Update(&auth_model.Source{UpdatedUnix: timeutil.TimeStampNow() - 60})
require.NoError(t, err)
has, _ := db.GetEngine(t.Context()).ID(source.ID).And("is_active=?", true).Get(&auth_model.Source{})
assert.True(t, has)
auth_model.UnmarshalAuthSourceConfig([]byte(`{"TestField":false}`), cfg)
assert.Same(t, source, cfg.GetAuthSource())
require.Eventually(t, func() bool {
has, _ := db.GetEngine(t.Context()).ID(source.ID).And("is_active=?", false).Get(&auth_model.Source{})
return has
}, time.Second, time.Millisecond)
})
}

View File

@@ -12,8 +12,11 @@ import (
"encoding/base64"
"encoding/hex"
"fmt"
"strconv"
"gitea.dev/models/db"
"gitea.dev/modules/cache"
"gitea.dev/modules/globallock"
"gitea.dev/modules/secret"
"gitea.dev/modules/setting"
"gitea.dev/modules/timeutil"
@@ -48,6 +51,11 @@ func (err ErrTwoFactorNotEnrolled) Unwrap() error {
return util.ErrNotExist
}
const (
totpMaxAttempts = 5
totpAttemptsTTL = 15 * 60 // seconds, renewed by every failed attempt
)
// TwoFactor represents a two-factor authentication token.
type TwoFactor struct {
ID int64 `xorm:"pk autoincr"`
@@ -120,26 +128,59 @@ func (t *TwoFactor) validateTOTP(passcode string) (bool, error) {
return totp.Validate(passcode, secretStr), nil
}
type TwoFactorResult int
const (
TwoFactorResultValid TwoFactorResult = iota + 1
TwoFactorResultInvalid
TwoFactorResultTooManyAttempts
)
// ValidateAndConsumeTOTP validates the passcode and atomically records it as used so that the
// same passcode cannot be redeemed more than once (RFC 6238 §5.2). It returns false for an
// invalid passcode as well as for a replay, including the case where a concurrent request with
// the same passcode won the race first. All TOTP login surfaces must go through this helper.
func (t *TwoFactor) ValidateAndConsumeTOTP(ctx context.Context, passcode string) (bool, error) {
ok, err := t.validateTOTP(passcode)
if err != nil || !ok {
return false, err
// same passcode cannot be redeemed more than once (RFC 6238 §5.2). Invalid passcodes and replays
// return TwoFactorResultInvalid; exhausted attempts return TwoFactorResultTooManyAttempts.
// All TOTP login surfaces must go through this helper.
func (t *TwoFactor) ValidateAndConsumeTOTP(ctx context.Context, passcode string) (result TwoFactorResult, err error) {
if passcode == "" {
// password-only Basic auth clients must not use up the attempts
return TwoFactorResultInvalid, nil
}
// Conditional update: only a row whose stored passcode differs from this one is updated, so a
// replay (or a concurrent duplicate) matches zero rows and is rejected. The row lock taken by
// the UPDATE serializes racing requests, closing the read-validate-write TOCTOU window.
t.LastUsedPasscode = passcode
n, err := db.GetEngine(ctx).ID(t.ID).
Where(builder.Or(builder.IsNull{"last_used_passcode"}, builder.Neq{"last_used_passcode": passcode})).
Cols("last_used_passcode").Update(t)
if err != nil {
return false, err
}
return n == 1, nil
// the lock spans check, validation and counting so that concurrent requests cannot exceed the limit
attemptsKey := fmt.Sprintf("twofa_attempts_%d", t.UID)
err = globallock.LockAndDo(ctx, attemptsKey, func(ctx context.Context) error {
value, _ := cache.GetCache().Get(attemptsKey)
attempts, _ := strconv.Atoi(value)
if attempts >= totpMaxAttempts {
result = TwoFactorResultTooManyAttempts
return nil
}
valid, err := t.validateTOTP(passcode)
if err != nil {
return err
}
if valid {
// Conditional update: only a row whose stored passcode differs from this one is updated, so a
// replay (or a concurrent duplicate) matches zero rows and is rejected. The row lock taken by
// the UPDATE serializes racing requests, closing the read-validate-write TOCTOU window.
t.LastUsedPasscode = passcode
n, err := db.GetEngine(ctx).ID(t.ID).
Where(builder.Or(builder.IsNull{"last_used_passcode"}, builder.Neq{"last_used_passcode": passcode})).
Cols("last_used_passcode").Update(t)
if err != nil {
return err
}
valid = n == 1
}
if !valid {
result = TwoFactorResultInvalid
return cache.GetCache().Put(attemptsKey, strconv.Itoa(attempts+1), totpAttemptsTTL)
}
cache.Remove(attemptsKey)
result = TwoFactorResultValid
return nil
})
return result, err
}
// NewTwoFactor creates a new two-factor authentication token.

View File

@@ -4,11 +4,15 @@
package auth_test
import (
"fmt"
"sync"
"sync/atomic"
"testing"
"time"
auth_model "gitea.dev/models/auth"
"gitea.dev/models/unittest"
"gitea.dev/modules/cache"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/pquerna/otp/totp"
@@ -31,21 +35,42 @@ func TestTwoFactorLifecycle(t *testing.T) {
require.NoError(t, err)
// first use of a valid passcode succeeds
ok, err := tfa.ValidateAndConsumeTOTP(t.Context(), passcode)
result, err := tfa.ValidateAndConsumeTOTP(t.Context(), passcode)
require.NoError(t, err)
assert.True(t, ok)
assert.Equal(t, auth_model.TwoFactorResultValid, result)
// replaying the same passcode is refused, even when still inside the TOTP validity window
reloaded, err := auth_model.GetTwoFactorByUID(t.Context(), tfa.UID)
require.NoError(t, err)
ok, err = reloaded.ValidateAndConsumeTOTP(t.Context(), passcode)
result, err = reloaded.ValidateAndConsumeTOTP(t.Context(), passcode)
require.NoError(t, err)
assert.False(t, ok)
assert.Equal(t, auth_model.TwoFactorResultInvalid, result)
// an invalid passcode is rejected without consuming anything
ok, err = reloaded.ValidateAndConsumeTOTP(t.Context(), "000000")
var limited atomic.Int32
var wg sync.WaitGroup
for range 10 {
wg.Go(func() {
result, err := reloaded.ValidateAndConsumeTOTP(t.Context(), "0000000")
assert.NoError(t, err)
if result == auth_model.TwoFactorResultTooManyAttempts {
limited.Add(1)
} else {
assert.Equal(t, auth_model.TwoFactorResultInvalid, result)
}
})
}
wg.Wait()
assert.EqualValues(t, 6, limited.Load())
nextPasscode, err := totp.GenerateCode(key.Secret(), time.Now().Add(30*time.Second))
require.NoError(t, err)
assert.False(t, ok)
result, err = reloaded.ValidateAndConsumeTOTP(t.Context(), nextPasscode)
require.NoError(t, err)
assert.Equal(t, auth_model.TwoFactorResultTooManyAttempts, result)
cache.Remove(fmt.Sprintf("twofa_attempts_%d", tfa.UID))
result, err = reloaded.ValidateAndConsumeTOTP(t.Context(), nextPasscode)
require.NoError(t, err)
assert.Equal(t, auth_model.TwoFactorResultValid, result)
})
t.Run("DisableTwoFactor", func(t *testing.T) {

View File

@@ -6,6 +6,7 @@ package db
import (
"fmt"
"gitea.dev/modules/json"
"gitea.dev/modules/setting"
"xorm.io/xorm"
@@ -84,3 +85,21 @@ func CellToInt[T ~int | int64](cell xorm.Cell, def T) (ret T, has bool, err erro
}
return T(val), true, err
}
func UnmarshalFromDB(data []byte, v any) bool {
if len(data) == 0 {
return true
}
err := json.Unmarshal(data, v)
if err != nil {
// Even if database value is corrupted, we should not let it cause unnecessary 500 error or block user's operation
// Corrupted database value is a really rare case, it should never happen unless site admin manually modified the database.
// Although some database values will be printed into logs, it isn't a real problem
// because such case should never really happen. If it happens, logging the value
// is the only way to tell site admin which record is wrong since there is no other context information,
// and then they need to re-create the config (with new secrets)
setting.PanicInDevOrTesting("UnmarshalFromDB %q failed: %v", string(data), err)
return false
}
return true
}

View File

@@ -7,7 +7,7 @@
updated_unix: 1546869730
- id: 2
user_id: 3
user_id: 14
application_id: 1
counter: 1
scope: "openid"
@@ -23,7 +23,7 @@
updated_unix: 1546869730
- id: 4
user_id: 99
user_id: 2
application_id: 2
counter: 1
scope: "whatever"

View File

@@ -1,19 +0,0 @@
-
id: 1
uid: 1
uri: https://user1.domain1.tld/
show: false
-
id: 2
uid: 1
uri: http://user1.domain2.tld/
show: true
-
id: 3
uid: 2
uri: https://domain1.tld/user2/
show: true
# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly

View File

@@ -217,11 +217,6 @@ func GetDeletedBranchByID(ctx context.Context, repoID, branchID int64) (*Branch,
return &branch, nil
}
func DeleteRepoBranches(ctx context.Context, repoID int64) error {
_, err := db.GetEngine(ctx).Where("repo_id=?", repoID).Delete(new(Branch))
return err
}
func DeleteBranches(ctx context.Context, repoID, doerID int64, branchIDs []int64) error {
return db.WithTx(ctx, func(ctx context.Context) error {
branches := make([]*Branch, 0, len(branchIDs))

256
models/issues/code_owner.go Normal file
View File

@@ -0,0 +1,256 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package issues
import (
"context"
"fmt"
"strings"
"time"
org_model "gitea.dev/models/organization"
user_model "gitea.dev/models/user"
"gitea.dev/modules/glob"
"gitea.dev/modules/optional"
"github.com/dlclark/regexp2/v2"
)
// GetCodeOwnersFromContent returns the code owners configuration
// Return empty slice if files missing
// Return warning messages on parsing errors
// We're trying to do the best we can when parsing a file.
// Invalid lines are skipped. Non-existent users and teams too.
func GetCodeOwnersFromContent(ctx context.Context, data string) (rules []*CodeOwnerRule, warnings []string) {
globalReview := optional.None[bool]()
sectionReview := optional.None[bool]()
section := ""
var owners []string
lineNum := 0
for line := range strings.SplitSeq(data, "\n") {
lineNum++
line, reviewRequest, annotationWarnings := parseCodeOwnerAnnotation(line)
for _, warning := range annotationWarnings {
warnings = append(warnings, fmt.Sprintf("Line: %d: %s", lineNum, warning))
}
line = strings.TrimSpace(line)
if line == "" {
if reviewRequest.Has() {
globalReview = reviewRequest
}
continue
}
// To support section headers, there could be an edge case for end users:
// `[a-z] @user` now becomes a section header instead of a rule, but it is really rare to happen.
// A workaround is to use `/[a-z] @user` to make it a rule.
if heading := line; strings.HasPrefix(heading, "[") {
if end := strings.IndexByte(heading, ']'); end > 1 && (end == len(heading)-1 || heading[end+1] == ' ' || heading[end+1] == '\t') {
section = heading[1:end]
owners = TokenizeCodeOwnersLine(heading[end+1:])
sectionReview = reviewRequest
continue
}
}
tokens := TokenizeCodeOwnersLine(line)
if len(tokens) == 0 {
continue
}
if len(tokens) == 1 && len(owners) > 0 {
tokens = append(tokens, owners...)
}
if len(tokens) < 2 {
warnings = append(warnings, fmt.Sprintf("Line: %d: incorrect format", lineNum))
continue
}
rule, wr := ParseCodeOwnersLine(ctx, tokens)
for _, w := range wr {
warnings = append(warnings, fmt.Sprintf("Line: %d: %s", lineNum, w))
}
if rule == nil {
continue
}
rule.LineNum = lineNum
rule.Section = section
rule.ReviewRequest = reviewRequest
if !rule.ReviewRequest.Has() {
rule.ReviewRequest = sectionReview
}
if !rule.ReviewRequest.Has() {
rule.ReviewRequest = globalReview
}
rules = append(rules, rule)
}
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 {
LineNum int
Section string
ReviewRequest optional.Option[bool]
Rule *regexp2.Regexp // it supports negative lookahead, does better for end users
Negative bool
// TODO: these objects should not be loaded here, but rather lazily when needed.
// The parsed rule should only contain the user/team names.
Users []*user_model.User
Teams []*org_model.Team
}
func ParseCodeOwnersLine(ctx context.Context, tokens []string) (rule *CodeOwnerRule, warnings []string) {
rule = &CodeOwnerRule{Negative: strings.HasPrefix(tokens[0], "!")}
// Strip leading "!" for negative rules, then strip leading "/" since
// git returns relative paths (e.g. "docs/foo.md" not "/docs/foo.md")
// and the regex is already anchored with ^...$, so the "/" is redundant.
pattern := strings.TrimPrefix(tokens[0], "!")
pattern = strings.TrimPrefix(pattern, "/")
expr := fmt.Sprintf("^%s$", pattern)
// To make life easier, we support glob expressions if the pattern contains a "*" or "/*" (e.g. "*.md" or "docs/*.md")
// However, "src/test\\..*\\.js" is still the only way because "src/test.*.js" can't be used as a glob at the moment.
// TODO: In the future, maybe it's better to introduce a new annotation to specify the pattern type, e.g.: "pattern-syntax=glob" or "pattern-syntax=regexp"
if strings.HasPrefix(pattern, "*") || strings.Contains(pattern, "/*") {
compiled, compileErr := glob.CompileToRegexp(pattern)
if compileErr != nil {
return nil, []string{fmt.Sprintf("incorrect codeowner glob: %s", compileErr)}
}
expr = compiled.String()
}
var err error
rule.Rule, err = regexp2.Compile(expr, regexp2.None)
if err != nil {
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, "@")
// Only @org/team can contain slashes
if strings.Contains(user, "/") {
s := strings.Split(user, "/")
if len(s) != 2 {
warnings = append(warnings, "incorrect codeowner group: "+user)
continue
}
orgName := s[0]
teamName := s[1]
org, err := org_model.GetOrgByName(ctx, orgName)
if err != nil {
warnings = append(warnings, "incorrect codeowner organization: "+user)
continue
}
teams, err := org.LoadTeams(ctx)
if err != nil {
warnings = append(warnings, "incorrect codeowner team: "+user)
continue
}
for _, team := range teams {
if team.Name == teamName {
rule.Teams = append(rule.Teams, team)
}
}
} else {
u, err := user_model.GetUserByName(ctx, user)
if err != nil {
warnings = append(warnings, "incorrect codeowner user: "+user)
continue
}
rule.Users = append(rule.Users, u)
}
}
if len(rule.Users) == 0 && len(rule.Teams) == 0 {
warnings = append(warnings, "no users/groups matched")
return nil, warnings
}
return rule, warnings
}
func TokenizeCodeOwnersLine(line string) (tokens []string) {
line = strings.TrimSpace(line)
line = strings.ReplaceAll(line, "\t", " ")
if line == "" {
return nil
}
escape := false
var token strings.Builder
for _, char := range line {
if escape {
token.WriteRune(char)
escape = false
} else if char == '\\' {
escape = true
} else if char == '#' {
break
} else if char == ' ' {
if token.Len() > 0 {
tokens = append(tokens, token.String())
token.Reset()
}
} else {
token.WriteRune(char)
}
}
if token.Len() > 0 {
tokens = append(tokens, token.String())
}
return tokens
}
func parseCodeOwnerAnnotation(line string) (string, optional.Option[bool], []string) {
escape := false
for i := 0; i < len(line); i++ {
if escape {
escape = false
continue
}
if line[i] == '\\' {
escape = true
continue
}
if line[i] != '#' {
continue
}
review := optional.None[bool]()
var warnings []string
if strings.HasPrefix(line[i:], "##") {
annotation, _, _ := strings.Cut(line[i+2:], "#")
for field := range strings.FieldsSeq(annotation) {
key, value, ok := strings.Cut(field, "=")
if key != "review-request" {
warnings = append(warnings, "unknown codeowner annotation: "+key)
continue
}
if !ok || (value != "true" && value != "false") {
warnings = append(warnings, "incorrect review-request annotation: "+field)
continue
}
review = optional.Some(value == "true")
}
}
return line[:i], review, warnings
}
return line, optional.None[bool](), nil
}

View File

@@ -12,6 +12,7 @@ import (
"slices"
"strconv"
"strings"
"sync"
"gitea.dev/models/db"
project_model "gitea.dev/models/project"
@@ -112,10 +113,15 @@ type Issue struct {
TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
}
var (
issueTasksPat = regexp.MustCompile(`(^\s*[-*]\s\[[\sxX]\]\s.)|(\n\s*[-*]\s\[[\sxX]\]\s.)`)
issueTasksDonePat = regexp.MustCompile(`(^\s*[-*]\s\[[xX]\]\s.)|(\n\s*[-*]\s\[[xX]\]\s.)`)
)
var globalVars = sync.OnceValue(func() (ret struct {
issueTasksPat *regexp.Regexp
issueTasksDonePat *regexp.Regexp
},
) {
ret.issueTasksPat = regexp.MustCompile(`(^\s*[-*]\s\[[\sxX]\]\s.)|(\n\s*[-*]\s\[[\sxX]\]\s.)`)
ret.issueTasksDonePat = regexp.MustCompile(`(^\s*[-*]\s\[[xX]\]\s.)|(\n\s*[-*]\s\[[xX]\]\s.)`)
return ret
})
// IssueIndex represents the issue index table
type IssueIndex db.ResourceIndex
@@ -463,12 +469,12 @@ func (issue *Issue) IsPoster(uid int64) bool {
// GetTasks returns the amount of tasks in the issues content
func (issue *Issue) GetTasks() int {
return len(issueTasksPat.FindAllStringIndex(issue.Content, -1))
return len(globalVars().issueTasksPat.FindAllStringIndex(issue.Content, -1))
}
// GetTasksDone returns the amount of completed tasks in the issues content
func (issue *Issue) GetTasksDone() int {
return len(issueTasksDonePat.FindAllStringIndex(issue.Content, -1))
return len(globalVars().issueTasksDonePat.FindAllStringIndex(issue.Content, -1))
}
// GetLastEventTimestamp returns the last user visible event timestamp, either the creation of this issue or the close.

View File

@@ -313,6 +313,7 @@ func clearIssueLabels(ctx context.Context, issue *Issue, doer *user_model.User)
return fmt.Errorf("removeLabel: %w", err)
}
}
issue.Labels = nil
return nil
}
@@ -435,6 +436,7 @@ func ReplaceIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer
}
}
issue.isLabelsLoaded = false
issue.Labels = nil
return issue.LoadLabels(ctx)
})

View File

@@ -244,13 +244,7 @@ func NewLabel(ctx context.Context, l *Label) error {
func NewLabels(ctx context.Context, labels ...*Label) error {
return db.WithTx(ctx, func(ctx context.Context) error {
for _, l := range labels {
color, err := label.NormalizeColor(l.Color)
if err != nil {
return err
}
l.Color = color
if err := db.Insert(ctx, l); err != nil {
if err := NewLabel(ctx, l); err != nil {
return err
}
}

View File

@@ -10,7 +10,7 @@ import (
"fmt"
"io"
"strings"
"time"
"sync"
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
@@ -24,11 +24,22 @@ import (
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"github.com/dlclark/regexp2/v2"
"xorm.io/builder"
)
var ErrMustCollaborator = util.NewPermissionDeniedErrorf("user must be a collaborator")
var Errors = sync.OnceValue(func() (ret struct {
MustRepoUnitReader error
MustMemberOrCollaborator error
MustRepoUnitWriter error
MustHeadRepoCodeWriter error
},
) {
ret.MustRepoUnitReader = util.NewPermissionDeniedErrorf("user must have read access")
ret.MustMemberOrCollaborator = util.NewPermissionDeniedErrorf("user must be a member or collaborator")
ret.MustRepoUnitWriter = util.NewPermissionDeniedErrorf("user must have write access")
ret.MustHeadRepoCodeWriter = util.NewPermissionDeniedErrorf("user must have write access to the head repository's code")
return ret
})
const reviewedBy = "Reviewed-by: "
@@ -825,162 +836,6 @@ func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr *
return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0
}
// GetCodeOwnersFromContent returns the code owners configuration
// Return empty slice if files missing
// Return warning messages on parsing errors
// We're trying to do the best we can when parsing a file.
// Invalid lines are skipped. Non-existent users and teams too.
func GetCodeOwnersFromContent(ctx context.Context, data string) ([]*CodeOwnerRule, []string) {
if len(data) == 0 {
return nil, nil
}
rules := make([]*CodeOwnerRule, 0)
lines := strings.Split(data, "\n")
warnings := make([]string, 0)
for i, line := range lines {
tokens := TokenizeCodeOwnersLine(line)
if len(tokens) == 0 {
continue
} else if len(tokens) < 2 {
warnings = append(warnings, fmt.Sprintf("Line: %d: incorrect format", i+1))
continue
}
rule, wr := ParseCodeOwnersLine(ctx, tokens)
for _, w := range wr {
warnings = append(warnings, fmt.Sprintf("Line: %d: %s", i+1, w))
}
if rule == nil {
continue
}
rules = append(rules, rule)
}
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
Users []*user_model.User
Teams []*org_model.Team
}
func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule, []string) {
var err error
rule := &CodeOwnerRule{
Users: make([]*user_model.User, 0),
Teams: make([]*org_model.Team, 0),
Negative: strings.HasPrefix(tokens[0], "!"),
}
warnings := make([]string, 0)
// Strip leading "!" for negative rules, then strip leading "/" since
// git returns relative paths (e.g. "docs/foo.md" not "/docs/foo.md")
// and the regex is already anchored with ^...$, so the "/" is redundant.
pattern := strings.TrimPrefix(tokens[0], "!")
pattern = strings.TrimPrefix(pattern, "/")
expr := fmt.Sprintf("^%s$", pattern)
rule.Rule, err = regexp2.Compile(expr, regexp2.None)
if err != nil {
warnings = append(warnings, fmt.Sprintf("incorrect codeowner regexp: %s", err))
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, "@")
// Only @org/team can contain slashes
if strings.Contains(user, "/") {
s := strings.Split(user, "/")
if len(s) != 2 {
warnings = append(warnings, "incorrect codeowner group: "+user)
continue
}
orgName := s[0]
teamName := s[1]
org, err := org_model.GetOrgByName(ctx, orgName)
if err != nil {
warnings = append(warnings, "incorrect codeowner organization: "+user)
continue
}
teams, err := org.LoadTeams(ctx)
if err != nil {
warnings = append(warnings, "incorrect codeowner team: "+user)
continue
}
for _, team := range teams {
if team.Name == teamName {
rule.Teams = append(rule.Teams, team)
}
}
} else {
u, err := user_model.GetUserByName(ctx, user)
if err != nil {
warnings = append(warnings, "incorrect codeowner user: "+user)
continue
}
rule.Users = append(rule.Users, u)
}
}
if (len(rule.Users) == 0) && (len(rule.Teams) == 0) {
warnings = append(warnings, "no users/groups matched")
return nil, warnings
}
return rule, warnings
}
func TokenizeCodeOwnersLine(line string) []string {
if len(line) == 0 {
return nil
}
line = strings.TrimSpace(line)
line = strings.ReplaceAll(line, "\t", " ")
tokens := make([]string, 0)
escape := false
token := ""
for _, char := range line {
if escape {
token += string(char)
escape = false
} else if string(char) == "\\" {
escape = true
} else if string(char) == "#" {
break
} else if string(char) == " " {
if len(token) > 0 {
tokens = append(tokens, token)
token = ""
}
} else {
token += string(char)
}
}
if len(token) > 0 {
tokens = append(tokens, token)
}
return tokens
}
// InsertPullRequests inserted pull requests
func InsertPullRequests(ctx context.Context, prs ...*PullRequest) error {
return db.WithTx(ctx, func(ctx context.Context) error {

View File

@@ -44,6 +44,7 @@ func TestPullRequest(t *testing.T) {
t.Run("ParseCodeOwnersLine", testParseCodeOwnersLine)
t.Run("CodeOwnerAbsolutePathPatterns", testCodeOwnerAbsolutePathPatterns)
t.Run("CodeOwnerPatternMatchTimeout", testCodeOwnerPatternMatchTimeout)
t.Run("CodeOwnerSectionsAndAnnotations", testCodeOwnerSectionsAndAnnotations)
t.Run("GetApprovers", testGetApprovers)
t.Run("GetPullRequestByMergedCommit", testGetPullRequestByMergedCommit)
t.Run("Migrate_InsertPullRequests", testMigrateInsertPullRequests)
@@ -335,7 +336,7 @@ func testParseCodeOwnersLine(t *testing.T) {
given := []CodeOwnerTest{
{Line: "", Tokens: nil},
{Line: "# comment", Tokens: []string{}},
{Line: "# comment", Tokens: nil},
{Line: "!.* @user1 @org1/team1", Tokens: []string{"!.*", "@user1", "@org1/team1"}},
{Line: `.*\\.js @user2 #comment`, Tokens: []string{`.*\.js`, "@user2"}},
{Line: `docs/(aws|google|azure)/[^/]*\\.(md|txt) @org3 @org2/team2`, Tokens: []string{`docs/(aws|google|azure)/[^/]*\.(md|txt)`, "@org3", "@org2/team2"}},
@@ -347,7 +348,7 @@ func testParseCodeOwnersLine(t *testing.T) {
for _, g := range given {
tokens := issues_model.TokenizeCodeOwnersLine(g.Line)
assert.Equal(t, g.Tokens, tokens, "Codeowners tokenizer failed")
assert.Equal(t, g.Tokens, tokens, "Codeowners tokenizer failed: input=%s", g.Line)
}
}
@@ -448,3 +449,49 @@ func testMigrateInsertPullRequests(t *testing.T) {
unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.PullRequest{})
}
func testCodeOwnerSectionsAndAnnotations(t *testing.T) {
rules, warnings := issues_model.GetCodeOwnersFromContent(t.Context(), "*.js @user5\n[Docs] @user1\n*.md\n")
require.Empty(t, warnings)
require.Len(t, rules, 2)
for _, rule := range rules {
assert.False(t, rule.ReviewRequest.Has())
}
rules, warnings = issues_model.GetCodeOwnersFromContent(t.Context(), `## review-request=false # global
*.js @user5 ## review-request=true # line
[Documentation] @user1 ## review-request=true # section
*.md
/docs/* @user5 ## review-request=false
[Other Section] @user5
*.txt
`)
match := func(rule *issues_model.CodeOwnerRule, path string) bool {
v, _ := rule.Rule.MatchString(path)
return v
}
require.Empty(t, warnings)
require.Len(t, rules, 4)
assert.True(t, rules[0].ReviewRequest.Value())
assert.True(t, match(rules[0], "src/app.js"))
assert.False(t, match(rules[0], "appXjs"))
assert.Equal(t, "Documentation", rules[1].Section)
assert.Equal(t, "user1", rules[1].Users[0].Name)
assert.True(t, rules[1].ReviewRequest.Value())
assert.Equal(t, "user5", rules[2].Users[0].Name)
assert.False(t, rules[2].ReviewRequest.Value())
assert.True(t, match(rules[2], "docs/guide.md"))
assert.False(t, match(rules[2], "other/docs/guide.md"))
assert.False(t, rules[3].ReviewRequest.Value())
rules, warnings = issues_model.GetCodeOwnersFromContent(t.Context(), "*.js @user5 ## review-request=invalid\n*[ @user5\n")
require.Len(t, rules, 1)
assert.False(t, rules[0].ReviewRequest.Has())
require.Len(t, warnings, 2)
rules, warnings = issues_model.GetCodeOwnersFromContent(t.Context(), `file\#name @user5 ## review-request=false`)
require.Empty(t, warnings)
require.Len(t, rules, 1)
assert.True(t, match(rules[0], "file#name"))
assert.False(t, rules[0].ReviewRequest.Value())
}

View File

@@ -121,11 +121,15 @@ func (rt ReviewType) Icon() string {
// Review represents collection of code comments giving feedback for a PR
type Review struct {
ID int64 `xorm:"pk autoincr"`
Type ReviewType
Reviewer *user_model.User `xorm:"-"`
ReviewerID int64 `xorm:"index"`
ReviewerTeamID int64 `xorm:"NOT NULL DEFAULT 0"`
ID int64 `xorm:"pk autoincr"`
Type ReviewType
Reviewer *user_model.User `xorm:"-"`
// HINT: REVIEWER-ID-NEGATIVE: ReviewerID and ReviewerTeamID must be positive at the moment,
// because the current template uses a trick "ItemID < 0" means "team id"
ReviewerID int64 `xorm:"index"`
ReviewerTeamID int64 `xorm:"NOT NULL DEFAULT 0"`
ReviewerTeam *organization.Team `xorm:"-"`
OriginalAuthor string
OriginalAuthorID int64
@@ -186,7 +190,7 @@ func (r *Review) LoadReviewerTeam(ctx context.Context) (err error) {
return nil
}
r.ReviewerTeam, err = organization.GetTeamByID(ctx, r.ReviewerTeamID)
r.ReviewerTeamID, r.ReviewerTeam, err = organization.GetPossibleTeamByID(ctx, r.ReviewerTeamID)
return err
}
@@ -285,23 +289,12 @@ func IsOfficialReviewer(ctx context.Context, issue *Issue, reviewer *user_model.
}
if rule == nil {
// if no rule is found, then user with write access can make official reviews
err := pr.LoadBaseRepo(ctx)
if err != nil {
if err := pr.LoadBaseRepo(ctx); err != nil {
return false, err
}
writeAccess, err := access_model.HasAccessUnit(ctx, reviewer, pr.BaseRepo, unit.TypeCode, perm.AccessModeWrite)
if err != nil {
return false, err
}
return writeAccess, nil
return access_model.HasAccessUnit(ctx, reviewer, pr.BaseRepo, unit.TypeCode, perm.AccessModeWrite)
}
official, err := git_model.IsUserOfficialReviewer(ctx, rule, reviewer)
if official || err != nil {
return official, err
}
return false, nil
return git_model.IsUserOfficialReviewer(ctx, rule, reviewer)
}
// IsOfficialReviewerTeam check if reviewer in this team can make official reviews in issue (counts towards required approvals)
@@ -313,66 +306,82 @@ func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organizatio
if err != nil {
return false, err
}
if pb == nil {
return false, nil
if pb != nil && pb.EnableApprovalsWhitelist {
return slices.Contains(pb.ApprovalsWhitelistTeamIDs, team.ID), nil
}
if !pb.EnableApprovalsWhitelist {
return team.UnitAccessMode(ctx, unit.TypeCode) >= perm.AccessModeWrite, nil
}
return slices.Contains(pb.ApprovalsWhitelistTeamIDs, team.ID), nil
return team.UnitAccessMode(ctx, unit.TypeCode) >= perm.AccessModeWrite, nil
}
// RecalculateReviewsOfficial re-evaluates the "official" flag of the latest approve
// and reject reviews of an issue against its pull request's current base branch.
// It must be called whenever the target branch changes, otherwise an approval that
// was official on the previous (possibly unprotected) branch would keep satisfying
// the new branch's protection rules.
func RecalculateReviewsOfficial(ctx context.Context, issue *Issue) error {
if err := issue.LoadPullRequest(ctx); err != nil {
// updateReviewerOfficial keeps the official flag only on the reviewer's latest approval, rejection or review request, and only if the reviewer is official
func updateReviewerOfficial(ctx context.Context, issueID, reviewerID int64, official bool) error {
if _, err := db.GetEngine(ctx).Where(builder.Eq{"issue_id": issueID, "reviewer_id": reviewerID}).
NoAutoTime().Cols("official").Update(&Review{}); err != nil {
return err
}
if !official {
return nil
}
latest, err := GetReviewByIssueIDAndUserID(ctx, issueID, reviewerID)
if IsErrReviewNotExist(err) {
return nil
} else if err != nil {
return err
}
_, err = db.GetEngine(ctx).ID(latest.ID).NoAutoTime().Cols("official").Update(&Review{Official: true})
return err
}
// Clearing and restoring the official flags must happen atomically, otherwise a
// failure in between would leave the reviews without any official flag set.
// RecalculateReviewsOfficial re-evaluates the "official" flag of an issue's reviews and review requests
// against its pull request's current base branch. It must be called whenever the target branch changes,
// otherwise an approval that was official on the previous (possibly unprotected) branch would keep
// satisfying the new branch's protection rules.
func RecalculateReviewsOfficial(ctx context.Context, issue *Issue) error {
return db.WithTx(ctx, func(ctx context.Context) error {
// Only the latest approve/reject review of each reviewer counts as official, so
// clear the flag on all of them first and restore it only where it still applies.
if _, err := db.GetEngine(ctx).
Where("issue_id = ?", issue.ID).
In("type", ReviewTypeApprove, ReviewTypeReject).
Cols("official").
Update(&Review{Official: false}); err != nil {
if _, err := db.GetEngine(ctx).Where("issue_id = ?", issue.ID).NoAutoTime().Cols("official").Update(&Review{}); err != nil {
return err
}
reviews, err := FindLatestReviews(ctx, FindReviewOptions{
Types: []ReviewType{ReviewTypeApprove, ReviewTypeReject},
IssueID: issue.ID,
})
if err != nil {
reviewerIDs := make([]int64, 0, 5)
if err := db.GetEngine(ctx).Table("review").Cols("reviewer_id").
Where(builder.Eq{"issue_id": issue.ID, "original_author_id": 0}).
And(builder.In("type", ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest)).
And(builder.Neq{"reviewer_id": 0}).
Distinct("reviewer_id").Find(&reviewerIDs); err != nil {
return err
}
for _, review := range reviews {
if err := review.LoadReviewer(ctx); err != nil {
return err
}
if review.Reviewer == nil {
continue
}
official, err := IsOfficialReviewer(ctx, issue, review.Reviewer)
for _, reviewerID := range reviewerIDs {
_, reviewer, err := user_model.GetPossibleUserByID(ctx, reviewerID)
if err != nil {
return err
}
if official {
if _, err := db.GetEngine(ctx).ID(review.ID).Cols("official").Update(&Review{Official: true}); err != nil {
return err
}
official, err := IsOfficialReviewer(ctx, issue, reviewer)
if err != nil {
return err
}
if err := updateReviewerOfficial(ctx, issue.ID, reviewerID, official); err != nil {
return err
}
}
teamRequests := make([]*Review, 0, 5)
if err := db.GetEngine(ctx).Where(builder.Eq{"issue_id": issue.ID, "type": ReviewTypeRequest}).
And(builder.Gt{"reviewer_team_id": 0}).Find(&teamRequests); err != nil {
return err
}
for _, request := range teamRequests {
if err := request.LoadReviewerTeam(ctx); organization.IsErrTeamNotExist(err) {
continue
} else if err != nil {
return err
}
official, err := IsOfficialReviewerTeam(ctx, issue, request.ReviewerTeam)
if err != nil {
return err
}
if _, err := db.GetEngine(ctx).ID(request.ID).NoAutoTime().Cols("official").Update(&Review{Official: official}); err != nil {
return err
}
}
return nil
})
}
@@ -472,7 +481,13 @@ func SubmitReview(ctx context.Context, doer *user_model.User, issue *Issue, revi
defer committer.Close()
sess := db.GetEngine(ctx)
isApproveOrReject := reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject
official := false
if isApproveOrReject {
if official, err = IsOfficialReviewer(ctx, issue, doer); err != nil {
return nil, nil, err
}
}
review, err := GetCurrentReview(ctx, doer, issue)
if err != nil {
@@ -484,16 +499,6 @@ func SubmitReview(ctx context.Context, doer *user_model.User, issue *Issue, revi
return nil, nil, ContentEmptyErr{}
}
if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
// Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil {
return nil, nil, err
}
if official, err = IsOfficialReviewer(ctx, issue, doer); err != nil {
return nil, nil, err
}
}
// No current review. Create a new one!
if review, err = CreateReview(ctx, CreateReviewOptions{
Type: reviewType,
@@ -514,16 +519,6 @@ func SubmitReview(ctx context.Context, doer *user_model.User, issue *Issue, revi
return nil, nil, ContentEmptyErr{}
}
if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
// Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil {
return nil, nil, err
}
if official, err = IsOfficialReviewer(ctx, issue, doer); err != nil {
return nil, nil, err
}
}
review.Official = official
review.Issue = issue
review.Content = content
@@ -544,6 +539,16 @@ func SubmitReview(ctx context.Context, doer *user_model.User, issue *Issue, revi
}
}
if isApproveOrReject {
if err := updateReviewerOfficial(ctx, issue.ID, doer.ID, official); err != nil {
return nil, nil, err
}
}
if reviewType == ReviewTypePending {
return review, nil, committer.Commit()
}
comm, err := CreateComment(ctx, &CreateCommentOptions{
Type: CommentTypeReview,
Doer: doer,
@@ -558,7 +563,7 @@ func SubmitReview(ctx context.Context, doer *user_model.User, issue *Issue, revi
}
// try to remove team review request if need
if issue.Repo.Owner.IsOrganization() && (reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject) {
if issue.Repo.Owner.IsOrganization() && isApproveOrReject {
teamReviewRequests := make([]*Review, 0, 10)
if err := sess.SQL("SELECT * FROM review WHERE issue_id = ? AND reviewer_team_id > 0 AND type = ?", issue.ID, ReviewTypeRequest).Find(&teamReviewRequests); err != nil {
return nil, nil, err
@@ -696,8 +701,6 @@ func InsertReviews(ctx context.Context, reviews []*Review) error {
// AddReviewRequest add a review request from one reviewer
func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_model.User, isCodeOwners bool) (*Comment, error) {
return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) {
sess := db.GetEngine(ctx)
review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID)
if err != nil && !IsErrReviewNotExist(err) {
return nil, err
@@ -723,15 +726,9 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo
}
}
// if the reviewer is an official reviewer,
// remove the official flag in the all previous reviews
official, err := IsOfficialReviewer(ctx, issue, reviewer)
if err != nil {
return nil, err
} else if official {
if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, reviewer.ID); err != nil {
return nil, err
}
}
review, err = CreateReview(ctx, CreateReviewOptions{
@@ -745,6 +742,10 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo
return nil, err
}
if err := updateReviewerOfficial(ctx, issue.ID, reviewer.ID, official); err != nil {
return nil, err
}
comment, err := CreateComment(ctx, &CreateCommentOptions{
Type: CommentTypeReviewRequest,
Doer: doer,
@@ -785,10 +786,9 @@ func RemoveReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user
official, err := IsOfficialReviewer(ctx, issue, reviewer)
if err != nil {
return nil, err
} else if official {
if err := restoreLatestOfficialReview(ctx, issue.ID, reviewer.ID); err != nil {
return nil, err
}
}
if err := updateReviewerOfficial(ctx, issue.ID, reviewer.ID, official); err != nil {
return nil, err
}
return CreateComment(ctx, &CreateCommentOptions{
@@ -802,22 +802,6 @@ func RemoveReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user
})
}
// Recalculate the latest official review for reviewer
func restoreLatestOfficialReview(ctx context.Context, issueID, reviewerID int64) error {
review, err := GetReviewByIssueIDAndUserID(ctx, issueID, reviewerID)
if err != nil && !IsErrReviewNotExist(err) {
return err
}
if review != nil {
if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil {
return err
}
}
return nil
}
// AddTeamReviewRequest add a review request from one team
func AddTeamReviewRequest(ctx context.Context, issue *Issue, reviewer *organization.Team, doer *user_model.User, isCodeOwners bool) (*Comment, error) {
return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) {
@@ -834,10 +818,6 @@ func AddTeamReviewRequest(ctx context.Context, issue *Issue, reviewer *organizat
official, err := IsOfficialReviewerTeam(ctx, issue, reviewer)
if err != nil {
return nil, fmt.Errorf("isOfficialReviewerTeam(): %w", err)
} else if !official {
if official, err = IsOfficialReviewer(ctx, issue, doer); err != nil {
return nil, fmt.Errorf("isOfficialReviewer(): %w", err)
}
}
if review, err = CreateReview(ctx, CreateReviewOptions{
@@ -850,12 +830,6 @@ func AddTeamReviewRequest(ctx context.Context, issue *Issue, reviewer *organizat
return nil, err
}
if official {
if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_team_id=?", false, issue.ID, reviewer.ID); err != nil {
return nil, err
}
}
comment, err := CreateComment(ctx, &CreateCommentOptions{
Type: CommentTypeReviewRequest,
Doer: doer,
@@ -890,25 +864,6 @@ func RemoveTeamReviewRequest(ctx context.Context, issue *Issue, reviewer *organi
return nil, err
}
official, err := IsOfficialReviewerTeam(ctx, issue, reviewer)
if err != nil {
return nil, fmt.Errorf("isOfficialReviewerTeam(): %w", err)
}
if official {
// recalculate which is the latest official review from that team
review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, -reviewer.ID)
if err != nil && !IsErrReviewNotExist(err) {
return nil, err
}
if review != nil {
if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil {
return nil, err
}
}
}
if doer == nil {
return nil, nil
}
@@ -1001,44 +956,22 @@ func DeleteReview(ctx context.Context, r *Review) error {
return errors.New("review request can not be deleted using this method")
}
opts := FindCommentsOptions{
Type: CommentTypeCode,
IssueID: r.IssueID,
ReviewID: r.ID,
}
if _, err := db.Delete[Comment](ctx, opts); err != nil {
return err
}
opts = FindCommentsOptions{
Type: CommentTypeReview,
IssueID: r.IssueID,
ReviewID: r.ID,
}
if _, err := db.Delete[Comment](ctx, opts); err != nil {
return err
}
opts = FindCommentsOptions{
Type: CommentTypeDismissReview,
IssueID: r.IssueID,
ReviewID: r.ID,
}
if _, err := db.Delete[Comment](ctx, opts); err != nil {
return err
for _, commentType := range [...]CommentType{CommentTypeCode, CommentTypeReview, CommentTypeDismissReview} {
if _, err := db.Delete[Comment](ctx, FindCommentsOptions{
Type: commentType,
IssueID: r.IssueID,
ReviewID: r.ID,
}); err != nil {
return err
}
}
if _, err := db.DeleteByID[Review](ctx, r.ID); err != nil {
return err
}
if r.Official {
if err := restoreLatestOfficialReview(ctx, r.IssueID, r.ReviewerID); err != nil {
return err
}
if r.Official && r.OriginalAuthorID == 0 {
return updateReviewerOfficial(ctx, r.IssueID, r.ReviewerID, true)
}
return nil
})

View File

@@ -9,6 +9,7 @@ import (
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/organization"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
@@ -434,3 +435,17 @@ func TestRecalculateReviewsOfficial(t *testing.T) {
review = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: review.ID})
assert.True(t, review.Official)
}
func TestTeamReviewRequestOfficial(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 12})
assert.NoError(t, issue.LoadRepo(t.Context()))
comment, err := issues_model.AddTeamReviewRequest(t.Context(), issue, unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2}), &user_model.User{ID: 2}, false)
assert.NoError(t, err)
assert.True(t, unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: comment.ReviewID}).Official)
assert.NoError(t, db.Insert(t.Context(), &git_model.ProtectedBranch{RepoID: issue.RepoID, RuleName: "master", EnableApprovalsWhitelist: true}))
assert.NoError(t, issues_model.RecalculateReviewsOfficial(t.Context(), issue))
assert.False(t, unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: comment.ReviewID}).Official)
}

View File

@@ -74,9 +74,9 @@ const OwnerTeamName = "Owners"
// Team represents a organization team.
type Team struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"INDEX"`
LowerName string
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"INDEX UNIQUE(s)"`
LowerName string `xorm:"UNIQUE(s)"`
Name string
Description string
AccessMode perm.AccessMode `xorm:"'authorize'"`

View File

@@ -87,15 +87,7 @@ func SearchPackages(ctx context.Context, opts *PackageSearchOptions) ([]*package
if err != nil {
return nil, err
}
pfds := make([]*packages.PackageFileDescriptor, 0, len(pkgFiles))
for _, pf := range pkgFiles {
pfd, err := packages.GetPackageFileDescriptor(ctx, pf)
if err != nil {
return nil, err
}
pfds = append(pfds, pfd)
}
return pfds, nil
return packages.GetPackageFileDescriptors(ctx, pkgFiles)
}
// GetDistributions gets all available distributions

View File

@@ -33,6 +33,7 @@ import (
"gitea.dev/modules/packages/rpm"
"gitea.dev/modules/packages/rubygems"
"gitea.dev/modules/packages/swift"
"gitea.dev/modules/packages/terraform_module"
"gitea.dev/modules/packages/vagrant"
"gitea.dev/modules/util"
@@ -212,8 +213,10 @@ func GetPackageDescriptorWithCache(ctx context.Context, pv *PackageVersion, c *c
metadata = &rubygems.Metadata{}
case TypeSwift:
metadata = &swift.Metadata{}
case TypeTerraformModule:
metadata = &terraform_module.Metadata{}
case TypeTerraformState:
// terraform packages have no metadata
// terraform state packages have no metadata
case TypeVagrant:
metadata = &vagrant.Metadata{}
default:

View File

@@ -30,29 +30,30 @@ type Type string
// List of supported packages
const (
TypeAlpine Type = "alpine"
TypeArch Type = "arch"
TypeCargo Type = "cargo"
TypeChef Type = "chef"
TypeComposer Type = "composer"
TypeConan Type = "conan"
TypeConda Type = "conda"
TypeContainer Type = "container"
TypeCran Type = "cran"
TypeDebian Type = "debian"
TypeGeneric Type = "generic"
TypeGo Type = "go"
TypeHelm Type = "helm"
TypeMaven Type = "maven"
TypeNpm Type = "npm"
TypeNuGet Type = "nuget"
TypePub Type = "pub"
TypePyPI Type = "pypi"
TypeRpm Type = "rpm"
TypeRubyGems Type = "rubygems"
TypeSwift Type = "swift"
TypeTerraformState Type = "terraform"
TypeVagrant Type = "vagrant"
TypeAlpine Type = "alpine"
TypeArch Type = "arch"
TypeCargo Type = "cargo"
TypeChef Type = "chef"
TypeComposer Type = "composer"
TypeConan Type = "conan"
TypeConda Type = "conda"
TypeContainer Type = "container"
TypeCran Type = "cran"
TypeDebian Type = "debian"
TypeGeneric Type = "generic"
TypeGo Type = "go"
TypeHelm Type = "helm"
TypeMaven Type = "maven"
TypeNpm Type = "npm"
TypeNuGet Type = "nuget"
TypePub Type = "pub"
TypePyPI Type = "pypi"
TypeRpm Type = "rpm"
TypeRubyGems Type = "rubygems"
TypeSwift Type = "swift"
TypeTerraformModule Type = "terraform-module"
TypeTerraformState Type = "terraform"
TypeVagrant Type = "vagrant"
)
var TypeList = []Type{
@@ -77,6 +78,7 @@ var TypeList = []Type{
TypeRpm,
TypeRubyGems,
TypeSwift,
TypeTerraformModule,
TypeTerraformState,
TypeVagrant,
}
@@ -126,6 +128,8 @@ func (pt Type) Name() string {
return "RubyGems"
case TypeSwift:
return "Swift"
case TypeTerraformModule:
return "Terraform Module"
case TypeTerraformState:
return "Terraform State"
case TypeVagrant:
@@ -179,6 +183,8 @@ func (pt Type) SVGName() string {
return "gitea-rubygems"
case TypeSwift:
return "gitea-swift"
case TypeTerraformModule:
return "gitea-terraform"
case TypeTerraformState:
return "gitea-terraform"
case TypeVagrant:

View File

@@ -224,30 +224,32 @@ func RecalculateTeamAccesses(ctx context.Context, repo *repo_model.Repository, i
// RecalculateUserAccess recalculates new access for a single user
// Usable if we know access only affected one user
func RecalculateUserAccess(ctx context.Context, repo *repo_model.Repository, uid int64) (err error) {
func RecalculateUserAccess(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (err error) {
if err = repo.LoadOwner(ctx); err != nil {
return err
}
minMode := perm.AccessModeRead
if !repo.IsPrivate {
if !repo.IsPrivate && !repo.Owner.IsOrganization() && !user.IsRestricted {
minMode = perm.AccessModeWrite
}
accessMode := perm.AccessModeNone
e := db.GetEngine(ctx)
collaborator, err := repo_model.GetCollaboration(ctx, repo.ID, uid)
collaborator, err := repo_model.GetCollaboration(ctx, repo.ID, user.ID)
if err != nil {
return err
} else if collaborator != nil {
accessMode = collaborator.Mode
}
if err = repo.LoadOwner(ctx); err != nil {
return err
} else if repo.Owner.IsOrganization() {
if repo.Owner.IsOrganization() {
var teams []organization.Team
if err := e.Join("INNER", "team_repo", "team_repo.team_id = team.id").
Join("INNER", "team_user", "team_user.team_id = team.id").
Where("team.org_id = ?", repo.OwnerID).
And("team_repo.repo_id=?", repo.ID).
And("team_user.uid=?", uid).
And("team_user.uid=?", user.ID).
Find(&teams); err != nil {
return err
}
@@ -262,10 +264,10 @@ func RecalculateUserAccess(ctx context.Context, repo *repo_model.Repository, uid
}
// Delete old user accesses and insert new one for repository.
if _, err = e.Delete(&Access{RepoID: repo.ID, UserID: uid}); err != nil {
if _, err = e.Delete(&Access{RepoID: repo.ID, UserID: user.ID}); err != nil {
return fmt.Errorf("delete old user accesses: %w", err)
} else if accessMode >= minMode {
if err = db.Insert(ctx, &Access{RepoID: repo.ID, UserID: uid, Mode: accessMode}); err != nil {
if err = db.Insert(ctx, &Access{RepoID: repo.ID, UserID: user.ID, Mode: accessMode}); err != nil {
return fmt.Errorf("insert new user accesses: %w", err)
}
}

View File

@@ -35,7 +35,8 @@ type Permission struct {
everyoneAccessMode map[unit.Type]perm_model.AccessMode // the unit's minimal access mode for every signed-in user
anonymousAccessMode map[unit.Type]perm_model.AccessMode // the unit's minimal access mode for anonymous (non-signed-in) user
orgRepoTeams []*organization.Team
isCollaborator bool
orgRepoTeams []*organization.Team
}
// IsOwner returns true if current user is the owner of repository.
@@ -48,6 +49,10 @@ func (p *Permission) IsAdmin() bool {
return p.AccessMode >= perm_model.AccessModeAdmin
}
func (p *Permission) IsMemberOrCollaborator() bool {
return p.IsAdmin() || p.isCollaborator || len(p.orgRepoTeams) > 0
}
// HasAnyUnitAccess returns true if the user might have at least one access mode to any unit of this repository.
// It doesn't count the "public(anonymous/everyone) access mode".
// TODO: most calls to this function should be replaced with `HasAnyUnitAccessOrPublicAccess`
@@ -240,23 +245,10 @@ func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) {
origPermUnits := perm.units
perm.units = make([]*repo_model.RepoUnit, 0, len(perm.units))
for _, u := range origPermUnits {
shouldKeep := false
for t := range perm.unitsMode {
if shouldKeep = u.Type == t; shouldKeep {
break
}
}
for t := range perm.anonymousAccessMode {
if shouldKeep = shouldKeep || u.Type == t; shouldKeep {
break
}
}
for t := range perm.everyoneAccessMode {
if shouldKeep = shouldKeep || u.Type == t; shouldKeep {
break
}
}
if shouldKeep {
_, hasUnitMode := perm.unitsMode[u.Type]
_, hasAnonymousMode := perm.anonymousAccessMode[u.Type]
_, hasEveryoneMode := perm.everyoneAccessMode[u.Type]
if hasUnitMode || hasAnonymousMode || hasEveryoneMode {
perm.units = append(perm.units, u)
}
}
@@ -445,9 +437,8 @@ func GetIndividualUserRepoPermission(ctx context.Context, repo *repo_model.Repos
return perm, nil
}
var isCollaborator bool
if user != nil {
isCollaborator, err = repo_model.IsCollaborator(ctx, repo.ID, user.ID)
perm.isCollaborator, err = repo_model.IsCollaborator(ctx, repo.ID, user.ID)
if err != nil {
return perm, err
}
@@ -460,7 +451,7 @@ func GetIndividualUserRepoPermission(ctx context.Context, repo *repo_model.Repos
// Prevent strangers from checking out public repo of private organization/users
// Allow user if they are a collaborator of a repo within a private user or a private organization but not a member of the organization itself
// TODO: rename it to "IsOwnerVisibleToDoer"
if !organization.HasOrgOrUserVisible(ctx, repo.Owner, user) && !isCollaborator {
if !organization.HasOrgOrUserVisible(ctx, repo.Owner, user) && !perm.isCollaborator {
perm.AccessMode = perm_model.AccessModeNone
return perm, nil
}
@@ -503,7 +494,7 @@ func GetIndividualUserRepoPermission(ctx context.Context, repo *repo_model.Repos
perm.unitsMode = make(map[unit.Type]perm_model.AccessMode)
// Collaborators on organization
if isCollaborator {
if perm.isCollaborator {
for _, u := range repo.Units {
perm.unitsMode[u.Type] = perm.AccessMode
}

View File

@@ -9,6 +9,7 @@ import (
"fmt"
"math"
"regexp"
"sync"
"gitea.dev/models/db"
"gitea.dev/modules/setting"
@@ -36,10 +37,12 @@ const (
)
// ColumnColorPattern is a regexp witch can validate ColumnColor
var ColumnColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$")
var ColumnColorPattern = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile("^#[0-9a-fA-F]{6}$")
})
func validateColumnColor(color string) error {
if len(color) != 0 && !ColumnColorPattern.MatchString(color) {
if len(color) != 0 && !ColumnColorPattern().MatchString(color) {
return util.ErrorWrap(util.ErrUnprocessableContent, "invalid column color %q, expected a 6-digit hex string like #FF0000", color)
}
return nil

View File

@@ -43,6 +43,41 @@ func columnIssueIDs(column *Column) []int64 {
return []int64{column.ID}
}
// LockProject serializes concurrent card moves of a project, avoiding row-lock deadlocks between them.
func LockProject(ctx context.Context, projectID int64) error {
_, err := db.GetEngine(ctx).Exec("UPDATE `project` SET title=title WHERE id=?", projectID)
return err
}
// ColumnIssueSorting is an issue's position in a column.
type ColumnIssueSorting struct {
IssueID int64 `xorm:"issue_id"`
Sorting int64 `xorm:"sorting"`
}
// GetColumnIssueSortings returns the column's issues except excludeIssueID in board order.
func GetColumnIssueSortings(ctx context.Context, column *Column, excludeIssueID int64) (rows []ColumnIssueSorting, _ error) {
err := db.GetEngine(ctx).Table("project_issue").
Select("project_issue.issue_id, project_issue.sorting").
Join("INNER", "issue", "issue.id = project_issue.issue_id").
Where("project_issue.project_id=?", column.ProjectID).
In("project_issue.project_board_id", columnIssueIDs(column)).
And("project_issue.issue_id<>?", excludeIssueID).
OrderBy("project_issue.sorting ASC, issue.created_unix DESC, issue.id DESC").
Find(&rows)
return rows, err
}
// SetIssueSorting sets the issue's sorting if it is still in the column.
func SetIssueSorting(ctx context.Context, column *Column, issueID, sorting int64) error {
_, err := db.GetEngine(ctx).
Where("project_id=? AND issue_id=?", column.ProjectID, issueID).
In("project_board_id", columnIssueIDs(column)).
Cols("sorting").
Update(&ProjectIssue{Sorting: sorting})
return err
}
// IsIssueInColumn reports whether the issue is placed in the column.
func IsIssueInColumn(ctx context.Context, issueID int64, column *Column) (bool, error) {
return db.GetEngine(ctx).

View File

@@ -459,9 +459,6 @@ func DeleteProjectByRepoID(ctx context.Context, repoID int64) error {
if _, err := db.GetEngine(ctx).Exec("DELETE FROM project_board WHERE project_board.id IN (SELECT project_board.id FROM project_board INNER JOIN project WHERE project.id = project_board.project_id AND project.repo_id = ?)", repoID); err != nil {
return err
}
if _, err := db.GetEngine(ctx).Table("project").Where("repo_id = ? ", repoID).Delete(&Project{}); err != nil {
return err
}
case setting.Database.Type.IsPostgreSQL():
if _, err := db.GetEngine(ctx).Exec("DELETE FROM project_issue USING project WHERE project.id = project_issue.project_id AND project.repo_id = ? ", repoID); err != nil {
return err
@@ -469,9 +466,6 @@ func DeleteProjectByRepoID(ctx context.Context, repoID int64) error {
if _, err := db.GetEngine(ctx).Exec("DELETE FROM project_board USING project WHERE project.id = project_board.project_id AND project.repo_id = ? ", repoID); err != nil {
return err
}
if _, err := db.GetEngine(ctx).Table("project").Where("repo_id = ? ", repoID).Delete(&Project{}); err != nil {
return err
}
default:
if _, err := db.GetEngine(ctx).Exec("DELETE project_issue FROM project_issue INNER JOIN project ON project.id = project_issue.project_id WHERE project.repo_id = ? ", repoID); err != nil {
return err
@@ -479,10 +473,10 @@ func DeleteProjectByRepoID(ctx context.Context, repoID int64) error {
if _, err := db.GetEngine(ctx).Exec("DELETE project_board FROM project_board INNER JOIN project ON project.id = project_board.project_id WHERE project.repo_id = ? ", repoID); err != nil {
return err
}
if _, err := db.GetEngine(ctx).Table("project").Where("repo_id = ? ", repoID).Delete(&Project{}); err != nil {
return err
}
}
if _, err := db.GetEngine(ctx).Table("project").Where("repo_id = ? ", repoID).Delete(&Project{}); err != nil {
return err
}
return updateRepositoryProjectCount(ctx, repoID)
}

View File

@@ -42,6 +42,10 @@ func init() {
db.RegisterModel(new(Attachment))
}
func (a *Attachment) IsLinked() bool {
return a.IssueID > 0 || a.ReleaseID > 0 || a.CommentID > 0
}
// IncreaseDownloadCount is update download count + 1
func (a *Attachment) IncreaseDownloadCount(ctx context.Context) error {
// Update download count.

View File

@@ -7,54 +7,26 @@ import (
"context"
"gitea.dev/models/unit"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
)
// ___________.__ ___________ __
// \__ ___/|__| _____ ___\__ ___/___________ ____ | | __ ___________
// | | | |/ \_/ __ \| | \_ __ \__ \ _/ ___\| |/ // __ \_ __ \
// | | | | Y Y \ ___/| | | | \// __ \\ \___| <\ ___/| | \/
// |____| |__|__|_| /\___ >____| |__| (____ /\___ >__|_ \\___ >__|
// \/ \/ \/ \/ \/ \/
// CanEnableTimetracker returns true when the server admin enabled time tracking
// This overrules IsTimetrackerEnabled
func (repo *Repository) CanEnableTimetracker() bool {
return setting.Service.EnableTimetracking
}
// IsTimetrackerEnabled returns whether or not the timetracker is enabled. It returns the default value from config if an error occurs.
func (repo *Repository) IsTimetrackerEnabled(ctx context.Context) bool {
if !setting.Service.EnableTimetracking {
return false
}
var u *RepoUnit
var err error
if u, err = repo.GetUnit(ctx, unit.TypeIssues); err != nil {
return setting.Service.DefaultEnableTimetracking
}
return u.IssuesConfig().EnableTimetracker
return repo.MustGetUnit(ctx, unit.TypeIssues).IssuesConfig().EnableTimetracker
}
// AllowOnlyContributorsToTrackTime returns value of IssuesConfig or the default value
func (repo *Repository) AllowOnlyContributorsToTrackTime(ctx context.Context) bool {
var u *RepoUnit
var err error
if u, err = repo.GetUnit(ctx, unit.TypeIssues); err != nil {
return setting.Service.DefaultAllowOnlyContributorsToTrackTime
}
return u.IssuesConfig().AllowOnlyContributorsToTrackTime
return repo.MustGetUnit(ctx, unit.TypeIssues).IssuesConfig().AllowOnlyContributorsToTrackTime
}
// IsDependenciesEnabled returns if dependencies are enabled and returns the default setting if not set.
func (repo *Repository) IsDependenciesEnabled(ctx context.Context) bool {
var u *RepoUnit
var err error
if u, err = repo.GetUnit(ctx, unit.TypeIssues); err != nil {
log.Trace("IsDependenciesEnabled: %v", err)
return setting.Service.DefaultEnableDependencies
}
return u.IssuesConfig().EnableDependencies
return repo.MustGetUnit(ctx, unit.TypeIssues).IssuesConfig().EnableDependencies
}

View File

@@ -151,7 +151,7 @@ func UpdateLanguageStats(ctx context.Context, repo *Repository, commitID string,
var topLang string
var s int64
for lang, size := range stats {
if size > s {
if size > s || (size == s && lang < topLang) {
s = size
topLang = lang
}

View File

@@ -0,0 +1,28 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo_test
import (
"testing"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUpdateLanguageStatsPrimaryTie(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
require.NoError(t, repo_model.UpdateLanguageStats(t.Context(), repo, "1234567890", map[string]int64{"Go": 10, "C": 10}))
stats, err := repo_model.GetLanguageStats(t.Context(), repo)
require.NoError(t, err)
require.Len(t, stats, 2)
for _, stat := range stats {
assert.Equal(t, stat.Language == "C", stat.IsPrimary, stat.Language)
}
}

View File

@@ -334,18 +334,32 @@ func GetTagNamesByRepoID(ctx context.Context, repoID int64) ([]string, error) {
return tags, sess.Find(&tags)
}
// GetLatestReleaseByRepoID returns the latest release for a repository
func GetLatestReleaseByRepoID(ctx context.Context, repoID int64) (*Release, error) {
cond := builder.NewCond().
And(builder.Eq{"repo_id": repoID}).
And(builder.Eq{"is_draft": false}).
And(builder.Eq{"is_prerelease": false}).
And(builder.Eq{"is_tag": false})
func (r *Release) CanBeLatest() bool {
return !r.IsDraft && !r.IsPrerelease && !r.IsTag
}
func latestReleaseCond(repoID int64) builder.Cond {
return FindReleasesOptions{RepoID: repoID, IsPreRelease: optional.Some(false)}.ToConds()
}
func GetLatestRelease(ctx context.Context, repo *Repository) (*Release, error) {
if repo.LatestReleaseID != 0 {
rel, has, err := db.Get[Release](ctx, latestReleaseCond(repo.ID).And(builder.Eq{"id": repo.LatestReleaseID}))
if err != nil {
return nil, err
} else if has {
return rel, nil
}
}
return GetLatestReleaseByRepoID(ctx, repo.ID)
}
// GetLatestReleaseByRepoID returns the newest published stable release of a repository by date
func GetLatestReleaseByRepoID(ctx context.Context, repoID int64) (*Release, error) {
rel := new(Release)
has, err := db.GetEngine(ctx).
Desc("created_unix", "id").
Where(cond).
Where(latestReleaseCond(repoID)).
Get(rel)
if err != nil {
return nil, err
@@ -356,6 +370,27 @@ func GetLatestReleaseByRepoID(ctx context.Context, repoID int64) (*Release, erro
return rel, nil
}
func SetLatestReleaseID(ctx context.Context, repo *Repository, releaseID int64) error {
repo.LatestReleaseID = releaseID
_, err := db.GetEngine(ctx).ID(repo.ID).Where(builder.Neq{"latest_release_id": releaseID}).
Cols("latest_release_id").NoAutoTime().Update(repo)
return err
}
func UnsetLatestReleaseID(ctx context.Context, repo *Repository, releaseID int64) error {
if releaseID == 0 {
return nil
}
if _, err := db.GetEngine(ctx).Where(builder.Eq{"id": repo.ID, "latest_release_id": releaseID}).
Cols("latest_release_id").NoAutoTime().Update(&Repository{LatestReleaseID: 0}); err != nil {
return err
}
if repo.LatestReleaseID == releaseID {
repo.LatestReleaseID = 0
}
return nil
}
type releaseMetaSearch struct {
IDs []int64
Releases []*Release
@@ -543,8 +578,3 @@ func FindTagsByCommitIDs(ctx context.Context, repoID int64, commitIDs ...string)
}
return res, nil
}
func DeleteRepoReleases(ctx context.Context, repoID int64) error {
_, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Delete(new(Release))
return err
}

View File

@@ -209,6 +209,7 @@ type Repository struct {
CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"`
Topics []string `xorm:"TEXT JSON"`
ObjectFormatName string `xorm:"VARCHAR(6) NOT NULL DEFAULT 'sha1'"`
LatestReleaseID int64 `xorm:"NOT NULL DEFAULT 0"` // 0 selects the latest release by date
TrustModel TrustModelType

View File

@@ -204,8 +204,6 @@ type SearchRepoOptions struct {
// True -> include just has milestones
// False -> include just has no milestone
HasMilestones optional.Option[bool]
// LowerNames represents valid lower names to restrict to
LowerNames []string
// When specified true, apply some filters over the conditions:
// - Don't show forks, when opts.Fork is OptionalBoolNone.
// - Do not display repositories that don't have a description, an icon and topics.
@@ -658,8 +656,13 @@ func userAllPublicRepoCond(cond builder.Cond, ownerVisibilityLimit []structs.Vis
// AccessibleRepositoryCondition takes a user a returns a condition for checking if a repository is accessible
func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) builder.Cond {
unitCond := builder.NewCond()
if unitType != unit.TypeInvalid {
unitCond = builder.Exists(builder.Select("id").From("repo_unit").Where(builder.Expr("repo_unit.repo_id = `repository`.id").
And(builder.Eq{"repo_unit.type": unitType}, builder.NotIn("repo_unit.type", unit.DisabledRepoUnitsGet()))))
}
if user_model.IsPublicOnlyDoer(user) && !user.IsRestricted {
return PublicRepoUnderPublicOwnerCond()
return PublicRepoUnderPublicOwnerCond().And(unitCond)
}
cond := builder.NewCond()
@@ -698,7 +701,7 @@ func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) bu
cond = userAllPublicRepoCond(cond, orgVisibilityLimit)
}
}
return cond.And(DoerTokenRepoCond(user))
return cond.And(DoerTokenRepoCond(user), unitCond)
}
// SearchRepositoryByName takes keyword and part of repository name to search,
@@ -824,10 +827,6 @@ func GetUserRepositories(ctx context.Context, opts SearchRepoOptions) (Repositor
cond = cond.And(builder.Eq{"is_private": false})
}
if len(opts.LowerNames) > 0 {
cond = cond.And(builder.In("lower_name", opts.LowerNames))
}
sess := db.GetEngine(ctx)
count, err := sess.Where(cond).Count(new(Repository))

View File

@@ -485,6 +485,19 @@ func TestSearchRepositoryByTopicName(t *testing.T) {
}
}
func TestFindUserCodeAccessibleRepoIDsRequiresCodeUnit(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
publicOnlyUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
publicOnlyUser.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialAccessToken, 1, "public-only,read:repository")
for _, user := range []*user_model.User{nil, publicOnlyUser} {
repoIDs, err := repo_model.FindUserCodeAccessibleRepoIDs(t.Context(), user)
require.NoError(t, err)
assert.Contains(t, repoIDs, int64(1))
assert.NotContains(t, repoIDs, int64(50))
}
}
func TestFindUserActionsAccessibleOwnerRepoIDs(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
// user2 is on org3's owner team, so it can access org3's private repo3 (which has the actions unit)
@@ -492,6 +505,7 @@ func TestFindUserActionsAccessibleOwnerRepoIDs(t *testing.T) {
// org3 is a public org owning repo3 (private) and repo32 (public), both with the actions unit
const orgID = 3
require.NoError(t, db.Insert(t.Context(), &repo_model.RepoUnit{RepoID: 32, Type: unit.TypeActions, Config: &repo_model.ActionsConfig{}}))
all, err := repo_model.SearchRepositoryIDsByCondition(t.Context(), repo_model.UserActionsAccessibleOwnerRepoCond(orgID, user))
require.NoError(t, err)

View File

@@ -56,12 +56,11 @@ func init() {
// UnitConfig describes common unit config
type UnitConfig struct{}
// FromDB fills up a UnitConfig from serialized format.
func (cfg *UnitConfig) FromDB(bs []byte) error {
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
db.UnmarshalFromDB(bs, cfg)
return nil
}
// ToDB exports a UnitConfig to a serialized format.
func (cfg *UnitConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
@@ -71,12 +70,11 @@ type ExternalWikiConfig struct {
ExternalWikiURL string
}
// FromDB fills up a ExternalWikiConfig from serialized format.
func (cfg *ExternalWikiConfig) FromDB(bs []byte) error {
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
db.UnmarshalFromDB(bs, cfg)
return nil
}
// ToDB exports a ExternalWikiConfig to a serialized format.
func (cfg *ExternalWikiConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
@@ -89,29 +87,48 @@ type ExternalTrackerConfig struct {
ExternalTrackerRegexpPattern string
}
// FromDB fills up a ExternalTrackerConfig from serialized format.
func (cfg *ExternalTrackerConfig) FromDB(bs []byte) error {
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
db.UnmarshalFromDB(bs, cfg)
return nil
}
// ToDB exports a ExternalTrackerConfig to a serialized format.
func (cfg *ExternalTrackerConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
const (
// CreationRestrictionReader allows any user with read access to the unit to create issues/pull requests (default, legacy behavior)
CreationRestrictionReader = "reader"
// CreationRestrictionMember restricts creation to effective collaborators (owner, team members, explicit collaborators) with at least read access
CreationRestrictionMember = "member"
// CreationRestrictionWriter restricts creation to users with at least write access
CreationRestrictionWriter = "writer"
)
// IssuesConfig describes issues config
type IssuesConfig struct {
EnableTimetracker bool
AllowOnlyContributorsToTrackTime bool
EnableDependencies bool
CreationRestriction string
}
func DefaultIssuesConfig() *IssuesConfig {
return &IssuesConfig{
EnableTimetracker: setting.Service.DefaultEnableTimetracking,
AllowOnlyContributorsToTrackTime: setting.Service.DefaultAllowOnlyContributorsToTrackTime,
EnableDependencies: setting.Service.DefaultEnableDependencies,
CreationRestriction: CreationRestrictionReader,
}
}
// FromDB fills up a IssuesConfig from serialized format.
func (cfg *IssuesConfig) FromDB(bs []byte) error {
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
*cfg = *DefaultIssuesConfig()
db.UnmarshalFromDB(bs, cfg)
cfg.CreationRestriction = util.IfZero(cfg.CreationRestriction, CreationRestrictionReader)
return nil
}
// ToDB exports a IssuesConfig to a serialized format.
func (cfg *IssuesConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
@@ -134,6 +151,7 @@ type PullRequestsConfig struct {
DefaultSquashMergeMessage SquashMergeMessage
DefaultAllowMaintainerEdit bool
DefaultTargetBranch string
CreationRestriction string
}
func DefaultPullRequestsConfig() *PullRequestsConfig {
@@ -147,6 +165,7 @@ func DefaultPullRequestsConfig() *PullRequestsConfig {
AllowRebaseUpdate: true,
DefaultUpdateStyle: UpdateStyleMerge,
DefaultAllowMaintainerEdit: true,
CreationRestriction: CreationRestrictionReader,
}
cfg.DefaultDeleteBranchAfterMerge = setting.Repository.PullRequest.DefaultDeleteBranchAfterMerge
cfg.DefaultMergeStyle = MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle)
@@ -155,16 +174,15 @@ func DefaultPullRequestsConfig() *PullRequestsConfig {
return cfg
}
// FromDB fills up a PullRequestsConfig from serialized format.
func (cfg *PullRequestsConfig) FromDB(bs []byte) error {
// set default values for existing PullRequestConfig in DB
*cfg = *DefaultPullRequestsConfig()
_ = json.UnmarshalHandleDoubleEncode(bs, &cfg) // don't let corrupted database value cause unnecessary 500 error
db.UnmarshalFromDB(bs, cfg)
cfg.DefaultUpdateStyle = util.IfZero(cfg.DefaultUpdateStyle, UpdateStyleMerge)
cfg.CreationRestriction = util.IfZero(cfg.CreationRestriction, CreationRestrictionReader)
return nil
}
// ToDB exports a PullRequestsConfig to a serialized format.
func (cfg *PullRequestsConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
@@ -228,34 +246,18 @@ type ProjectsConfig struct {
ProjectsMode ProjectsMode
}
// FromDB fills up a ProjectsConfig from serialized format.
func (cfg *ProjectsConfig) FromDB(bs []byte) error {
// TODO: remove GetProjectsMode, only use ProjectsMode
cfg.ProjectsMode = ProjectsModeAll
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
db.UnmarshalFromDB(bs, cfg)
cfg.ProjectsMode = util.IfZero(cfg.ProjectsMode, ProjectsModeAll)
return nil
}
// ToDB exports a ProjectsConfig to a serialized format.
func (cfg *ProjectsConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
func (cfg *ProjectsConfig) GetProjectsMode() ProjectsMode {
if cfg.ProjectsMode != "" {
return cfg.ProjectsMode
}
return ProjectsModeAll
}
func (cfg *ProjectsConfig) IsProjectsAllowed(m ProjectsMode) bool {
projectsMode := cfg.GetProjectsMode()
if m == ProjectsModeNone {
return true
}
return projectsMode == m || projectsMode == ProjectsModeAll
return m == ProjectsModeNone || cfg.ProjectsMode == m || cfg.ProjectsMode == ProjectsModeAll
}
// BeforeSet is invoked from XORM before setting the value of a field of this object.

View File

@@ -6,6 +6,7 @@ package repo
import (
"slices"
"gitea.dev/models/db"
"gitea.dev/models/perm"
"gitea.dev/models/unit"
"gitea.dev/modules/json"
@@ -173,14 +174,12 @@ func (cfg *ActionsConfig) ClampPermissions(perms ActionsTokenPermissions) Action
return ClampActionsTokenPermissions(perms, maxPerms)
}
// FromDB fills up a ActionsConfig from serialized format.
func (cfg *ActionsConfig) FromDB(bs []byte) error {
_ = json.UnmarshalHandleDoubleEncode(bs, &cfg)
db.UnmarshalFromDB(bs, cfg)
cfg.TokenPermissionMode, _ = util.EnumValue(cfg.TokenPermissionMode)
return nil
}
// ToDB exports a ActionsConfig to a serialized format.
func (cfg *ActionsConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}

View File

@@ -8,6 +8,7 @@ import (
"fmt"
"regexp"
"strings"
"sync"
"gitea.dev/models/db"
"gitea.dev/modules/container"
@@ -22,7 +23,9 @@ func init() {
db.RegisterModel(new(RepoTopic))
}
var topicPattern = regexp.MustCompile(`^[a-z0-9][-.a-z0-9]*$`)
var topicPattern = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`^[a-z0-9][-.a-z0-9]*$`)
})
// Topic represents a topic of repositories
type Topic struct {
@@ -55,7 +58,7 @@ func (err ErrTopicNotExist) Unwrap() error {
// ValidateTopic checks a topic by length and match pattern rules
func ValidateTopic(topic string) bool {
return len(topic) <= 35 && topicPattern.MatchString(topic)
return len(topic) <= 35 && topicPattern().MatchString(topic)
}
// SanitizeAndValidateTopics sanitizes and checks an array or topics

View File

@@ -18,7 +18,9 @@ var (
ErrBlockOrganization = util.NewInvalidArgumentErrorf("cannot block an organization")
ErrCanNotBlock = util.NewInvalidArgumentErrorf("cannot block the user")
ErrCanNotUnblock = util.NewInvalidArgumentErrorf("cannot unblock the user")
ErrBlockedUser = util.NewPermissionDeniedErrorf("user is blocked")
// ErrBlockedUser is for end users: the doer is blocked by the repo owner, or an assignee when creating an issue, etc
ErrBlockedUser = util.NewPermissionDeniedErrorf("you are blocked by the user")
)
type Blocking struct {

View File

@@ -43,7 +43,12 @@ func IsErrUserNotExist(err error) bool {
}
func (err ErrUserNotExist) Error() string {
ret := fmt.Sprintf("user does not exist [uid: %d, name: %s]", err.UID, err.Name)
ret := "user does not exist"
if err.UID != 0 {
ret += fmt.Sprintf(" [id: %d]", err.UID)
} else if err.Name != "" {
ret += fmt.Sprintf(" [name: %s]", err.Name)
}
if err.ExtraMsg != "" {
ret += ": " + err.ExtraMsg
}

View File

@@ -1,127 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
import (
"context"
"fmt"
"gitea.dev/models/db"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// UserOpenID is the list of all OpenID identities of a user.
// Since this is a middle table, name it OpenID is not suitable, so we ignore the lint here
type UserOpenID struct { //revive:disable-line:exported
ID int64 `xorm:"pk autoincr"`
UID int64 `xorm:"INDEX NOT NULL"`
URI string `xorm:"UNIQUE NOT NULL"`
Show bool `xorm:"DEFAULT false"`
}
func init() {
db.RegisterModel(new(UserOpenID))
}
// GetUserOpenIDs returns all openid addresses that belongs to given user.
func GetUserOpenIDs(ctx context.Context, uid int64) ([]*UserOpenID, error) {
openids := make([]*UserOpenID, 0, 5)
if err := db.GetEngine(ctx).
Where("uid=?", uid).
Asc("id").
Find(&openids); err != nil {
return nil, err
}
return openids, nil
}
// isOpenIDUsed returns true if the openid has been used.
func isOpenIDUsed(ctx context.Context, uri string) (bool, error) {
if len(uri) == 0 {
return true, nil
}
return db.Exist[UserOpenID](ctx, builder.Eq{"uri": uri})
}
// ErrOpenIDAlreadyUsed represents a "OpenIDAlreadyUsed" kind of error.
type ErrOpenIDAlreadyUsed struct {
OpenID string
}
// IsErrOpenIDAlreadyUsed checks if an error is a ErrOpenIDAlreadyUsed.
func IsErrOpenIDAlreadyUsed(err error) bool {
_, ok := err.(ErrOpenIDAlreadyUsed)
return ok
}
func (err ErrOpenIDAlreadyUsed) Error() string {
return fmt.Sprintf("OpenID already in use [oid: %s]", err.OpenID)
}
func (err ErrOpenIDAlreadyUsed) Unwrap() error {
return util.ErrAlreadyExist
}
// AddUserOpenID adds an pre-verified/normalized OpenID URI to given user.
// NOTE: make sure openid.URI is normalized already
func AddUserOpenID(ctx context.Context, openid *UserOpenID) error {
used, err := isOpenIDUsed(ctx, openid.URI)
if err != nil {
return err
} else if used {
return ErrOpenIDAlreadyUsed{openid.URI}
}
return db.Insert(ctx, openid)
}
// GetUserOpenIDByID returns the OpenID with the given ID owned by uid.
func GetUserOpenIDByID(ctx context.Context, id, uid int64) (*UserOpenID, error) {
oid, has, err := db.Get[UserOpenID](ctx, builder.Eq{"id": id, "uid": uid})
if err != nil {
return nil, err
} else if !has {
return nil, util.NewNotExistErrorf("OpenID is unknown")
}
return oid, nil
}
// DeleteUserOpenID deletes an openid address of given user.
func DeleteUserOpenID(ctx context.Context, openid *UserOpenID) (err error) {
var deleted int64
// ask to check UID
address := UserOpenID{
UID: openid.UID,
}
if openid.ID > 0 {
deleted, err = db.GetEngine(ctx).ID(openid.ID).Delete(&address)
} else {
deleted, err = db.GetEngine(ctx).
Where("openid=?", openid.URI).
Delete(&address)
}
if err != nil {
return err
} else if deleted != 1 {
return util.NewNotExistErrorf("OpenID is unknown")
}
return nil
}
// ToggleUserOpenIDVisibility toggles visibility of an openid address of given user.
func ToggleUserOpenIDVisibility(ctx context.Context, id int64, user *User) error {
affected, err := db.GetEngine(ctx).Exec("update `user_open_id` set `show` = not `show` where `id` = ? AND uid = ?", id, user.ID)
if err != nil {
return err
}
if n, _ := affected.RowsAffected(); n != 1 {
return util.NewNotExistErrorf("OpenID is unknown")
}
return nil
}

View File

@@ -1,70 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user_test
import (
"testing"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUserOpenIDs(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
t.Run("GetUserOpenIDs", func(t *testing.T) {
oids, err := user_model.GetUserOpenIDs(t.Context(), int64(1))
if assert.NoError(t, err) && assert.Len(t, oids, 2) {
assert.Equal(t, "https://user1.domain1.tld/", oids[0].URI)
assert.False(t, oids[0].Show)
assert.Equal(t, "http://user1.domain2.tld/", oids[1].URI)
assert.True(t, oids[1].Show)
}
oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2))
if assert.NoError(t, err) && assert.Len(t, oids, 1) {
assert.Equal(t, "https://domain1.tld/user2/", oids[0].URI)
assert.True(t, oids[0].Show)
}
})
t.Run("ToggleUserOpenIDVisibilityRequiresOwnership", func(t *testing.T) {
unauthorizedUser, err := user_model.GetUserByID(t.Context(), int64(2))
require.NoError(t, err)
err = user_model.ToggleUserOpenIDVisibility(t.Context(), int64(1), unauthorizedUser)
require.Error(t, err)
assert.ErrorIs(t, err, util.ErrNotExist)
})
t.Run("ToggleUserOpenIDVisibility", func(t *testing.T) {
user, err := user_model.GetUserByID(t.Context(), int64(2))
require.NoError(t, err)
oids, err := user_model.GetUserOpenIDs(t.Context(), int64(2))
require.NoError(t, err)
require.Len(t, oids, 1)
assert.True(t, oids[0].Show)
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID, user)
require.NoError(t, err)
oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2))
require.NoError(t, err)
require.Len(t, oids, 1)
assert.False(t, oids[0].Show)
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID, user)
require.NoError(t, err)
oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2))
require.NoError(t, err)
if assert.Len(t, oids, 1) {
assert.True(t, oids[0].Show)
}
})
}

View File

@@ -22,7 +22,6 @@ import (
"gitea.dev/models/auth"
"gitea.dev/models/db"
"gitea.dev/modules/auth/openid"
"gitea.dev/modules/auth/password/hash"
"gitea.dev/modules/base"
"gitea.dev/modules/container"
@@ -838,10 +837,7 @@ func createUser(ctx context.Context, u *User, meta *Meta, createdByAdmin bool, o
}
// trim user agent string to a reasonable length, if necessary
userAgent := strings.TrimSpace(meta.InitialUserAgent)
if len(userAgent) > 255 {
userAgent = userAgent[:255]
}
userAgent := util.TruncateRunes(strings.TrimSpace(meta.InitialUserAgent), 255)
if err = SetUserSetting(ctx, u.ID, SignupUserAgent, userAgent); err != nil {
return err
}
@@ -1441,32 +1437,6 @@ func GetIndividualUserByLoginSource(ctx context.Context, loginType auth.Type, lo
return user, has, err
}
// GetUserByOpenID returns the user object by given OpenID if exists.
func GetUserByOpenID(ctx context.Context, uri string) (*User, error) {
if len(uri) == 0 {
return nil, ErrUserNotExist{Name: uri}
}
uri, err := openid.Normalize(uri)
if err != nil {
return nil, err
}
log.Trace("Normalized OpenID URI: " + uri)
// Otherwise, check in openid table
oid := &UserOpenID{}
has, err := db.GetEngine(ctx).Where("uri=?", uri).Get(oid)
if err != nil {
return nil, err
}
if has {
return GetUserByID(ctx, oid.UID)
}
return nil, ErrUserNotExist{Name: uri}
}
// GetAdminUser returns the first administrator
func GetAdminUser(ctx context.Context) (*User, error) {
var admin User

View File

@@ -417,25 +417,6 @@ func TestNewUserRedirect(t *testing.T) {
}
}
func TestGetUserByOpenID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
_, err := user_model.GetUserByOpenID(t.Context(), "https://unknown")
if assert.Error(t, err) {
assert.True(t, user_model.IsErrUserNotExist(err))
}
user, err := user_model.GetUserByOpenID(t.Context(), "https://user1.domain1.tld")
if assert.NoError(t, err) {
assert.Equal(t, int64(1), user.ID)
}
user, err = user_model.GetUserByOpenID(t.Context(), "https://domain1.tld/user2/")
if assert.NoError(t, err) {
assert.Equal(t, int64(2), user.ID)
}
}
func TestFollowUser(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

View File

@@ -1,5 +1,5 @@
name: Step with leading new line
'on':
"on":
push:
jobs:
test:

View File

@@ -8,6 +8,7 @@ import (
"fmt"
"regexp"
"strings"
"sync"
"gitea.dev/actionslib/pkg/model"
@@ -75,7 +76,9 @@ func ParseWorkflowCallConfig(content []byte) (*model.WorkflowCall, error) {
const SecretsInherit = "inherit"
// callerSecretValueRegexp matches the `${{ secrets.NAME }}` form expected for each value in a caller's `secrets:` mapping.
var callerSecretValueRegexp = regexp.MustCompile(`^\s*\$\{\{\s*secrets\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}\s*$`)
var callerSecretValueRegexp = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`^\s*\$\{\{\s*secrets\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}\s*$`)
})
// ParseCallerSecrets decodes a caller's "secrets:" YAML node into one of two forms:
// - inherit == true: the caller wrote `secrets: inherit`; mapping is nil
@@ -101,7 +104,7 @@ func ParseCallerSecrets(node yaml.Node) (inherit bool, mapping map[string]string
if err := v.Decode(&sv); err != nil {
return false, nil, fmt.Errorf("decode secret %q: %w", k.Value, err)
}
matches := callerSecretValueRegexp.FindStringSubmatch(sv)
matches := callerSecretValueRegexp().FindStringSubmatch(sv)
if len(matches) != 2 {
return false, nil, fmt.Errorf("caller secret %q value must be of the form ${{ secrets.NAME }}", k.Value)
}

View File

@@ -16,6 +16,7 @@ import (
"gitea.dev/models/dbfs"
"gitea.dev/modules/log"
"gitea.dev/modules/storage"
"gitea.dev/modules/util"
"gitea.dev/modules/zstd"
"google.golang.org/protobuf/types/known/timestamppb"
@@ -213,10 +214,7 @@ func OpenLogs(ctx context.Context, inStorage bool, filename string) (io.ReadSeek
func FormatLog(timestamp time.Time, content string) string {
// Content shouldn't contain new line, it will break log indexes, other control chars are safe.
content = strings.ReplaceAll(content, "\n", `\n`)
if len(content) > MaxLineSize {
content = content[:MaxLineSize]
}
return fmt.Sprintf("%s %s", timestamp.UTC().Format(timeFormat), content)
return fmt.Sprintf("%s %s", timestamp.UTC().Format(timeFormat), util.TruncateStringBytes(content, MaxLineSize))
}
func ParseLog(in string) (time.Time, string, error) {

View File

@@ -165,11 +165,12 @@ func ScopedWorkflowStatusContextName(prefix, displayName, jobName, event string)
return strings.TrimSpace(fmt.Sprintf("%s: %s", prefix, WorkflowStatusContextName(displayName, jobName, event)))
}
// ShouldEventCreateCommitStatus reports whether a run triggered by the given workflow `on:` event posts a commit status,
// IsPullRequestCheckEvent reports whether a run triggered by the given workflow `on:` event posts a commit status that pull requests count,
// so its context can serve as a required status check.
// TODO: this allowlist duplicates the truth in services/actions.getCommitStatusEventNameAndCommitID, which decides the actual event string and whether a status is posted.
// The two are kept in sync by hand and can drift; unify them into a single source so adding a status-producing event in one place automatically updates the other.
func ShouldEventCreateCommitStatus(event string) bool {
// TODO: this allowlist duplicates the truth in services/actions.getCommitStatusEventNameAndCommitID, which decides the actual event string and whether a status is posted,
// and in services/pull.GetLatestPullCommitStatuses, which decides whether pull requests count it.
// They are kept in sync by hand and can drift, unify them into a single source so adding a status-producing event in one place automatically updates the others.
func IsPullRequestCheckEvent(event string) bool {
switch event {
case "push", "pull_request", "pull_request_target", "pull_request_review", "pull_request_review_comment", "release":
return true
@@ -510,27 +511,11 @@ func matchIssuesEvent(issuePayload *api.IssuePayload, evt *jobparser.Event) bool
// See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issues
// Actions with the same name:
// opened, edited, closed, reopened, assigned, unassigned, milestoned, demilestoned
// Actions need to be converted:
// label_updated -> labeled (when adding) or unlabeled (when removing)
// label_cleared -> unlabeled
// Actions need to be converted: see toActivityTypes
// Unsupported activity types:
// deleted, transferred, pinned, unpinned, locked, unlocked
actions := []string{}
switch issuePayload.Action {
case api.HookIssueLabelUpdated:
if len(issuePayload.Changes.AddedLabels) > 0 {
actions = append(actions, "labeled")
}
if len(issuePayload.Changes.RemovedLabels) > 0 {
actions = append(actions, "unlabeled")
}
case api.HookIssueLabelCleared:
actions = append(actions, "unlabeled")
default:
actions = append(actions, string(issuePayload.Action))
}
actions := toActivityTypes(issuePayload.Action, issuePayload.Changes)
for _, val := range vals {
if matchActivityType(val, actions...) {
matchTimes++
@@ -557,25 +542,14 @@ func matchPullRequestEvent(ctx context.Context, gitRepo *git.Repository, commit
// See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
// Actions with the same name:
// opened, edited, closed, reopened, assigned, unassigned, review_requested, review_request_removed, milestoned, demilestoned
// Actions need to be converted:
// synchronized -> synchronize
// label_updated -> labeled
// label_cleared -> unlabeled
// Actions need to be converted: see toActivityTypes
// Unsupported activity types:
// converted_to_draft, ready_for_review, locked, unlocked, auto_merge_enabled, auto_merge_disabled, enqueued, dequeued
action := prPayload.Action
switch action {
case api.HookIssueSynchronized:
action = "synchronize"
case api.HookIssueLabelUpdated:
action = "labeled"
case api.HookIssueLabelCleared:
action = "unlabeled"
}
log.Trace("matching pull_request %s with %v", action, vals)
actions := toActivityTypes(prPayload.Action, prPayload.Changes)
log.Trace("matching pull_request %v with %v", actions, vals)
for _, val := range vals {
if matchActivityType(val, string(action)) {
if matchActivityType(val, actions...) {
activityTypeMatched = true
matchTimes++
break
@@ -587,7 +561,7 @@ func matchPullRequestEvent(ctx context.Context, gitRepo *git.Repository, commit
headCommit = commit
err error
)
if evt.Name == GithubEventPullRequestTarget && (len(acts["paths"]) > 0 || len(acts["paths-ignore"]) > 0) {
if (len(acts["paths"]) > 0 || len(acts["paths-ignore"]) > 0) && commit.ID.String() != prPayload.PullRequest.Head.Sha {
headCommit, err = gitRepo.GetCommit(ctx, prPayload.PullRequest.Head.Sha)
if err != nil {
log.Error("GetCommit [ref: %s]: %v", prPayload.PullRequest.Head.Sha, err)
@@ -894,6 +868,27 @@ func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event
return matchTimes == len(evt.Acts())
}
// toActivityTypes returns several types because one label update can both add and remove labels
func toActivityTypes(action api.HookIssueAction, changes *api.ChangesPayload) []string {
switch action {
case api.HookIssueSynchronized:
return []string{"synchronize"}
case api.HookIssueLabelUpdated:
var types []string
if len(changes.AddedLabels) > 0 {
types = append(types, "labeled")
}
if len(changes.RemovedLabels) > 0 {
types = append(types, "unlabeled")
}
return types
case api.HookIssueLabelCleared:
return []string{"unlabeled"}
default:
return []string{string(action)}
}
}
// matchActivityType treats an invalid user-provided pattern as no match instead of panicking
func matchActivityType(pattern string, actions ...string) bool {
g, err := glob.Compile(pattern, '/')

View File

@@ -185,10 +185,17 @@ func TestDetectMatched(t *testing.T) {
{
desc: "HookEventPullRequest(pull_request) `label_updated` action matches GithubEventPullRequest(pull_request) with `label` activity type",
triggedEvent: webhook_module.HookEventPullRequest,
payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated},
payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated, Changes: &api.ChangesPayload{AddedLabels: []*api.Label{{ID: 1, Name: "label"}}}},
yamlOn: "on:\n pull_request:\n types: [labeled]",
expected: detectMatched,
},
{
desc: "HookEventPullRequestLabel(pull_request_label) label removal doesn't match GithubEventPullRequest(pull_request) with `labeled` activity type",
triggedEvent: webhook_module.HookEventPullRequestLabel,
payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated, Changes: &api.ChangesPayload{RemovedLabels: []*api.Label{{ID: 1}}}},
yamlOn: "on:\n pull_request:\n types: [labeled]",
expected: detectNotApplicable,
},
{
desc: "HookEventPullRequestReviewComment(pull_request_review_comment) matches GithubEventPullRequestReviewComment(pull_request_review_comment)",
triggedEvent: webhook_module.HookEventPullRequestReviewComment,

View File

@@ -6,6 +6,7 @@ package assetfs
import (
"bytes"
"compress/gzip"
"errors"
"io"
"io/fs"
"os"
@@ -301,12 +302,12 @@ var GetExecutableModTime = sync.OnceValue(func() (modTime time.Time) {
return st.ModTime()
})
func GenerateEmbedBindata(fsRootPath, outputFile string) error {
func GenerateEmbedBindata(fsRootPath, outputFile string) (err error) {
output, err := os.OpenFile(outputFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm)
if err != nil {
return err
}
defer output.Close()
defer func() { err = errors.Join(err, output.Close()) }()
meta := &EmbeddedMeta{}
meta.Root = &embeddedFileInfo{}

View File

@@ -1,57 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package openid
import (
"sync"
"time"
"github.com/yohcop/openid-go"
)
type timedDiscoveredInfo struct {
info openid.DiscoveredInfo
time time.Time
}
type timedDiscoveryCache struct {
cache map[string]timedDiscoveredInfo
ttl time.Duration
mutex *sync.Mutex
}
func newTimedDiscoveryCache(ttl time.Duration) *timedDiscoveryCache {
return &timedDiscoveryCache{cache: map[string]timedDiscoveredInfo{}, ttl: ttl, mutex: &sync.Mutex{}}
}
func (s *timedDiscoveryCache) Put(id string, info openid.DiscoveredInfo) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.cache[id] = timedDiscoveredInfo{info: info, time: time.Now()}
}
// Delete timed-out cache entries
func (s *timedDiscoveryCache) cleanTimedOut() {
now := time.Now()
for k, e := range s.cache {
diff := now.Sub(e.time)
if diff > s.ttl {
delete(s.cache, k)
}
}
}
func (s *timedDiscoveryCache) Get(id string) openid.DiscoveredInfo {
s.mutex.Lock()
defer s.mutex.Unlock()
// Delete old cached while we are at it.
s.cleanTimedOut()
if info, has := s.cache[id]; has {
return info.info
}
return nil
}

View File

@@ -1,49 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package openid
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type testDiscoveredInfo struct{}
func (s *testDiscoveredInfo) ClaimedID() string {
return "claimedID"
}
func (s *testDiscoveredInfo) OpEndpoint() string {
return "opEndpoint"
}
func (s *testDiscoveredInfo) OpLocalID() string {
return "opLocalID"
}
func TestTimedDiscoveryCache(t *testing.T) {
ttl := 50 * time.Millisecond
dc := newTimedDiscoveryCache(ttl)
// Put some initial values
dc.Put("foo", &testDiscoveredInfo{}) // openid.opEndpoint: "a", openid.opLocalID: "b", openid.claimedID: "c"})
// Make sure we can retrieve them
di := dc.Get("foo")
require.NotNil(t, di)
assert.Equal(t, "opEndpoint", di.OpEndpoint())
assert.Equal(t, "opLocalID", di.OpLocalID())
assert.Equal(t, "claimedID", di.ClaimedID())
// Attempt to get a non-existent value
assert.Nil(t, dc.Get("bar"))
// Sleep for a while and try to retrieve again
time.Sleep(ttl * 3 / 2)
assert.Nil(t, dc.Get("foo"))
}

View File

@@ -1,49 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package openid
import (
"net/http"
"sync"
"time"
"gitea.dev/modules/egress"
"github.com/yohcop/openid-go"
)
// For the demo, we use in-memory infinite storage nonce and discovery
// cache. In your app, do not use this as it will eat up memory and
// never
// free it. Use your own implementation, on a better database system.
// If you have multiple servers for example, you may need to share at
// least
// the nonceStore between them.
var (
nonceStore = openid.NewSimpleNonceStore()
discoveryCache = newTimedDiscoveryCache(24 * time.Hour)
// openIDInstance keeps user-supplied OpenID identifiers within [security] ALLOWED_HOST_LIST
openIDInstance = sync.OnceValue(func() *openid.OpenID {
return openid.NewOpenID(&http.Client{
Timeout: 30 * time.Second,
Transport: egress.NewSecurityPolicy("openid").NewHTTPTransport(),
})
})
)
// Verify handles response from OpenID provider
func Verify(fullURL string) (id string, err error) {
return openIDInstance().Verify(fullURL, discoveryCache, nonceStore)
}
// Normalize normalizes an OpenID URI
func Normalize(url string) (id string, err error) {
return openid.Normalize(url)
}
// RedirectURL redirects browser
func RedirectURL(id, callbackURL, realm string) (string, error) {
return openIDInstance().RedirectURL(id, callbackURL, realm)
}

View File

@@ -1,29 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package openid
import (
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestOpenIDDiscoveryBlocksInternalHost(t *testing.T) {
var reached atomic.Bool
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reached.Store(true)
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
// RedirectURL performs server-side discovery of the identifier URL; a loopback URL
// must be refused at dial time instead of reaching the internal server
_, err := RedirectURL(srv.URL, "http://example.com/callback", "http://example.com/")
require.Error(t, err)
assert.False(t, reached.Load(), "OpenID discovery must not reach an internal/loopback host")
}

View File

@@ -29,6 +29,9 @@ func TestAmbiguousCharacters(t *testing.T) {
found := i < len(ambiguous.Confusable) && ambiguous.Confusable[i] == confusable
assert.True(t, found, "%c is not in %d", confusable, i)
}
for r := rune(0x20); r < 0x7f; r++ {
assert.False(t, unicode.Is(ambiguous.RangeTable, r), "%c must not be ambiguous, isStringAsciiSafe skips it", r)
}
}
var confusableTo rune

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