Compare commits

...

92 Commits

Author SHA1 Message Date
silverwind
11363e2f0c chore(renovate): bundle major updates, use chore commit type (#38470)
1. combine PRs for major/non-major, less PRs is less work with the
fixups.
2. always set `chore`, more often than not, this is more correct then
`fix`.

---------

Signed-off-by: silverwind <me@silverwind.io>
2026-07-15 22:10:43 +02:00
wxiaoguang
82edc3da01 refactor: decouple git.Repository(ctx) from git.Commit & git.Tree (#38464)
1. Storing "ctx" in a long-living object is wrong
2. Make the commit & tree cacheable (for the future performance
optimization)
3. Also fix some bad designs like `// FIXME: bad design, this field can
be nil if the commit is from "last commit cache"`

ref:
* #33893
2026-07-15 17:30:01 +00:00
Shudhanshu Singh
ed678b9d45 fix(actions): make job list item fully clickable (#38462)
Clicking the empty space to the right of a job in the Actions sidebar
didn't switch jobs: the interactive `<a>`/`<button>` used `display:
contents` and so generated no clickable box.

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

Drop the wrapper `div` and `display: contents`, moving the `.item` and
layout styles directly onto the `<a>`/`<button>` so the whole row is
clickable. Reusable-caller `<button>` rows also get `width: 100%` and
`line-height: inherit` to match the `<a>` rows.

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-07-15 17:06:34 +00:00
wxiaoguang
ce683b34c6 fix: mail template for push event (#38467)
fix #38465
2026-07-15 18:57:44 +02:00
Giteabot
fa50ad2aa9 chore(deps): update dependency djlint to v1.40.4 (#38428) 2026-07-15 13:32:27 +00:00
Giteabot
c86eb7081b fix(deps): update npm dependencies (#38431) 2026-07-15 07:14:50 +00:00
Giteabot
506075c480 fix(deps): update go dependencies (#38429) 2026-07-15 06:47:08 +00:00
Jeremy Stover
9fa2bff5fd fix(admin): exit dev test queue producer loop when context is cancelled (#38451)
Co-authored-by: Kadajett <jeremy@semfora.ai>
2026-07-15 03:25:50 +00:00
xkm
880ddb5724 fix(actions): prevent bulk actions from affecting all runners (#38453)
Fix the bug in the site-admin runner bulk actions introduced by #37869:
the runner IDs are empty then all runners will be deleted.

Fixes #38449

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-15 02:36:03 +00:00
GiteaBot
7c629e1ba7 [skip ci] Updated translations via Crowdin 2026-07-15 00:45:33 +00:00
Harsh Satyajit Thakur
b6904c9730 fix: make "test push webhook" always work (#38425)
* fix #38309
* fix #26238
* fix #37886

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-14 18:24:18 +00:00
Shudhanshu Singh
9c08df8bc8 fix(org): align follow button and wrap description (#38448)
### Description
Fixes the organization page header layout issue where:
1. Long organization descriptions did not wrap and instead stretched the
header container out of bounds.
2. The "Follow" button floated dynamically adjacent to the description's
right edge depending on the description length, instead of remaining at
a fixed position aligned with the right edge of the page container.

Fixes https://github.com/go-gitea/gitea/issues/38445
### Cause
The flex container `<div class="flex-relaxed-list">` lacked `tw-flex-1`
(to grow to fill the container) and `tw-min-w-0` (to allow it to shrink
below its content's size). Consequently, the flex minimum width
defaulted to `min-content`, stretching the container for long unwrapped
descriptions.

### Solution
Added `tw-flex-1 tw-min-w-0` to the `<div class="flex-relaxed-list">`
container in `templates/org/header.tmpl`. This restricts the width,
enables proper text wrapping, and fixes the "Follow" button to the right
edge of the content container.

### Screenshots
Before
<img width="1300" height="615" alt="image"
src="https://github.com/user-attachments/assets/11e6ab99-b847-45ff-8bdb-8622bfeee8aa"
/>
After
<img width="1300" height="615" alt="image"
src="https://github.com/user-attachments/assets/84ac0633-b192-4be5-8226-13bfad3ab2ec"
/>

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-14 17:54:55 +00:00
silverwind
0e4d93537a fix(actions): populate github.event for scheduled runs (#38446)
Scheduled runs stored a `null` event payload, so
`github.event.repository`/`sender`/`organization` were empty in
scheduled workflows. Every other event carries them, and so does GitHub
Actions. `CreateScheduleTask` now synthesizes them.
2026-07-14 18:30:16 +02:00
Giteabot
da5a004fc4 chore(deps): update npm dependencies (major) (#38432) 2026-07-14 13:04:08 +02:00
Giteabot
ed9b02985a fix(deps): update module github.com/google/go-github/v88 to v89 (#38433) 2026-07-14 08:48:58 +00:00
Giteabot
f12a0a9183 chore(deps): update action dependencies (#38430) 2026-07-14 10:24:40 +02:00
wxiaoguang
d15cfa363a chore: don't auto refresh the merge box when user has interacted with it (#38435)
Otherwise, the user just isn't able to use "auto merge" form
2026-07-13 08:38:41 +02:00
bircni
f69e15afe7 fix: various security fixes (#38406)
Addresses a batch of privately reported security issues, grouped by
area:

- **SSRF** - migration PR-patch/asset fetches, OAuth2 avatar & OpenID
discovery, pull-mirror URL re-validation, and the outbound proxy path.
- **Access-token scope** - prevent scope escalation on token creation;
keep public-only tokens confined (feeds, packages, Actions listings,
star/watch lists, limited/private owners).
- **Access control / disclosure** - go-get default-branch leak, webhook
authorization-header leak, watch clearing on private transitions,
label/attachment scoping.
- **Denial of service** - input bounds for npm dist-tags, Debian control
files, Arch file lists, and SSH keys.

### 📌 Attention for site admins

Not breaking - existing configs keep working - but two changes are worth
a look:

- **New SSRF protection** Outbound requests (migrations, OAuth2 avatars,
OpenID discovery, pull mirrors, proxy path) are now validated against
the allow/block host lists. If your instance legitimately reaches
internal hosts, you may need to add them to
`[security].ALLOWED_HOST_LIST` (and the relevant `ALLOW_LOCALNETWORKS`
settings).
- **Deprecation** `[webhook].ALLOWED_HOST_LIST` is deprecated and will
be removed in a future release. Use `[security].ALLOWED_HOST_LIST`
instead; the old key still works for now.

---------

Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
Co-authored-by: techknowlogick <techknowlogick@gitea.io>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
2026-07-12 17:14:09 +00:00
TowyTowy
d2bd1589fe fix(util): reject invalid characters between time-estimate units (#38416)
### What / why

`TimeEstimateParse` (used by the issue time-estimate form) only checked
that the first token starts at the beginning of the string and the last
token ends at its end, but never checked the gaps between consecutive
tokens. Non-whitespace garbage embedded between two valid units was
silently dropped and the string accepted with a wrong value instead of
being reported as invalid.

Examples that were wrongly accepted before this change:

- `1h 2x 3m` → 3780s (parsed as 1h3m)
- `1h_2m`    → 3720s
- `1h,1m`    → 3660s

All three now return an "invalid time string" error, while valid inputs
such as `1h 1m 1s` and `1h1m1s` keep working.

### How

Reject any non-whitespace content between two matched units.

---------

Signed-off-by: TowyTowy <towy@airreps.link>
Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-12 13:38:34 +00:00
Shudhanshu Singh
b998c3c1aa feat(actions): implement adaptive auto-refresh for workflow runs list (#38329)
### Description
This PR implements an optimized, adaptive client-side auto-refresh
mechanism for the Gitea Actions workflow runs list page. It allows users
to see workflow progress updates dynamically without having to reload
the page.

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

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-12 12:14:37 +00:00
Lovepreet Singh
65c5a5ff7b fix(turnstile): route CAPTCHA verification through the configured proxy (#38412)
Fixes #38217

## Problem

Turnstile CAPTCHA verification uses `http.DefaultClient`, so the request
to `challenges.cloudflare.com` bypasses Gitea's configured HTTP proxy —
unlike other outbound HTTP clients such as the update checker
(`modules/updatechecker/update_checker.go`) and migrations. In
deployments where egress is only permitted through the configured proxy,
verification fails.

## Fix

Build the client with `proxy.Proxy()` as the transport proxy, mirroring
the update checker:

```go
func httpClient() *http.Client {
	return &http.Client{
		Transport: &http.Transport{
			Proxy: proxy.Proxy(),
		},
	}
}
```

The client is built per call (rather than a package-level var) because
`proxy.Proxy()` reads `setting.Proxy` when invoked; building it at
request time ensures it reflects the loaded settings. When no proxy is
configured, behavior is unchanged (`proxy.Proxy()` returns a no-op /
`http.ProxyFromEnvironment`).

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-12 11:33:14 +00:00
Harsh Satyajit Thakur
aba0eb1749 fix: represent a deleted assignee team as a Ghost team (#38413)
Fixes #35472.

`Comment.LoadAssigneeUserAndTeam` already has a Ghost user fallback
for a deleted assignee user, but the parallel branch for a deleted
assignee team just swallowed the not-found error and left
`AssigneeTeam` as `nil`. This is inconsistent (the reporter's example
shows `assignee` becoming a Ghost user while `assignee_team` becomes
`null`), and it's also a latent nil pointer bug: other code that
assumes `AssigneeTeam` is set once this function returns without
error will panic.

Added `organization.NewGhostTeam()` / `Team.IsGhost()`, mirroring the
existing `user_model.NewGhostUser()` / `User.IsGhost()` pattern, and
used it in the same fallback branch.

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-12 12:54:59 +02:00
GiteaBot
678b5aba30 [skip ci] Updated translations via Crowdin 2026-07-12 00:54:22 +00:00
wxiaoguang
bd5f881c51 fix: refresh pull request merge box when the commit status is pending (#38410) 2026-07-11 19:36:10 +02:00
Yarden Shoham
d3d57dd9b4 chore: remove Yarden Shoham from maintainers (#38407) 2026-07-11 15:29:41 +02:00
wxiaoguang
1bbd127a1a fix: actions task state concurrent update (#38405)
fix #38333
2026-07-11 15:03:42 +02:00
bircni
f803f8e269 fix(actions): keep workflow run trailing on one row with long branch names (#38382)
The flex-list refactor (#37505) raised the shared `.item-trailing`
selector's specificity, so its `flex-wrap: wrap` started overriding the
run list's intended `flex-wrap: nowrap` — a long branch name pushed the
trailing content past its fixed 280px width and wrapped the kebab menu
onto its own line.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-10 23:43:32 +02:00
bircni
8401fe7c54 fix(pull): re-evaluate review official flag on target branch change (#38319)
The `official` flag of a pull request review is computed against the
target branch's protection rules at submit time and stored on the review
record. `ChangeTargetBranch` updated the base branch but never
re-evaluated it, so an `official` approval obtained against an
unprotected branch could be retargeted onto a protected branch and
satisfy its required approvals, bypassing the branch protection.

This re-evaluates the `official` flag of the latest approve/reject
reviews against the new base branch whenever the target branch changes.
2026-07-10 19:18:47 +00:00
Shudhanshu Singh
c12e92c21d fix(web): use locale-aware date formatting for contribution calendar tooltips (#38398)
The contribution calendar manually constructed localized date strings
instead of using the browser's locale-aware date formatting. Replace
this with `toLocaleDateString()` to correctly format dates for all
locales and calendar systems, including non-Gregorian calendars.

Fixes https://github.com/go-gitea/gitea/issues/38375
2026-07-10 18:38:09 +00:00
bircni
aab3242f7b fix(security): harden access checks and migration validation (#38324)
Harden access checks for issue dependencies, team repository membership,
notifications, stars, tracked times and repository migrations.
2026-07-10 19:30:43 +02:00
bircni
f452c369ac fix: enforce public-only token scope and harden push options / locale parsing (#38323)
- **Locale DoS:** the `Locale` middleware passed the raw
`Accept-Language` header to `ParseAcceptLanguage`, whose guard only
counts `-` while the scanner aliases `_` to `-` — a large `_`-separated
header on an unauthenticated request burned CPU. The header is now
length-bounded before parsing.
- **Public-only token scope:** `GET /teams/{id}/repos`,
`.../repos/{org}/{repo}`, `/teams/{id}/activities/feeds`, and
`/users/{username}/orgs/{org}/permissions` still returned private
repo/activity/permission data to a public-only token. They now filter
via `TokenCanAccessRepo` / `ApplyPublicOnly` and reject non-public org
permissions.
- **Push-option visibility:** `repo.private` / `repo.template` push
options were applied to any existing repo, letting an owner/admin
silently flip visibility bypassing audit, webhooks, and notifications.
They are now honored only on push-to-create.
2026-07-10 16:39:01 +00:00
wxiaoguang
c5c991b1a4 fix: co-author detection (#38392)
Committer can also be co-author, it should only not be included in the
co-author list if it is not in the "Co-author-by" list.

* Author & Co-author: they changed the code (attribution)
* Committer: they submitted the commit but didn't change the code (e.g.:
maintainer signed a commit)

Fix #38384
2026-07-10 12:52:00 +02:00
bircni
362539b78e fix(api): stop leaking private repo metadata after access revocation (#38321)
The `/user/starred` and `/user/subscriptions` endpoints returned private
repositories a user had starred/watched even after their access to those
repositories was revoked, still exposing the repository name,
description and visibility (including later metadata changes).

Private repositories in the starred/watched queries are now gated on the
actor's current access via `AccessibleRepositoryCondition`, so users who
no longer have access no longer receive the metadata. Public
repositories and public-only tokens are unaffected.
2026-07-09 22:21:44 +00:00
bircni
66a3723cbb fix(lfs): require proof of possession for cross-repo objects (#38322)
The LFS batch and upload handlers linked an object that already existed
in the content store but was not linked to the current repo whenever the
token's user could access it in another repo. Deploy-key tokens carry
the repo owner's identity, so a single-repo write deploy key could link
and then download objects from any repo the owner can see.

This drops the cross-repo access check: the batch handler now makes the
client upload (hash-verified) any object not yet linked to the repo, and
the upload handler skips proof of possession only when the object is
already linked to the current repo.
2026-07-09 21:43:08 +00:00
bircni
27e33b7ba1 fix: incorrect co-author detection on commit page (#38386)
The commit page built its "co-authored by" list from
`AllParticipantIdentities()[1:]`, which only drops the author and leaves
the committer in the list even though the committer is already shown
separately as "committed by". This caused the committer to be wrongly
rendered as a co-author (issue #38384): a commit authored by
`silverwind`, committed by `bircni`, with a `Co-authored-by: silverwind`
trailer displayed "co-authored by bircni" instead of the actual trailer
identity. This adds a `Commit.CoAuthorIdentities()` method that excludes
both the author and the committer, uses it on the commit page, and
covers the fix with a regression test.

Closes #38384
2026-07-09 22:31:14 +02:00
Zettat123
761470c01d enhance(actions): only create filtered-out workflow commit status for required contexts (#38371)
Follow #38237

#38237 posts "skipped" commit statuses for every workflow that is not
triggered due to a filter (e.g. `paths` or `branches`) mismatch.
However, for non-required workflows, creating "skipped" commit statuses
for them would generate a lot of noise.

To address this issue, this PR adds a check before creating commit
status:

- For the context that matches any required status check patterns, a
"skipped" commit status will be created. The `Required` label can inform
users that this status check is required, but has been skipped because
of a filter mismatch.
- For a non-required context, nothing will be created.

NOTE: Reducing noise is a best-effort approach and isn't entirely
accurate. When creating commit statuses, it is impossible to predict
which branch protection rule will take effect. Therefore, we have to
compare the commit status context against the required patterns from all
rules. If any rule matches, the context is considered "required".
2026-07-09 15:34:56 +00:00
silverwind
c0bbd82cd4 fix(ui): restore commits table column widths (#38379)
https://github.com/go-gitea/gitea/pull/37594 widened the author column
from `three wide` to `four wide`, leaving a large empty gap around the
SHA column in the common single-author case. The old author cell was
capped at 180px, so the wider column only adds whitespace: avatar-stack
names ellipsize at 240px each, and wider multi-author rows still expand
naturally in the auto-layout table. Restore the pre-existing
`three`/`eight` widths.

Co-authored-by: bircni <bircni@icloud.com>
2026-07-09 14:26:59 +00:00
silverwind
7fd34ff033 test(e2e): fix race in pdf file render test (#38380)
`data-render-name` is set before the plugin's async render runs, so
measuring the container height right after the attribute appears can
observe the pre-render 48px height when the `pdfobject` chunk loads
slowly (flaked in CI). Poll for the height instead, like the asciicast
test in the same file does.
2026-07-09 13:25:01 +00:00
wxiaoguang
1e682a26eb refactor: introduce ActivePageTimer to help to do partial page refresh (#38372)
Before, the logic is already there for "pull merge box".

After, the logic is extracted into a general class ActivePageTimer and
will help more pages (including #38329)
2026-07-09 14:39:03 +02:00
GiteaBot
3b3a06e06f [skip ci] Updated translations via Crowdin 2026-07-09 00:56:40 +00:00
Milwad Khosravi
545ed92354 chore(typo): fix grammar in comments, API docs and error messages (#38370) 2026-07-08 23:52:06 +02:00
wxiaoguang
49ef93940a fix: golang html template url escaping (#38363)
fix #38362
2026-07-08 00:16:32 +00:00
bircni
308a6f12ae perf(actions): debounce runner heartbeat writes and throttle task picks (#38281)
3 reductions in the DB load generated by many runners polling
`FetchTask`:

**1. Debounce runner heartbeat writes**
Every poll wrote `last_online`, and every `UpdateTask`/`UpdateLog` wrote
`last_active` — while a runner streams logs that is many writes per
second per runner. These are now persisted only when stale enough to
actually affect the active/offline status (`ShouldPersistLastOnline` /
`ShouldPersistLastActive`), using the existing columns.

**2. Throttle concurrent task picks**
A new in-process semaphore (`MAX_CONCURRENT_TASK_PICKS`) bounds how many
runners run the task-assignment transaction at once, so a fleet polling
together cannot stampede the query. Throttled polls retry on their next
poll without advancing the runner's tasks version.

**3. Paginate the task-pick query**
`CreateTaskForRunner` previously loaded every waiting job in the
runner's scope into memory on each poll (no `LIMIT`). Now it pages
through the waiting backlog oldest-first with `LIMIT`, claiming the
first label-matching job.

---------

Co-authored-by: Zettat123 <zettat123@gmail.com>
2026-07-07 19:16:20 +00:00
bircni
97078b96cf fix(mirror): disable HTTP redirects on pull mirror sync (#38320)
Pull mirror sync ran `git fetch` / `remote update` / `remote prune`
without disabling HTTP redirects. A mirror remote that later starts
redirecting to an otherwise-blocked or internal address could be used as
an SSRF/exfiltration vector on scheduled syncs, bypassing the
allow/block validation applied at migration time.

This sets `http.followRedirects=false` on all three remote-contacting
commands in the pull mirror path, matching the existing guard already
present on the clone path.
2026-07-07 17:35:21 +00:00
Giteabot
6507f1fd94 chore(deps): update dependency djlint to v1.40.1 (#38354) 2026-07-07 16:31:17 +00:00
Giteabot
0964899799 fix(deps): update npm dependencies (#38352) 2026-07-07 18:17:10 +02:00
Giteabot
550efdcdfd chore(deps): update action dependencies (#38353) 2026-07-07 14:41:01 +02:00
Giteabot
b96bd22372 fix(deps): update go dependencies (#38346) 2026-07-07 10:16:00 +00:00
wxiaoguang
a74f618ade fix: minio init check (#38355)
Fix the buggy behavior introduced by "S3: log human readable error on connection failure (#26856)"
2026-07-07 07:32:25 +00:00
Copilot
2b89e2ac97 fix(pulls): add branch-name option for DEFAULT_TITLE_SOURCE (#38356)
Adds a new `branch-name` value for the `[repository.pull-request]`
`DEFAULT_TITLE_SOURCE` setting that always uses the normalized branch
name as the PR title, regardless of commit count.

Fix #38317

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-07 07:08:05 +00:00
wxiaoguang
26bff7f47e fix: org project view assignee list (#38357)
fix #38129
2026-07-07 14:40:12 +08:00
Shudhanshu Singh
582217a0da feat(webhook): add reviewer name to MS Teams review request notifications (#38289)
Include the requested reviewer's username (along with their full name in
parentheses, if available) and render the `Repository` and `Pull
request` fields as clickable links in Microsoft Teams webhook
notifications.

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

## Screenshots

<img width="1246" height="651" alt="image"
src="https://github.com/user-attachments/assets/7299ce10-c6d4-4c89-a05a-a258d72c00e5"
/>

---------

Signed-off-by: Shudhanshu Singh <sudhanshuwriterblc@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-06 18:30:58 +00:00
Giteabot
a46e331637 chore(deps): update action dependencies (#38340) 2026-07-06 18:04:10 +00:00
Lunny Xiao
580cc26d63 chore: Upgrade xorm to 1.4.1 (#38224)
Fix #22275

Changelog: https://gitea.com/xorm/xorm/compare/v1.3.11..v1.4.1
2026-07-06 17:07:58 +00:00
Giteabot
e3d83bcf9c chore(deps): update tool dependencies (#38344)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
|
[github.com/editorconfig-checker/editorconfig-checker/v3](https://redirect.github.com/editorconfig-checker/editorconfig-checker)
| `v3.7.0` → `v3.8.0` |
![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2feditorconfig-checker%2feditorconfig-checker%2fv3/v3.8.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2feditorconfig-checker%2feditorconfig-checker%2fv3/v3.7.0/v3.8.0?slim=true)
|
| golang.org/x/vuln | `v1.4.0` → `v1.5.0` |
![age](https://developer.mend.io/api/mc/badges/age/go/golang.org%2fx%2fvuln/v1.5.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/go/golang.org%2fx%2fvuln/v1.4.0/v1.5.0?slim=true)
|
2026-07-06 18:40:40 +02:00
Giteabot
44f927eacf fix(deps): update npm dependencies (#38342) 2026-07-06 14:13:28 +00:00
Giteabot
35413d5b65 chore(deps): update dependency djlint to v1.40.0 (#38341) 2026-07-06 15:56:31 +02:00
Zettat123
e797a27d4e fix(actions): release claimed task if context is cancelled during FetchTask (#38343)
When a runner's `FetchTask` request context is cancelled after the job
is claimed but before the task reaches the runner (e.g. request
timeout), the job was left referencing a running task no runner ever
executes, so it stayed unpickable.

Fix: Check the context after assembling the task and release the task so
the job returns to waiting status for another runner.
2026-07-06 06:38:44 +02:00
GiteaBot
4b15260277 [skip ci] Updated translations via Crowdin 2026-07-06 01:02:23 +00:00
GiteaBot
18fdc77130 [skip ci] Updated translations via Crowdin 2026-07-05 01:02:27 +00:00
Zettat123
2c2691b969 test: compare key file contents instead of FileInfo in TestInitKeys (#38330)
`TestInitKeys` verified whether a host key file was regenerated by
comparing `os.FileInfo` before and after a `InitDefaultHostKeys` call.
On systems where the OS clock / filesystem timestamp granularity is
coarse, both writes land in the same tick and get an identical mtime.
The resulting `FileInfo` is then byte-for-byte equal even though the key
was actually regenerated, so `assert.NotEqual` fails.

Since a regenerated key always produces different random bytes,
comparing the content can reliably detect regeneration.
2026-07-04 22:30:12 +02:00
wxiaoguang
08d4abbb46 docs: describe duties of mergers prior to merging a PR (#38308)
Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Signed-off-by: delvh <dev.lh@web.de>
Co-authored-by: delvh <dev.lh@web.de>
2026-07-04 21:00:22 +02:00
Lunny Xiao
e4ef995f2a fix(release): validate web attachment renames against allowed types (#38314)
This fixes the web release edit flow so renamed release attachments are
validated against `[repository.release] ALLOWED_TYPES`.

Previously, the API attachment edit endpoint already enforced release
attachment type restrictions, but the web release edit form passed
`attachment-edit-*` values into `release_service.UpdateRelease`, which
updated attachment names directly without validating the new filename
against `setting.Repository.Release.AllowedTypes`.

As a result, a user with repository write access could rename an
existing release attachment to a disallowed extension through the web
UI.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-04 15:02:17 +02:00
GiteaBot
7fdfb8d642 [skip ci] Updated translations via Crowdin 2026-07-04 00:56:26 +00:00
bircni
d4c4142123 fix(actions): make runner list pagination order deterministic (#38313)
The runner-listing API endpoints (`admin`, `org`, `user`, `repo`, and
`shared`) return runners in a non-deterministic order across pages. When
paging through runners, some runners from page 1 could reappear on page
2 (and others get skipped entirely).

The cause is in `FindRunnerOptions.ToOrders()`. Most sort modes order by
a **non-unique** column only:

- default / `online` / `offline` → `last_online`
- `alphabetically` / `reversealphabetically` → `name`

When multiple runners tie on the sort key (e.g. every offline runner
shares `last_online = 0`, or two runners have the same name), the
database is free to return the tied rows in any order between separate
queries. Combined with `LIMIT`/`OFFSET` pagination, this means the same
runner can land on more than one page.

## Fix

Append the unique primary key `id` as a stable tiebreaker to each
non-unique sort order:

The `newest`/`oldest` modes already sort by the unique `id`, so they are
left unchanged.
2026-07-03 20:35:47 +00:00
bircni
f7fd510224 fix(release): gate draft release attachments on web download endpoints (#38318)
Draft-release access control was enforced only on the API release
endpoints (`/api/v1/repos/{owner}/{repo}/releases/...`) but not on the
UUID-based web attachment endpoints (`/attachments/{uuid}`,
`/{owner}/{repo}/attachments/{uuid}`,
`/{owner}/{repo}/releases/attachments/{uuid}`).

Anyone who obtained an attachment UUID — including unauthenticated
callers — could download files belonging to a hidden draft release,
since `ServeAttachment` only checked repo-level read permission and
never the release's draft state.
2026-07-03 20:07:37 +02:00
silverwind
38a5824753 ci(snap): build snaps natively instead of on launchpad (#38312)
The nightly snap build fails with `snapcraft remote-build`'s
`BadRequest()`: it force-pushes Gitea's full history to a fresh
Launchpad repo each run and creates the recipe before Launchpad has
indexed the `main` ref. The race is unwinnable at Gitea's repo size, and
Canonical does not support `remote-build` in CI.

Build locally on native amd64 + arm64 runners with
`snapcore/action-build` + `snapcore/action-publish` instead — no
Launchpad, no race. Drops the `LAUNCHPAD_CREDENTIALS` secret (publishing
keeps `SNAPCRAFT_STORE_CREDENTIALS`); the `snap/` recipe is unchanged,
so the same snaps ship to `latest/edge`.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-07-03 07:22:27 +00:00
silverwind
e8d2c493bb chore: update eslint-plugin-unicorn to v70 (#38310)
Bumps to https://github.com/sindresorhus/eslint-plugin-unicorn/releases/tag/v70.0.0,
enable all new rules, no violations.

---------

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.8) <noreply@anthropic.com>
2026-07-02 14:09:37 -07:00
Shudhanshu Singh
b09920a537 feat(webhook): support Telegram Bot API 10.1 Rich Messages (#38298)
Upgrades Gitea's Telegram webhook integration to support Telegram Bot
API 10.1 (June 2026 release). This enables Gitea webhooks to take
advantage of rich formatted messages (tables, nested blocks, collapsible
details, etc.) by routing them through the /sendRichMessage endpoint
with the new rich_message payload structure.

Old `/sendMessage` webhook URLs are written to `/sendRichMessage`
at runtime to prevent the need for database migrations

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

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-02 15:14:48 +00:00
silverwind
c6184ed184 chore(snap): drop armhf build (#38311)
Launchpad no longer builds `core24` snaps for `armhf`. Since `snapcraft
remote-build` submits a single request for all platforms, the `armhf`
rejection fails with `snapcraft internal error: BadRequest()` and takes
the `amd64` and `arm64` builds down with it, so nothing gets published.

Dropping 32-bit ARM from `snap/snapcraft.yaml` and the workflow's
`--build-for` restores nightly snap publishing for the remaining
architectures.
2026-07-02 13:55:45 +02:00
Shudhanshu Singh
8909958055 fix(actions): prevent chevron overlap with log text when timestamps are enabled (#38227)
### Description
This PR resolves a UI alignment bug in the Gitea Actions log viewer
where the expand/collapse disclosure chevron overlaps with the log text
(specifically the timestamp) when timestamps are enabled.

### Cause
When log timestamps are enabled, the timestamp element
(`.log-time-stamp`) is rendered as the first element next to the line
number. Because it only had a default `10px` left margin, it positioned
itself exactly where the group's expand/collapse chevron is located,
causing them to overlap.

### Solution
Updated the CSS styles in `web_src/js/components/ActionRunJobView.vue`
to dynamically apply the `21px` margin to whichever element is the first
visible element after the line number:
- If the timestamp is visible, it gets the `21px` margin to clear the
chevron, and the subsequent log message gets a `10px` margin.
- If the timestamp is hidden, the log message receives the `21px`
margin.

### Before / After
**Before:**
<img width="853" height="348" alt="actions_log_before"
src="https://github.com/user-attachments/assets/d09a752e-18cb-4fe3-b749-4979cbe45240"
/>


**After:**
<img width="862" height="511" alt="actions_log_after"
src="https://github.com/user-attachments/assets/63063f05-8cd6-4986-a993-ed12f28625c8"
/>

Fixes #38222.

---------

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: Giteabot <teabot@gitea.io>
Co-authored-by: silverwind <me@silverwind.io>
2026-07-02 05:20:25 +00:00
silverwind
638e4bce09 chore: upgrade go-swagger to v0.35.0 and enforce zero swagger warnings (#38299)
Updates `go-swagger` to v0.35.0 and makes swagger generation and
validation warning-free, with the build now failing on any warning
(`go-swagger` itself exits `0` on warnings).

- Generation passes `--enable-allof-compounding` (keeps `$ref` fields
bare, no spec change) and `--skip-enum-desc` (drops the enum description
that duplicates `x-go-enum-desc` and was the only source of `allOf`
noise in the OpenAPI 3.0 output).
- Fixed warnings at the source: dropped `swagger:strfmt` where it
conflicts with `required: true` (`required` kept, `time.Time` still maps
to `date-time`), fixed a malformed `units_map` example, moved the
`parameterBodies` injection hack to `swagger:parameters`, and removed
unused responses.

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

---------

Co-authored-by: bircni <bircni@icloud.com>
2026-07-01 21:07:34 +00:00
puni9869
a031454586 fix: Improve since/until when counting commits for X-Total-Count (#38243)
Follow up for https://github.com/go-gitea/gitea/pull/38204.

---------

Signed-off-by: puni9869 <80308335+puni9869@users.noreply.github.com>
2026-07-01 20:43:46 +00:00
silverwind
c52a07dcfe chore: remove eslint-plugin-array-func (#38294)
The rules from `eslint-plugin-array-func` are redundant with
`eslint-plugin-unicorn`:

- `from-map` → `unicorn/prefer-array-from-map`
- `no-unnecessary-this-arg` → `unicorn/no-array-method-this-argument`
- `prefer-flat` / `prefer-flat-map` → `unicorn/prefer-array-flat` /
`unicorn/prefer-array-flat-map` (already disabled here)

The two remaining rules (`avoid-reverse`, `prefer-array-from`) are niche
and not worth carrying an extra dependency for.

Co-authored-by: bircni <bircni@icloud.com>
2026-07-01 20:16:42 +00:00
bircni
b6ef881a9f docs: Welcome Zettat to TOC (#38303) 2026-07-01 20:07:44 +00:00
Kausthubh J Rao
6240d8bf89 fix(workflows): branch protection status checks fail when workflow uses on: paths filter (#38237) 2026-07-01 21:47:47 +02:00
silverwind
9cb2719fab chore: update node.js to v26 (#38285)
- bump ci, flake and `@types/node` to node 26
- regenerate flake.lock which is needed for that package
- refactor workflow to use shared composite action
2026-07-01 16:28:36 +02:00
silverwind
e8654c7e06 refactor: replace vue-bar-graph dependency with inlined SVG chart (#38292)
Inlines the small SVG bar graph into `RepoActivityTopAuthors.vue` (its
only consumer) and drops the `vue-bar-graph` npm dependency.

- Bars render at static height (dropped the grow animation).
- Theme-aware axis color instead of a hardcoded `#555555`.
- Removed the dangling `role="img"`/`aria-labelledby` on the `<svg>`.
- Reserve the chart height so the page does not shift when the component
mounts.

<img width="416" height="110" alt="Screenshot 2026-07-01 at 11 15 25"
src="https://github.com/user-attachments/assets/b2db4d0c-20f1-4345-9951-32a908abfaba"
/>
<img width="419" height="110" alt="Screenshot 2026-07-01 at 11 15 35"
src="https://github.com/user-attachments/assets/853305a5-575f-4a26-ba3b-12fc51081324"
/>

fyi @lafriks

---------

Signed-off-by: silverwind <me@silverwind.io>
2026-07-01 11:17:23 +00:00
Zettat123
67a6bd7fc0 feat(auth): add disable-2fa command (#38275)
This PR adds the `gitea admin user disable-2fa` command to disable 2FA
for a user

When the only admin in the instance loses their 2FA credentials, this
command can be used to disable 2FA, allowing them to log in and reset
it.

---------

Co-authored-by: Giteabot <teabot@gitea.io>
2026-07-01 12:33:16 +02:00
Aidan Fahey
77e221ffaf fix(oauth2): persist linkAccountData during auto-link 2FA flow (#38274)
Fixes HTTP 500 when OIDC auto account linking (`ACCOUNT_LINKING=auto`)
requires local 2FA. `oauth2LinkAccount` set `linkAccount` in the session
before redirecting to 2FA but did not persist `linkAccountData`, so
`TwoFactorPost` failed with `not in LinkAccount session`. The manual
linking flow already stored both, this aligns auto-link with that
behavior.

Closes #38171

---------

Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-01 10:03:38 +00:00
bircni
458c11bd68 fix(actions): allow Actions bot to push to protected branches (#38284)
Fixes #38278

## Problem

When branch protection matches the branch an Actions workflow pushes to,
the runner's `git push` is rejected — even though the workflow token has
`contents: write` and the same push performed with a PAT (write access)
succeeds. Disabling protection or changing the pattern so it no longer
matches makes the push work.

## Root cause

In `preReceiveBranch` (`routers/private/hook_pre_receive.go`), the "can
the doer push to this protected branch" check resolves the pusher with
`user_model.GetUserByID(ctx, ctx.opts.UserID)`. For an Actions push the
user ID is `-2` (the virtual `ActionsUserID`), which has no database
row, so the lookup fails. Even past that, `CanUserPush` →
`HasAccessUnit`/whitelist membership cannot evaluate a virtual user and
returns `false`. As a result the Actions bot was rejected on every
matching protected branch, despite the earlier `assertCanWriteRef`
already confirming the token's code-write via
`GetActionsUserRepoPermission`.

This was inconsistent: a PAT with identical write access passed the
exact same check.

## Fix

Evaluate the Actions bot against its already-computed token permission
instead of a user lookup, mirroring the existing
`IsUserMergeWhitelisted` pattern:

- Add `CanActionsUserPush` / `CanActionsUserForcePush` on
`ProtectedBranch`, which take the precomputed `access_model.Permission`.
- Allow the push when push is enabled, **no** push whitelist is
enforced, and the token has code-write.
- Keep the bot blocked when a whitelist is enforced — it cannot be added
to one, so it must use a pull request. This preserves the whitelist as a
real security boundary.

Force-push, signed-commit and protected-file-path checks are untouched.

---------

Signed-off-by: bircni <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-01 09:19:47 +00:00
GiteaBot
3d2bbd25ec [skip ci] Updated translations via Crowdin 2026-07-01 01:19:35 +00:00
Avinash Thakur
7745720292 feat: extend <video> tag allowed attributes (#38279)
autoplay is useless nowadays without "muted" as browsers won't autoplay
unmuted videos.
Similarly, other attributes are also commonly used and harmless to keep.

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

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

---------

Signed-off-by: Avinash Thakur <19588421+80avin@users.noreply.github.com>
2026-06-30 20:31:13 +00:00
bircni
d46d0540d0 fix(actions): include all aggregable run statuses in status filter (#38280)
The **Status** filter dropdown on the repository Actions run list does
not let you filter for **Blocked** runs (nor **Cancelled** or
**Skipped**). These statuses are missing from the dropdown even though a
run can legitimately end up in any of them.

A run's status is computed by `aggregateJobStatus`, which can return
`Blocked`, `Cancelled` and `Skipped`. Because the filter dropdown only
offered Success, Failure, Waiting, Running and Cancelling, runs in those
other states existed but were impossible to filter for.
2026-06-30 19:59:30 +00:00
techknowlogick
e449018730 non-shallow clone for snapcraft
Signed-off-by: techknowlogick <techknowlogick@gitea.com>
2026-06-30 18:35:29 +02:00
Vinod-OAI
e1cdb71845 fix(archiver): use serializable repo-archive queue payload (#38273)
After upgrading from 1.25.x to 1.26.x, `repo-archive` workers can fail
to unmarshal queued items:

```
Failed to unmarshal item from queue "repo-archive":
json: unable to unmarshal into Go convert.Conversion within "/Repo/Units/0/Config":
cannot derive concrete type for nil interface with finite type set
```

`ArchiveRequest` started embedding `*repo_model.Repository` in 1.26,
which does not round-trip through the JSON queue.

This change stores a minimal `archiveQueueItem` (`RepoID`, `Type`,
`CommitID`, `Paths`) in `repo-archive` and loads the repository in the
worker. `UnmarshalJSON` accepts legacy payloads that used `RepoID` or
embedded `Repo.id`.

Fixes #38272

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

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

---------

Co-authored-by: bircni <bircni@icloud.com>
2026-06-30 14:42:05 +00:00
silverwind
a64131e22d chore: update eslint plugins and config (#38264)
1. Bump all eslint dependencies, enable some of the new unicorn rules
2. Remove `eslint-plugin-de-morgan`, it sometimes causes readability
issues
3. Disable some of the unicorn rules that are known to produce
false-positives
4. Remove obsolete type cast
5. Fix one violation of
https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-dom-node-replace-children.md

---------

Signed-off-by: silverwind <me@silverwind.io>
2026-06-30 13:53:29 +00:00
GiteaBot
0f0a38c1b9 [skip ci] Updated translations via Crowdin 2026-06-30 01:13:52 +00:00
silverwind
535f791166 ci: regenerate codemirror languages on renovate npm updates (#38267)
Adds `make generate-codemirror-languages` to the npm group's
`postUpgradeTasks` in `renovate.json5`, so renovate regenerates
`assets/codemirror-languages.json` whenever `@codemirror/language-data`
(or any npm dep) updates — mirroring the existing `make svg` handling.

Also reformats the `fileFilters` arrays multi-line and regenerates the
asset to pick up current upstream linguist languages.
2026-06-29 22:59:08 +00:00
Lunny Xiao
b34a09be38 build: fix snapcraft release (#38260)
Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: silverwind <me@silverwind.io>
2026-06-29 14:35:26 -07:00
Giteabot
6f2e328c85 chore(deps): update dependency js-yaml to v5 (#38262)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [js-yaml](https://redirect.github.com/nodeca/js-yaml) | [`4.2.0` →
`5.1.0`](https://renovatebot.com/diffs/npm/js-yaml/4.2.0/5.1.0) |
![age](https://developer.mend.io/api/mc/badges/age/npm/js-yaml/5.1.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/js-yaml/4.2.0/5.1.0?slim=true)
|

---

### Release Notes

<details>
<summary>nodeca/js-yaml (js-yaml)</summary>

###
[`v5.1.0`](https://redirect.github.com/nodeca/js-yaml/blob/HEAD/CHANGELOG.md#510---2026-06-23)

[Compare
Source](https://redirect.github.com/nodeca/js-yaml/compare/5.0.0...5.1.0)

##### Added

- Collection tags can finalize an incrementally populated carrier into a
  different result value.

##### Changed

- \[breaking] `quoteStyle` now selects the preferred quote style; use
the
  restored `forceQuotes` option to force quoting non-key strings.

###
[`v5.0.0`](https://redirect.github.com/nodeca/js-yaml/blob/HEAD/CHANGELOG.md#500---2026-06-20)

[Compare
Source](https://redirect.github.com/nodeca/js-yaml/compare/4.3.0...5.0.0)

##### Added

- Added named exports for schemas, tags, parser events and AST
utilities.
- Reworked `JSON_SCHEMA` and `CORE_SCHEMA` with spec-compliant scalar
resolution
  rules, and added `YAML11_SCHEMA`.
- Added `realMapTag` for lossless mappings with non-string and complex
keys.
Object-based mappings now reject complex keys instead of stringifying
them.
- Added `dump()` `transform` option for changing the generated AST
before
  rendering.
- Added `dump()` options `seqInlineFirst`, `flowBracketPadding`,
`flowSkipCommaSpace`, `flowSkipColonSpace`, `quoteFlowKeys`,
`quoteStyle` and
  `tagBeforeAnchor`.
- Added formal data layers (events and AST) for modular data pipelines.
  - Added low-level parser (to events), presenter and visitor APIs.
- Added the [YAML Test
Suite](https://redirect.github.com/yaml/yaml-test-suite) to the
  test set.

##### Changed

- See the [migration guide](docs/migrate_v4_to_v5.md) for upgrade notes.
- Rewritten in TypeScript and reorganized the public API around flat
named
  exports.
- Reduced the set of exported schemas:
  - YAML 1.2 schemas: `CORE_SCHEMA` (loader default), `JSON_SCHEMA`,
    `FAILSAFE_SCHEMA`.
- `YAML11_SCHEMA`, a combination of all YAML 1.1 tags (YAML 1.1 does not
    specify a schema, only "types").
- `load`/`dump` default behaviour is now specified exactly via schemas:
  - `load` uses `CORE_SCHEMA`, without `!!merge` by default.
- `dump` uses `YAML11_SCHEMA` + `CORE_SCHEMA` for the quoting check, to
    guarantee backward compatibility by default.
- `!!set` is now loaded as a JavaScript `Set`.
- Replaced the `Type` API with a tags API. Similar, but more precise and
  simpler. See examples for details. Tags can be defined via
`defineScalarTag()`, `defineSequenceTag()` and `defineMappingTag()`, or
as a
  spread + override of an existing tag.
- Renamed `Schema.extend()` to `Schema.withTags()`.
- Expanded YAML 1.2 conformance and improved handling of directives,
document
  markers, block keys, multiline scalars, tag syntax and other things.
- `load()` now throws on empty input instead of returning `undefined`.
- Moved browser builds to the `js-yaml/browser` export.
- Deprecated the `loadAll` signature with an iterator (still works, but
is a
  candidate for removal).

##### Removed

- Removed deprecated `safeLoad()`, `safeLoadAll()` and `safeDump()`
exports.
- Removed `DEFAULT_SCHEMA` and the nested `types` export.
- Removed loader options `onWarning`, `legacy` and `listener`.
- Removed dumper options `styles`, `replacer`, `noCompatMode`,
`condenseFlow`,
`quotingType` and `forceQuotes`. Renamed `noArrayIndent` to
`seqNoIndent`.
Formatting and representation are now configured through presenter
options,
  schemas and tag definitions. See migration guide on how to replace.
- Removed support for importing internal files from `lib/`.

###
[`v4.3.0`](https://redirect.github.com/nodeca/js-yaml/blob/HEAD/CHANGELOG.md#430-3150---2026-06-27)

[Compare
Source](https://redirect.github.com/nodeca/js-yaml/compare/4.2.0...4.3.0)

##### Security

- Backported `maxTotalMergeKeys` option.

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Only on Monday (`* * * * 1`)
- Automerge
  - At any time (no schedule defined)

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

♻ **Rebasing**: Whenever PR is behind base branch, 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](https://redirect.github.com/renovatebot/renovate).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNDEuNSIsInVwZGF0ZWRJblZlciI6IjQzLjE0MS41IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-06-29 17:22:21 +00:00
Giteabot
55983320ed chore(deps): update actions/cache action to v6 (#38261)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/cache](https://redirect.github.com/actions/cache) | action |
major | `v5.0.5` → `v6.1.0` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/37531) for more information.

---

### Release Notes

<details>
<summary>actions/cache (actions/cache)</summary>

###
[`v6.1.0`](https://redirect.github.com/actions/cache/releases/tag/v6.1.0)

[Compare
Source](https://redirect.github.com/actions/cache/compare/v6.0.0...v6.1.0)

##### What's Changed

- Bump
[@&#8203;actions/cache](https://redirect.github.com/actions/cache) to
v6.1.0 - handle read-only cache access by
[@&#8203;jasongin](https://redirect.github.com/jasongin) in
[#&#8203;1768](https://redirect.github.com/actions/cache/pull/1768)

**Full Changelog**:
<https://github.com/actions/cache/compare/v6...v6.1.0>

###
[`v6`](https://redirect.github.com/actions/cache/compare/v6.0.0...v6.0.0)

[Compare
Source](https://redirect.github.com/actions/cache/compare/v6.0.0...v6.0.0)

###
[`v6.0.0`](https://redirect.github.com/actions/cache/releases/tag/v6.0.0)

[Compare
Source](https://redirect.github.com/actions/cache/compare/v5.1.0...v6.0.0)

##### What's Changed

- Update packages, migrate to ESM by
[@&#8203;Samirat](https://redirect.github.com/Samirat) in
[#&#8203;1760](https://redirect.github.com/actions/cache/pull/1760)

**Full Changelog**:
<https://github.com/actions/cache/compare/v5...v6.0.0>

###
[`v5.1.0`](https://redirect.github.com/actions/cache/releases/tag/v5.1.0)

[Compare
Source](https://redirect.github.com/actions/cache/compare/v5.0.5...v5.1.0)

##### What's Changed

- Bump
[@&#8203;actions/cache](https://redirect.github.com/actions/cache) to
v5.1.0 - handle read-only cache access by
[@&#8203;jasongin](https://redirect.github.com/jasongin) in
[#&#8203;1775](https://redirect.github.com/actions/cache/pull/1775)

**Full Changelog**:
<https://github.com/actions/cache/compare/v5...v5.1.0>

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Only on Monday (`* * * * 1`)
- Automerge
  - At any time (no schedule defined)

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

♻ **Rebasing**: Whenever PR is behind base branch, 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](https://redirect.github.com/renovatebot/renovate).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNDEuNSIsInVwZGF0ZWRJblZlciI6IjQzLjE0MS41IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2026-06-29 17:00:15 +00:00
414 changed files with 7747 additions and 3805 deletions

View File

@@ -9,10 +9,10 @@ inputs:
runs: runs:
using: composite using: composite
steps: steps:
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Build regular image - name: Build regular image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with: with:
context: . context: .
platforms: ${{ inputs.platform }} platforms: ${{ inputs.platform }}
@@ -20,7 +20,7 @@ runs:
file: Dockerfile file: Dockerfile
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
- name: Build rootless image - name: Build rootless image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with: with:
context: . context: .
platforms: ${{ inputs.platform }} platforms: ${{ inputs.platform }}

View File

@@ -16,34 +16,34 @@ runs:
using: composite using: composite
steps: steps:
- if: ${{ github.workflow == 'cache-seeder' }} - if: ${{ github.workflow == 'cache-seeder' }}
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with: with:
path: ~/go/pkg/mod path: ~/go/pkg/mod
key: gomod-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }} key: gomod-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
- if: ${{ github.workflow != 'cache-seeder' }} - if: ${{ github.workflow != 'cache-seeder' }}
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with: with:
path: ~/go/pkg/mod path: ~/go/pkg/mod
key: gomod-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }} key: gomod-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
restore-keys: gomod-${{ runner.os }}-${{ runner.arch }} restore-keys: gomod-${{ runner.os }}-${{ runner.arch }}
- if: ${{ github.workflow == 'cache-seeder' && inputs.lint-cache != 'true' }} - if: ${{ github.workflow == 'cache-seeder' && inputs.lint-cache != 'true' }}
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with: with:
path: ~/.cache/go-build path: ~/.cache/go-build
key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }} key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
- if: ${{ github.workflow != 'cache-seeder' || inputs.lint-cache == 'true' }} - if: ${{ github.workflow != 'cache-seeder' || inputs.lint-cache == 'true' }}
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with: with:
path: ~/.cache/go-build path: ~/.cache/go-build
key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }} key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
restore-keys: gobuild-${{ runner.os }}-${{ runner.arch }} restore-keys: gobuild-${{ runner.os }}-${{ runner.arch }}
- if: ${{ inputs.lint-cache == 'true' && github.workflow == 'cache-seeder' }} - if: ${{ inputs.lint-cache == 'true' && github.workflow == 'cache-seeder' }}
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with: with:
path: ~/.cache/golangci-lint path: ~/.cache/golangci-lint
key: golint-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum', '.golangci.yml') }} key: golint-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum', '.golangci.yml') }}
- if: ${{ inputs.lint-cache == 'true' && github.workflow != 'cache-seeder' }} - if: ${{ inputs.lint-cache == 'true' && github.workflow != 'cache-seeder' }}
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with: with:
path: ~/.cache/golangci-lint path: ~/.cache/golangci-lint
key: golint-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum', '.golangci.yml') }} key: golint-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum', '.golangci.yml') }}

View File

@@ -13,10 +13,10 @@ runs:
- if: ${{ inputs.cache == 'true' }} - if: ${{ inputs.cache == 'true' }}
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: 24 node-version: 26
cache: pnpm cache: pnpm
cache-dependency-path: pnpm-lock.yaml cache-dependency-path: pnpm-lock.yaml
- if: ${{ inputs.cache != 'true' }} - if: ${{ inputs.cache != 'true' }}
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: 24 node-version: 26

View File

@@ -21,12 +21,12 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: renovatebot/github-action@6d859fc95779be83a0335ca704879b47e5d79641 # v46.1.16 - uses: renovatebot/github-action@b50d2ba2bd928235abdcc14d06dfafc217f1c565 # v46.1.18
with: with:
renovate-version: ${{ env.RENOVATE_VERSION }} renovate-version: ${{ env.RENOVATE_VERSION }}
configurationFile: renovate.json5 configurationFile: renovate.json5
token: ${{ secrets.RENOVATE_TOKEN }} token: ${{ secrets.RENOVATE_TOKEN }}
env: env:
RENOVATE_BINARY_SOURCE: install # auto-install go/node toolchains needed by post-upgrade tasks. RENOVATE_BINARY_SOURCE: install # auto-install go/node toolchains needed by post-upgrade tasks.
RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^make (tidy|svg)$"]' RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^make (tidy|svg|generate-codemirror-languages)$"]'
RENOVATE_REPOSITORIES: '["go-gitea/gitea"]' RENOVATE_REPOSITORIES: '["go-gitea/gitea"]'

View File

@@ -50,7 +50,7 @@ jobs:
shell: ${{ steps.changes.outputs.shell }} shell: ${{ steps.changes.outputs.shell }}
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
id: changes id: changes
with: with:
filters: | filters: |

View File

@@ -42,7 +42,7 @@ jobs:
- run: make lint-spell - run: make lint-spell
- if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true' || needs.files-changed.outputs.actions == 'true' - if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true' || needs.files-changed.outputs.actions == 'true'
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with: with:
python-version: 3.14 python-version: 3.14
- if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true' - if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true'

View File

@@ -131,7 +131,7 @@ jobs:
ports: ports:
- "7700:7700" - "7700:7700"
redis: redis:
image: redis:latest@sha256:c904002d182255b6db3cbe3a1e8ce6c187d15390c39500b59fc07181aabff7bf image: redis:latest@sha256:5d2c689b4b55fc3fab4b0cc8aaa950f85b508c76c1e0f35a90d8f411d55a8b2b
options: >- # wait until redis has started options: >- # wait until redis has started
--health-cmd "redis-cli ping" --health-cmd "redis-cli ping"
--health-interval 5s --health-interval 5s

View File

@@ -17,7 +17,7 @@ jobs:
contents: read contents: read
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0 - uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0
with: with:
sync-labels: true sync-labels: true
@@ -35,7 +35,7 @@ jobs:
ref: ${{ github.event.pull_request.base.sha }} ref: ${{ github.event.pull_request.base.sha }}
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: 24 node-version: 26
# Labels are only synced after the title lints, so an invalid title never reaches the label diff. # Labels are only synced after the title lints, so an invalid title never reaches the label diff.
- run: node ./tools/ci-tools.ts lint-pr-title - run: node ./tools/ci-tools.ts lint-pr-title
env: env:

View File

@@ -6,40 +6,30 @@ on:
- main - main
workflow_dispatch: workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: permissions:
contents: read contents: read
jobs: jobs:
build-and-publish: build-and-publish:
runs-on: ubuntu-latest strategy:
fail-fast: false
env: matrix:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} runner: [ubuntu-24.04, ubuntu-24.04-arm]
runs-on: ${{ matrix.runner }}
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
- name: Install snapcraft fetch-depth: 0
run: sudo snap install snapcraft --classic - uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1.3.0
id: build
- name: Authenticate snapcraft - uses: snapcore/action-publish@214b86e5ca036ead1668c79afb81e550e6c54d40 # v1.2.0
shell: bash env:
run: snapcraft login --with <(printf '%s' "$SNAPCRAFT_STORE_CREDENTIALS") SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
with:
- name: Remote build snap: ${{ steps.build.outputs.snap }}
run: | release: latest/edge
snapcraft remote-build \
--launchpad-accept-public-upload \
--build-for=amd64,arm64,armhf
- name: List built snaps
run: find . -maxdepth 1 -type f -name '*.snap' -print
- name: Upload and release snapcraft nightly build
run: |
set -euo pipefail
for snap in ./*.snap; do
echo "Uploading $snap to edge"
snapcraft upload --release="latest/edge" "$snap"
done

View File

@@ -23,12 +23,7 @@ jobs:
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - uses: ./.github/actions/node-setup
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make deps-frontend deps-backend - run: make deps-frontend deps-backend
# xgo build # xgo build
- run: make release - run: make release
@@ -61,7 +56,7 @@ jobs:
echo "Cleaned name is ${REF_NAME}" echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT" echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
- name: configure aws - name: configure aws
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
with: with:
aws-region: ${{ secrets.AWS_REGION }} aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
@@ -83,8 +78,8 @@ jobs:
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force - run: git fetch --unshallow --quiet --tags --force
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Get cleaned branch name - name: Get cleaned branch name
id: clean_name id: clean_name
env: env:
@@ -92,7 +87,7 @@ jobs:
run: | run: |
REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//') REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT" echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
id: meta id: meta
with: with:
images: |- images: |-
@@ -102,7 +97,7 @@ jobs:
type=raw,value=${{ steps.clean_name.outputs.branch }} type=raw,value=${{ steps.clean_name.outputs.branch }}
annotations: | annotations: |
org.opencontainers.image.authors="maintainers@gitea.io" org.opencontainers.image.authors="maintainers@gitea.io"
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
id: meta_rootless id: meta_rootless
with: with:
images: |- images: |-
@@ -116,18 +111,18 @@ jobs:
annotations: | annotations: |
org.opencontainers.image.authors="maintainers@gitea.io" org.opencontainers.image.authors="maintainers@gitea.io"
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT - name: Login to GHCR using PAT
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: build regular docker image - name: build regular docker image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64,linux/riscv64 platforms: linux/amd64,linux/arm64,linux/riscv64
@@ -137,7 +132,7 @@ jobs:
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max
- name: build rootless docker image - name: build rootless docker image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64,linux/riscv64 platforms: linux/amd64,linux/arm64,linux/riscv64

View File

@@ -24,12 +24,7 @@ jobs:
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - uses: ./.github/actions/node-setup
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make deps-frontend deps-backend - run: make deps-frontend deps-backend
# xgo build # xgo build
- run: make release - run: make release
@@ -62,7 +57,7 @@ jobs:
echo "Cleaned name is ${REF_NAME}" echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT" echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
- name: configure aws - name: configure aws
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
with: with:
aws-region: ${{ secrets.AWS_REGION }} aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
@@ -94,9 +89,9 @@ jobs:
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force - run: git fetch --unshallow --quiet --tags --force
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
id: meta id: meta
with: with:
images: |- images: |-
@@ -109,7 +104,7 @@ jobs:
type=semver,pattern={{version}} type=semver,pattern={{version}}
annotations: | annotations: |
org.opencontainers.image.authors="maintainers@gitea.io" org.opencontainers.image.authors="maintainers@gitea.io"
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
id: meta_rootless id: meta_rootless
with: with:
images: |- images: |-
@@ -125,18 +120,18 @@ jobs:
annotations: | annotations: |
org.opencontainers.image.authors="maintainers@gitea.io" org.opencontainers.image.authors="maintainers@gitea.io"
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT - name: Login to GHCR using PAT
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: build regular container image - name: build regular container image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64,linux/riscv64 platforms: linux/amd64,linux/arm64,linux/riscv64
@@ -144,7 +139,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }} annotations: ${{ steps.meta.outputs.annotations }}
- name: build rootless container image - name: build rootless container image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64,linux/riscv64 platforms: linux/amd64,linux/arm64,linux/riscv64

View File

@@ -27,12 +27,7 @@ jobs:
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - uses: ./.github/actions/node-setup
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make deps-frontend deps-backend - run: make deps-frontend deps-backend
# xgo build # xgo build
- run: make release - run: make release
@@ -65,7 +60,7 @@ jobs:
echo "Cleaned name is ${REF_NAME}" echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT" echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
- name: configure aws - name: configure aws
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
with: with:
aws-region: ${{ secrets.AWS_REGION }} aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
@@ -97,9 +92,9 @@ jobs:
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force - run: git fetch --unshallow --quiet --tags --force
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
id: meta id: meta
with: with:
images: |- images: |-
@@ -116,7 +111,7 @@ jobs:
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
annotations: | annotations: |
org.opencontainers.image.authors="maintainers@gitea.io" org.opencontainers.image.authors="maintainers@gitea.io"
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
id: meta_rootless id: meta_rootless
with: with:
images: |- images: |-
@@ -137,18 +132,18 @@ jobs:
annotations: | annotations: |
org.opencontainers.image.authors="maintainers@gitea.io" org.opencontainers.image.authors="maintainers@gitea.io"
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT - name: Login to GHCR using PAT
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: build regular container image - name: build regular container image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64,linux/riscv64 platforms: linux/amd64,linux/arm64,linux/riscv64
@@ -156,7 +151,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }} annotations: ${{ steps.meta.outputs.annotations }}
- name: build rootless container image - name: build rootless container image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64,linux/riscv64 platforms: linux/amd64,linux/arm64,linux/riscv64

View File

@@ -41,7 +41,6 @@ Jimmy Praet <jimmy.praet@telenet.be> (@jpraet)
Leon Hofmeister <dev.lh@web.de> (@delvh) Leon Hofmeister <dev.lh@web.de> (@delvh)
Wim <wim@42.be> (@42wim) Wim <wim@42.be> (@42wim)
Jason Song <i@wolfogre.com> (@wolfogre) Jason Song <i@wolfogre.com> (@wolfogre)
Yarden Shoham <git@yardenshoham.com> (@yardenshoham)
Yu Tian <zettat123@gmail.com> (@Zettat123) Yu Tian <zettat123@gmail.com> (@Zettat123)
Dong Ge <gedong_1994@163.com> (@sillyguodong) Dong Ge <gedong_1994@163.com> (@sillyguodong)
Xinyi Gong <hestergong@gmail.com> (@HesterG) Xinyi Gong <hestergong@gmail.com> (@HesterG)

View File

@@ -12,13 +12,13 @@ COMMA := ,
XGO_VERSION := go-1.26.x XGO_VERSION := go-1.26.x
AIR_PACKAGE ?= github.com/air-verse/air@v1.65.3 # renovate: datasource=go AIR_PACKAGE ?= github.com/air-verse/air@v1.65.3 # renovate: datasource=go
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.7.0 # renovate: datasource=go EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.8.0 # renovate: datasource=go
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 # renovate: datasource=go GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 # renovate: datasource=go
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 # renovate: datasource=go GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 # renovate: datasource=go
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 # renovate: datasource=go MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 # renovate: datasource=go
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.34.1 # renovate: datasource=go SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.35.0 # renovate: datasource=go
XGO_PACKAGE ?= src.techknowlogick.com/xgo@v1.9.0 # renovate: datasource=go XGO_PACKAGE ?= src.techknowlogick.com/xgo@v1.9.0 # renovate: datasource=go
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.4.0 # renovate: datasource=go GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.5.0 # renovate: datasource=go
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 # renovate: datasource=go ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 # renovate: datasource=go
SHELLCHECK_IMAGE ?= docker.io/koalaman/shellcheck:v0.11.0@sha256:61862eba1fcf09a484ebcc6feea46f1782532571a34ed51fedf90dd25f925a8d # renovate: datasource=docker SHELLCHECK_IMAGE ?= docker.io/koalaman/shellcheck:v0.11.0@sha256:61862eba1fcf09a484ebcc6feea46f1782532571a34ed51fedf90dd25f925a8d # renovate: datasource=docker
@@ -231,7 +231,9 @@ endif
generate-swagger: $(SWAGGER_SPEC) $(OPENAPI3_SPEC) ## generate the swagger spec from code comments generate-swagger: $(SWAGGER_SPEC) $(OPENAPI3_SPEC) ## generate the swagger spec from code comments
$(SWAGGER_SPEC): $(GO_SOURCES) $(SWAGGER_SPEC_INPUT) $(SWAGGER_SPEC): $(GO_SOURCES) $(SWAGGER_SPEC_INPUT)
$(GO) run $(SWAGGER_PACKAGE) generate spec --exclude "$(SWAGGER_EXCLUDE)" --input "$(SWAGGER_SPEC_INPUT)" --output './$(SWAGGER_SPEC)' @output="$$($(GO) run $(SWAGGER_PACKAGE) generate spec --enable-allof-compounding --skip-enum-desc --exclude "$(SWAGGER_EXCLUDE)" --input "$(SWAGGER_SPEC_INPUT)" --output './$(SWAGGER_SPEC)' 2>&1)" || { printf '%s\n' "$$output" >&2; exit 1; }; \
warnings="$$(printf '%s\n' "$$output" | grep -v '^go: ')"; \
if [ -n "$$warnings" ]; then printf '%s\n' "$$warnings" >&2; exit 1; fi
.PHONY: swagger-check .PHONY: swagger-check
swagger-check: generate-swagger swagger-check: generate-swagger
@@ -246,9 +248,11 @@ swagger-check: generate-swagger
swagger-validate: ## check if the swagger spec is valid swagger-validate: ## check if the swagger spec is valid
@# swagger "validate" requires that the "basePath" must start with a slash, but we are using Golang template "{{...}}" @# swagger "validate" requires that the "basePath" must start with a slash, but we are using Golang template "{{...}}"
@$(SED_INPLACE) -E -e 's|"basePath":( *)"(.*)"|"basePath":\1"/\2"|g' './$(SWAGGER_SPEC)' # add a prefix slash to basePath @$(SED_INPLACE) -E -e 's|"basePath":( *)"(.*)"|"basePath":\1"/\2"|g' './$(SWAGGER_SPEC)' # add a prefix slash to basePath
@# FIXME: there are some warnings @output="$$($(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)' 2>&1)"; status=$$?; \
$(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)' $(SED_INPLACE) -E -e 's|"basePath":( *)"/(.*)"|"basePath":\1"\2"|g' './$(SWAGGER_SPEC)'; \
@$(SED_INPLACE) -E -e 's|"basePath":( *)"/(.*)"|"basePath":\1"\2"|g' './$(SWAGGER_SPEC)' # remove the prefix slash from basePath printf '%s\n' "$$output" | grep -v '^go: '; \
[ $$status -eq 0 ] || exit $$status; \
case "$$output" in *WARNING:*) exit 1;; esac
.PHONY: generate-openapi3 .PHONY: generate-openapi3
generate-openapi3: $(OPENAPI3_SPEC) ## generate the OpenAPI 3.0 spec from the Swagger 2.0 spec generate-openapi3: $(OPENAPI3_SPEC) ## generate the OpenAPI 3.0 spec from the Swagger 2.0 spec

View File

@@ -436,6 +436,7 @@
"jsonl", "jsonl",
"mcmeta", "mcmeta",
"sarif", "sarif",
"slnlaunch",
"tact", "tact",
"tfstate", "tfstate",
"topojson", "topojson",
@@ -691,10 +692,17 @@
"extensions": [ "extensions": [
"ini", "ini",
"cnf", "cnf",
"container",
"dof", "dof",
"lektorproject", "lektorproject",
"mount",
"network",
"prefs", "prefs",
"properties", "properties",
"service",
"socket",
"target",
"timer",
"url", "url",
"conf" "conf"
], ],

File diff suppressed because one or more lines are too long

View File

@@ -18,6 +18,7 @@ func newUserCommand() *cli.Command {
microcmdUserDelete(), microcmdUserDelete(),
newUserGenerateAccessTokenCommand(), newUserGenerateAccessTokenCommand(),
microcmdUserMustChangePassword(), microcmdUserMustChangePassword(),
microcmdUserDisableTwoFactor(),
}, },
} }
} }

View File

@@ -0,0 +1,72 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"errors"
"fmt"
"strings"
auth_model "gitea.dev/models/auth"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
"github.com/urfave/cli/v3"
)
func microcmdUserDisableTwoFactor() *cli.Command {
return &cli.Command{
Name: "disable-2fa",
Usage: "Disable two-factor authentication for a user",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"u"},
Usage: "Username of the user to disable 2FA for",
},
&cli.Int64Flag{
Name: "id",
Usage: "ID of the user to disable 2FA for",
},
},
Action: runDisableTwoFactor,
}
}
func runDisableTwoFactor(ctx context.Context, c *cli.Command) error {
if !c.IsSet("id") && !c.IsSet("username") {
return errors.New("either --id or --username must be provided")
}
if !setting.IsInTesting {
if err := initDB(ctx); err != nil {
return err
}
}
var user *user_model.User
var err error
if c.IsSet("id") {
user, err = user_model.GetUserByID(ctx, c.Int64("id"))
} else {
user, err = user_model.GetUserByName(ctx, c.String("username"))
}
if err != nil {
return err
}
// When both selectors are given, make sure they refer to the same user.
if c.IsSet("id") && c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) {
return fmt.Errorf("the user with id %d is %q, which does not match the provided username %q", user.ID, user.Name, c.String("username"))
}
totp, webAuthn, err := auth_model.DisableTwoFactor(ctx, user.ID)
if err != nil {
return err
}
fmt.Printf("Disabled 2FA for user %q (removed %d TOTP and %d WebAuthn credential(s))\n", user.Name, totp, webAuthn)
return nil
}

View File

@@ -0,0 +1,119 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"io"
"strconv"
"testing"
auth_model "gitea.dev/models/auth"
"gitea.dev/models/db"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDisableTwoFactorCommand(t *testing.T) {
ctx := t.Context()
defer func() {
require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}, &auth_model.TwoFactor{}, &auth_model.WebAuthnCredential{}))
}()
t.Run("disable TOTP and WebAuthn", func(t *testing.T) {
require.NoError(t, microcmdUserCreate().Run(ctx, []string{"create", "--username", "tfuser", "--email", "tfuser@gitea.local", "--random-password"}))
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "tfuser"})
// Enroll TOTP.
tf := &auth_model.TwoFactor{UID: user.ID}
require.NoError(t, tf.SetSecret("test-secret"))
_, err := tf.GenerateScratchToken()
require.NoError(t, err)
require.NoError(t, auth_model.NewTwoFactor(ctx, tf))
// Register a WebAuthn credential.
_, err = auth_model.CreateCredential(ctx, user.ID, "test-key", &webauthn.Credential{ID: []byte("test-cred-id")})
require.NoError(t, err)
has, err := auth_model.HasTwoFactorOrWebAuthn(ctx, user.ID)
require.NoError(t, err)
require.True(t, has)
require.NoError(t, microcmdUserDisableTwoFactor().Run(ctx, []string{"disable-2fa", "--username", "tfuser"}))
// Both factors must be gone afterwards.
has, err = auth_model.HasTwoFactorOrWebAuthn(ctx, user.ID)
require.NoError(t, err)
assert.False(t, has)
})
t.Run("disable by id", func(t *testing.T) {
require.NoError(t, microcmdUserCreate().Run(ctx, []string{"create", "--username", "iduser", "--email", "iduser@gitea.local", "--random-password"}))
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "iduser"})
tf := &auth_model.TwoFactor{UID: user.ID}
require.NoError(t, tf.SetSecret("test-secret"))
require.NoError(t, auth_model.NewTwoFactor(ctx, tf))
require.NoError(t, microcmdUserDisableTwoFactor().Run(ctx, []string{"disable-2fa", "--id", strconv.FormatInt(user.ID, 10)}))
has, err := auth_model.HasTwoFactorOrWebAuthn(ctx, user.ID)
require.NoError(t, err)
assert.False(t, has)
})
t.Run("no enrollment is a no-op", func(t *testing.T) {
require.NoError(t, microcmdUserCreate().Run(ctx, []string{"create", "--username", "plainuser", "--email", "plainuser@gitea.local", "--random-password"}))
require.NoError(t, microcmdUserDisableTwoFactor().Run(ctx, []string{"disable-2fa", "--username", "plainuser"}))
})
t.Run("id and username must match when both given", func(t *testing.T) {
require.NoError(t, microcmdUserCreate().Run(ctx, []string{"create", "--username", "matchuser", "--email", "matchuser@gitea.local", "--random-password"}))
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "matchuser"})
id := strconv.FormatInt(user.ID, 10)
// Matching id + username is accepted.
require.NoError(t, microcmdUserDisableTwoFactor().Run(ctx, []string{"disable-2fa", "--id", id, "--username", "matchuser"}))
// Mismatched id + username is rejected.
cmd := microcmdUserDisableTwoFactor()
cmd.Writer, cmd.ErrWriter = io.Discard, io.Discard
err := cmd.Run(ctx, []string{"disable-2fa", "--id", id, "--username", "someotheruser"})
require.Error(t, err)
require.Contains(t, err.Error(), "does not match the provided username")
})
t.Run("failure cases", func(t *testing.T) {
testCases := []struct {
name string
args []string
expectedErr string
}{
{
name: "user does not exist",
args: []string{"disable-2fa", "--username", "nonexistentuser"},
expectedErr: "user does not exist",
},
{
name: "neither id nor username",
args: []string{"disable-2fa"},
expectedErr: "either --id or --username must be provided",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cmd := microcmdUserDisableTwoFactor()
cmd.Writer, cmd.ErrWriter = io.Discard, io.Discard
err := cmd.Run(ctx, tc.args)
require.Error(t, err)
require.Contains(t, err.Error(), tc.expectedErr)
})
}
})
}

View File

@@ -536,6 +536,13 @@ INTERNAL_TOKEN =
;; Leave it empty to apply the default policy, or set it to "unset" to disable Content-Security-Policy. ;; Leave it empty to apply the default policy, or set it to "unset" to disable Content-Security-Policy.
;CONTENT_SECURITY_POLICY_GENERAL = ;CONTENT_SECURITY_POLICY_GENERAL =
;; Webhook and oauth2 clients can only call allowed hosts for security reasons. Comma separated list, eg: external, 192.168.1.0/24, *.mydomain.com
;; Built-in: loopback (for localhost), private (for LAN/intranet), external (for public hosts on internet), * (for all hosts)
;; CIDR list: 1.2.3.0/8, 2001:db8::/32
;; Wildcard hosts: *.mydomain.com, 192.168.100.*
;; This list is enforced on direct connections only. When an HTTP proxy is configured, restricting the proxied target is the proxy server's responsibility.
;ALLOWED_HOST_LIST = external
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[camo] [camo]
@@ -1191,6 +1198,7 @@ LEVEL = Info
;; Default source for the pull request title when opening a new PR. ;; Default source for the pull request title when opening a new PR.
;; "first-commit" uses the oldest commit's summary. ;; "first-commit" uses the oldest commit's summary.
;; "auto" uses commit's summary if the PR only has one commit, normalizes the branch name if multiple commits. ;; "auto" uses commit's summary if the PR only has one commit, normalizes the branch name if multiple commits.
;; "branch-name" always uses the PR's branch name.
;DEFAULT_TITLE_SOURCE = auto ;DEFAULT_TITLE_SOURCE = auto
;; ;;
;; Delay mergeable check until page view or API access, for pull requests that have not been updated in the specified days when their base branches get updated. ;; Delay mergeable check until page view or API access, for pull requests that have not been updated in the specified days when their base branches get updated.
@@ -1754,13 +1762,6 @@ LEVEL = Info
;; Deliver timeout in seconds ;; Deliver timeout in seconds
;DELIVER_TIMEOUT = 5 ;DELIVER_TIMEOUT = 5
;; ;;
;; Webhook can only call allowed hosts for security reasons. Comma separated list, eg: external, 192.168.1.0/24, *.mydomain.com
;; Built-in: loopback (for localhost), private (for LAN/intranet), external (for public hosts on internet), * (for all hosts)
;; CIDR list: 1.2.3.0/8, 2001:db8::/32
;; Wildcard hosts: *.mydomain.com, 192.168.100.*
;; Since 1.15.7. Default to * for 1.15.x, external for 1.16 and later
;ALLOWED_HOST_LIST = external
;;
;; Allow insecure certification ;; Allow insecure certification
;SKIP_TLS_VERIFY = false ;SKIP_TLS_VERIFY = false
;; ;;
@@ -3008,6 +3009,10 @@ LEVEL = Info
;SCOPED_WORKFLOW_DIRS = .gitea/scoped_workflows ;SCOPED_WORKFLOW_DIRS = .gitea/scoped_workflows
;; Maximum number of attempts a single workflow run can have. Default value is 50. ;; Maximum number of attempts a single workflow run can have. Default value is 50.
;MAX_RERUN_ATTEMPTS = 50 ;MAX_RERUN_ATTEMPTS = 50
;; Maximum number of runners that may run the task-assignment query concurrently, per Gitea instance.
;; Caps this instance's DB load when many runners poll at once; excess runners retry on their next poll.
;; In a multi-instance deployment the cluster-wide limit is this value times the number of instances. Default value is 16.
;MAX_CONCURRENT_TASK_PICKS = 16
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -95,8 +95,15 @@ However, if there are no objections from maintainers, the PR can be merged with
### Commit messages ### Commit messages
Mergers are required to rewrite the PR title and the first comment (the summary) when necessary so the squash commit message is clear. Mergers are required to rewrite the PR title and the first comment (the summary) when necessary so the squash commit message is clear.
Usually the Pull Request description and commit message body should not be empty, unless the title is already clear enough or the description would be a copy of the comments in code.
The final commit message should not hedge: replace phrases like `hopefully, <x> won't happen anymore` with definite wording. The final commit message:
- should match the code changes.
- should only keep true co-authors, false-positive co-authors should be removed.
- should not hedge: replace phrases like `hopefully, <x> won't happen anymore` with definite wording.
- should not contain hidden information like `<!-- -->` or extra information after the description's divider `----`.
- should not contain unrelated contents (e.g.: Release Notes, Configuration, etc.) from a Renovate update PR.
#### PR Co-authors #### PR Co-authors
@@ -158,9 +165,16 @@ Any account with write access (including bots and TOC members) **must** use [2FA
Mergers are the maintainers who carry out the final merge of approved PRs. Their responsibilities, described throughout this guide, are: Mergers are the maintainers who carry out the final merge of approved PRs. Their responsibilities, described throughout this guide, are:
- Merging PRs from the [merge queue](#getting-prs-merged) in order, once a PR has `lgtm/done`, no open discussions, and no merge conflicts. - Merging PRs from the [merge queue](#getting-prs-merged) in order, once a PR has `lgtm/done`, no open discussions, and no merge conflicts.
- Rewriting the PR title and summary so the squash [commit message](#commit-messages) is clear, removing false-positive co-authors while keeping every true co-author. - Rewriting the PR title and description prior to the merge, making the [commit message](#commit-messages) clear.\
In particular, mergers should edit the PR description.\
Mergers should **not** edit the actual commit message except to remove unnecessary information. Because of that, even if users are looking at the PR, they can understand what changed.
- Assigning the correct labels (including `type/…`) needed for changelog and backport decisions. - Assigning the correct labels (including `type/…`) needed for changelog and backport decisions.
- Agreeing, together with the owners, on when a release is ready (see [release management](release-management.md)). - Agreeing, together with the owners, on when a release is ready (see [release management](release-management.md)).
- Merging a PR also means the PR looks good to the merger and is approved by the merger.
If a merger violates these merge guides more than 3 times in the past 365 days
(e.g.: merge with unresolved reviews without TOC decision to ignore the review, merge with garbage commit messages),
they may lose their merging privileges for at least three months.
#### Becoming a merger #### Becoming a merger
@@ -206,7 +220,7 @@ The result of this script needs then to be published in the TOC election issue t
- 2026-06-14 ~ 2026-12-31 - 2026-06-14 ~ 2026-12-31
- Company - Company
- [Jason Song](https://gitea.com/wolfogre) <i@wolfogre.com> - [Yu Tian](https://gitea.com/Zettat123) <zettat123@gmail.com>
- [Lunny Xiao](https://gitea.com/lunny) <xiaolunwen@gmail.com> - [Lunny Xiao](https://gitea.com/lunny) <xiaolunwen@gmail.com>
- [Matti Ranta](https://gitea.com/techknowlogick) <techknowlogick@gitea.com> - [Matti Ranta](https://gitea.com/techknowlogick) <techknowlogick@gitea.com>
- Community - Community
@@ -227,7 +241,7 @@ Here's the history of the owners and the time they served:
- [Andrew Thornton](https://gitea.com/zeripath) - [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023 - [Andrew Thornton](https://gitea.com/zeripath) - [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
- [6543](https://gitea.com/6543) - 2023, 2025 - [6543](https://gitea.com/6543) - 2023, 2025
- [John Olheiser](https://gitea.com/jolheiser) - 2023, 2024 - [John Olheiser](https://gitea.com/jolheiser) - 2023, 2024
- [Jason Song](https://gitea.com/wolfogre) - 2023 - [Jason Song](https://gitea.com/wolfogre) - 2023, 2025
## Governance Compensation ## Governance Compensation

View File

@@ -1,6 +1,4 @@
import arrayFunc from 'eslint-plugin-array-func';
import comments from '@eslint-community/eslint-plugin-eslint-comments'; import comments from '@eslint-community/eslint-plugin-eslint-comments';
import deMorgan from 'eslint-plugin-de-morgan';
import globals from 'globals'; import globals from 'globals';
import importPlugin from 'eslint-plugin-import-x'; import importPlugin from 'eslint-plugin-import-x';
import playwright from 'eslint-plugin-playwright'; import playwright from 'eslint-plugin-playwright';
@@ -15,7 +13,6 @@ import vue from 'eslint-plugin-vue';
import vueScopedCss from 'eslint-plugin-vue-scoped-css'; import vueScopedCss from 'eslint-plugin-vue-scoped-css';
import wc from 'eslint-plugin-wc'; import wc from 'eslint-plugin-wc';
import {defineConfig, globalIgnores} from 'eslint/config'; import {defineConfig, globalIgnores} from 'eslint/config';
import type {ESLint} from 'eslint';
import unescapedHtmlLiteral from './tools/eslint-rules/unescaped-html-literal.ts'; import unescapedHtmlLiteral from './tools/eslint-rules/unescaped-html-literal.ts';
@@ -64,10 +61,8 @@ export default defineConfig([
'@eslint-community/eslint-comments': comments, '@eslint-community/eslint-comments': comments,
'@stylistic': stylistic, '@stylistic': stylistic,
'@typescript-eslint': typescriptPlugin.plugin, '@typescript-eslint': typescriptPlugin.plugin,
'array-func': arrayFunc,
'de-morgan': deMorgan,
'gitea': {rules: {'unescaped-html-literal': unescapedHtmlLiteral}}, 'gitea': {rules: {'unescaped-html-literal': unescapedHtmlLiteral}},
'import-x': importPlugin as unknown as ESLint.Plugin, // https://github.com/un-ts/eslint-plugin-import-x/issues/203 'import-x': importPlugin,
regexp, regexp,
sonarjs, sonarjs,
unicorn, unicorn,
@@ -278,12 +273,6 @@ export default defineConfig([
'@typescript-eslint/unified-signatures': [2], '@typescript-eslint/unified-signatures': [2],
'accessor-pairs': [2], 'accessor-pairs': [2],
'array-callback-return': [2, {checkForEach: true}], 'array-callback-return': [2, {checkForEach: true}],
'array-func/avoid-reverse': [2],
'array-func/from-map': [2],
'array-func/no-unnecessary-this-arg': [2],
'array-func/prefer-array-from': [2],
'array-func/prefer-flat-map': [0], // handled by unicorn/prefer-array-flat-map
'array-func/prefer-flat': [0], // handled by unicorn/prefer-array-flat
'arrow-body-style': [0], 'arrow-body-style': [0],
'block-scoped-var': [2], 'block-scoped-var': [2],
'camelcase': [0], 'camelcase': [0],
@@ -294,8 +283,6 @@ export default defineConfig([
'consistent-this': [0], 'consistent-this': [0],
'constructor-super': [2], 'constructor-super': [2],
'curly': [0], 'curly': [0],
'de-morgan/no-negated-conjunction': [2],
'de-morgan/no-negated-disjunction': [2],
'default-case-last': [2], 'default-case-last': [2],
'default-case': [0], 'default-case': [0],
'default-param-last': [0], 'default-param-last': [0],
@@ -745,6 +732,7 @@ export default defineConfig([
'unicorn/consistent-json-file-read': [2], 'unicorn/consistent-json-file-read': [2],
'unicorn/consistent-optional-chaining': [2], 'unicorn/consistent-optional-chaining': [2],
'unicorn/consistent-template-literal-escape': [2], 'unicorn/consistent-template-literal-escape': [2],
'unicorn/consistent-tuple-labels': [2],
'unicorn/custom-error-definition': [0], 'unicorn/custom-error-definition': [0],
'unicorn/default-export-style': [2], 'unicorn/default-export-style': [2],
'unicorn/dom-node-dataset': [2, {preferAttributes: true}], 'unicorn/dom-node-dataset': [2, {preferAttributes: true}],
@@ -778,6 +766,7 @@ export default defineConfig([
'unicorn/no-array-sort-for-min-max': [2], 'unicorn/no-array-sort-for-min-max': [2],
'unicorn/no-array-splice': [0], 'unicorn/no-array-splice': [0],
'unicorn/no-asterisk-prefix-in-documentation-comments': [0], 'unicorn/no-asterisk-prefix-in-documentation-comments': [0],
'unicorn/no-async-promise-finally': [2],
'unicorn/no-await-expression-member': [0], 'unicorn/no-await-expression-member': [0],
'unicorn/no-await-in-promise-methods': [2], 'unicorn/no-await-in-promise-methods': [2],
'unicorn/no-blob-to-file': [2], 'unicorn/no-blob-to-file': [2],
@@ -814,8 +803,10 @@ export default defineConfig([
'unicorn/no-invalid-fetch-options': [2], 'unicorn/no-invalid-fetch-options': [2],
'unicorn/no-invalid-file-input-accept': [2], 'unicorn/no-invalid-file-input-accept': [2],
'unicorn/no-invalid-remove-event-listener': [2], 'unicorn/no-invalid-remove-event-listener': [2],
'unicorn/no-invalid-well-known-symbol-methods': [2],
'unicorn/no-keyword-prefix': [0], 'unicorn/no-keyword-prefix': [0],
'unicorn/no-late-current-target-access': [2], 'unicorn/no-late-current-target-access': [2],
'unicorn/no-late-event-control': [2],
'unicorn/no-lonely-if': [2], 'unicorn/no-lonely-if': [2],
'unicorn/no-loop-iterable-mutation': [2], 'unicorn/no-loop-iterable-mutation': [2],
'unicorn/no-magic-array-flat-depth': [0], 'unicorn/no-magic-array-flat-depth': [0],
@@ -852,9 +843,11 @@ export default defineConfig([
'unicorn/no-uncalled-method': [2], 'unicorn/no-uncalled-method': [2],
'unicorn/no-undeclared-class-members': [2], 'unicorn/no-undeclared-class-members': [2],
'unicorn/no-unnecessary-array-flat-depth': [2], 'unicorn/no-unnecessary-array-flat-depth': [2],
'unicorn/no-unnecessary-array-flat-map': [2],
'unicorn/no-unnecessary-array-splice-count': [2], 'unicorn/no-unnecessary-array-splice-count': [2],
'unicorn/no-unnecessary-await': [2], 'unicorn/no-unnecessary-await': [2],
'unicorn/no-unnecessary-boolean-comparison': [2], 'unicorn/no-unnecessary-boolean-comparison': [2],
'unicorn/no-unnecessary-fetch-options': [0],
'unicorn/no-unnecessary-global-this': [0], 'unicorn/no-unnecessary-global-this': [0],
'unicorn/no-unnecessary-nested-ternary': [2], 'unicorn/no-unnecessary-nested-ternary': [2],
'unicorn/no-unnecessary-polyfills': [2], 'unicorn/no-unnecessary-polyfills': [2],
@@ -867,6 +860,7 @@ export default defineConfig([
'unicorn/no-unreadable-object-destructuring': [0], 'unicorn/no-unreadable-object-destructuring': [0],
'unicorn/no-unsafe-buffer-conversion': [2], 'unicorn/no-unsafe-buffer-conversion': [2],
'unicorn/no-unsafe-dom-html': [0], 'unicorn/no-unsafe-dom-html': [0],
'unicorn/no-unsafe-promise-all-settled-values': [2],
'unicorn/no-unsafe-property-key': [0], 'unicorn/no-unsafe-property-key': [0],
'unicorn/no-unsafe-string-replacement': [0], 'unicorn/no-unsafe-string-replacement': [0],
'unicorn/no-unused-array-method-return': [2], 'unicorn/no-unused-array-method-return': [2],
@@ -896,13 +890,17 @@ export default defineConfig([
'unicorn/number-literal-case': [0], 'unicorn/number-literal-case': [0],
'unicorn/numeric-separators-style': [0], 'unicorn/numeric-separators-style': [0],
'unicorn/operator-assignment': [2], 'unicorn/operator-assignment': [2],
'unicorn/prefer-abort-signal-any': [2],
'unicorn/prefer-abort-signal-timeout': [2],
'unicorn/prefer-add-event-listener': [2], 'unicorn/prefer-add-event-listener': [2],
'unicorn/prefer-add-event-listener-options': [2], 'unicorn/prefer-add-event-listener-options': [2],
'unicorn/prefer-aggregate-error': [2],
'unicorn/prefer-array-find': [0], // handled by @typescript-eslint/prefer-find 'unicorn/prefer-array-find': [0], // handled by @typescript-eslint/prefer-find
'unicorn/prefer-array-flat': [2], 'unicorn/prefer-array-flat': [2],
'unicorn/prefer-array-flat-map': [2], 'unicorn/prefer-array-flat-map': [2],
'unicorn/prefer-array-from-async': [2], 'unicorn/prefer-array-from-async': [2],
'unicorn/prefer-array-from-map': [2], 'unicorn/prefer-array-from-map': [2],
'unicorn/prefer-array-from-range': [2],
'unicorn/prefer-array-index-of': [2], 'unicorn/prefer-array-index-of': [2],
'unicorn/prefer-array-iterable-methods': [2], 'unicorn/prefer-array-iterable-methods': [2],
'unicorn/prefer-array-last-methods': [2], 'unicorn/prefer-array-last-methods': [2],
@@ -912,6 +910,7 @@ export default defineConfig([
'unicorn/prefer-await': [2], 'unicorn/prefer-await': [2],
'unicorn/prefer-bigint-literals': [2], 'unicorn/prefer-bigint-literals': [2],
'unicorn/prefer-blob-reading-methods': [2], 'unicorn/prefer-blob-reading-methods': [2],
'unicorn/prefer-block-statement-over-iife': [2],
'unicorn/prefer-boolean-return': [2], 'unicorn/prefer-boolean-return': [2],
'unicorn/prefer-class-fields': [2], 'unicorn/prefer-class-fields': [2],
'unicorn/prefer-classlist-toggle': [2], 'unicorn/prefer-classlist-toggle': [2],
@@ -924,15 +923,18 @@ export default defineConfig([
'unicorn/prefer-dom-node-append': [2], 'unicorn/prefer-dom-node-append': [2],
'unicorn/prefer-dom-node-html-methods': [0], 'unicorn/prefer-dom-node-html-methods': [0],
'unicorn/prefer-dom-node-remove': [2], 'unicorn/prefer-dom-node-remove': [2],
'unicorn/prefer-dom-node-replace-children': [2],
'unicorn/prefer-dom-node-text-content': [2], 'unicorn/prefer-dom-node-text-content': [2],
'unicorn/prefer-early-return': [0], 'unicorn/prefer-early-return': [0],
'unicorn/prefer-else-if': [2], 'unicorn/prefer-else-if': [2],
'unicorn/prefer-error-is-error': [0],
'unicorn/prefer-event-target': [2], 'unicorn/prefer-event-target': [2],
'unicorn/prefer-export-from': [0], 'unicorn/prefer-export-from': [0],
'unicorn/prefer-flat-math-min-max': [2], 'unicorn/prefer-flat-math-min-max': [2],
'unicorn/prefer-get-or-insert-computed': [2], 'unicorn/prefer-get-or-insert-computed': [2],
'unicorn/prefer-global-number-constants': [2], 'unicorn/prefer-global-number-constants': [2],
'unicorn/prefer-global-this': [0], 'unicorn/prefer-global-this': [0],
'unicorn/prefer-group-by': [2],
'unicorn/prefer-has-check': [2], 'unicorn/prefer-has-check': [2],
'unicorn/prefer-hoisting-branch-code': [2], 'unicorn/prefer-hoisting-branch-code': [2],
'unicorn/prefer-https': [0], // false-positives on namespace and schema URIs 'unicorn/prefer-https': [0], // false-positives on namespace and schema URIs
@@ -942,6 +944,7 @@ export default defineConfig([
'unicorn/prefer-includes-over-repeated-comparisons': [0], // too opinionated 'unicorn/prefer-includes-over-repeated-comparisons': [0], // too opinionated
'unicorn/prefer-iterable-in-constructor': [2], 'unicorn/prefer-iterable-in-constructor': [2],
'unicorn/prefer-iterator-concat': [0], // too opinionated 'unicorn/prefer-iterator-concat': [0], // too opinionated
'unicorn/prefer-iterator-helpers': [2],
'unicorn/prefer-iterator-to-array': [2], 'unicorn/prefer-iterator-to-array': [2],
'unicorn/prefer-iterator-to-array-at-end': [2], 'unicorn/prefer-iterator-to-array-at-end': [2],
'unicorn/prefer-keyboard-event-key': [2], 'unicorn/prefer-keyboard-event-key': [2],
@@ -966,9 +969,11 @@ export default defineConfig([
'unicorn/prefer-object-destructuring-defaults': [2], 'unicorn/prefer-object-destructuring-defaults': [2],
'unicorn/prefer-object-from-entries': [2], 'unicorn/prefer-object-from-entries': [2],
'unicorn/prefer-object-iterable-methods': [2], 'unicorn/prefer-object-iterable-methods': [2],
'unicorn/prefer-observer-apis': [2],
'unicorn/prefer-optional-catch-binding': [2], 'unicorn/prefer-optional-catch-binding': [2],
'unicorn/prefer-path2d': [2], 'unicorn/prefer-path2d': [2],
'unicorn/prefer-private-class-fields': [0], 'unicorn/prefer-private-class-fields': [0],
'unicorn/prefer-promise-try': [2],
'unicorn/prefer-promise-with-resolvers': [2], 'unicorn/prefer-promise-with-resolvers': [2],
'unicorn/prefer-prototype-methods': [0], 'unicorn/prefer-prototype-methods': [0],
'unicorn/prefer-query-selector': [2], 'unicorn/prefer-query-selector': [2],
@@ -979,10 +984,12 @@ export default defineConfig([
'unicorn/prefer-response-static-json': [2], 'unicorn/prefer-response-static-json': [2],
'unicorn/prefer-scoped-selector': [0], 'unicorn/prefer-scoped-selector': [0],
'unicorn/prefer-set-has': [0], 'unicorn/prefer-set-has': [0],
'unicorn/prefer-set-methods': [0],
'unicorn/prefer-set-size': [2], 'unicorn/prefer-set-size': [2],
'unicorn/prefer-short-arrow-method': [2], 'unicorn/prefer-short-arrow-method': [2],
'unicorn/prefer-simple-condition-first': [0], 'unicorn/prefer-simple-condition-first': [0],
'unicorn/prefer-simple-sort-comparator': [2], 'unicorn/prefer-simple-sort-comparator': [2],
'unicorn/prefer-simplified-conditions': [2],
'unicorn/prefer-single-array-predicate': [2], 'unicorn/prefer-single-array-predicate': [2],
'unicorn/prefer-single-call': [2], 'unicorn/prefer-single-call': [2],
'unicorn/prefer-single-object-destructuring': [2], 'unicorn/prefer-single-object-destructuring': [2],
@@ -1002,6 +1009,7 @@ export default defineConfig([
'unicorn/prefer-switch': [0], 'unicorn/prefer-switch': [0],
'unicorn/prefer-temporal': [0], 'unicorn/prefer-temporal': [0],
'unicorn/prefer-ternary': [0], 'unicorn/prefer-ternary': [0],
'unicorn/prefer-toggle-attribute': [2],
'unicorn/prefer-top-level-await': [0], 'unicorn/prefer-top-level-await': [0],
'unicorn/prefer-type-error': [0], 'unicorn/prefer-type-error': [0],
'unicorn/prefer-type-literal-last': [0], 'unicorn/prefer-type-literal-last': [0],
@@ -1010,6 +1018,7 @@ export default defineConfig([
'unicorn/prefer-unicode-code-point-escapes': [0], 'unicorn/prefer-unicode-code-point-escapes': [0],
'unicorn/prefer-url-can-parse': [2], 'unicorn/prefer-url-can-parse': [2],
'unicorn/prefer-url-href': [2], 'unicorn/prefer-url-href': [2],
'unicorn/prefer-url-search-parameters': [2],
'unicorn/prefer-while-loop-condition': [2], 'unicorn/prefer-while-loop-condition': [2],
'unicorn/prevent-abbreviations': [0], 'unicorn/prevent-abbreviations': [0],
'unicorn/relative-url-style': [2], 'unicorn/relative-url-style': [2],

6
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1776877367, "lastModified": 1782723713,
"narHash": "sha256-EHq1/OX139R1RvBzOJ0aMRT3xnWyqtHBRUBuO1gFzjI=", "narHash": "sha256-oPXCU/SSUokcGaJREHibG1CBX3+s/W7orDWQOZDsEeQ=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "0726a0ecb6d4e08f6adced58726b95db924cef57", "rev": "b5aa0fbd538984f6e3d201be0005b4463d8b09f8",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -34,7 +34,7 @@
# only bump toolchain versions here # only bump toolchain versions here
go = pkgs.go_1_26; go = pkgs.go_1_26;
nodejs = pkgs.nodejs_24; nodejs = pkgs.nodejs_26;
python3 = pkgs.python314; python3 = pkgs.python314;
pnpm = pkgs.pnpm_10; pnpm = pkgs.pnpm_10;

60
go.mod
View File

@@ -1,6 +1,6 @@
module gitea.dev module gitea.dev
go 1.26.4 go 1.26.5
require ( require (
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
@@ -9,11 +9,11 @@ require (
gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a
gitea.com/go-chi/cache v0.2.1 gitea.com/go-chi/cache v0.2.1
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e gitea.com/go-chi/session v0.0.0-20260708011333-ebced8a7a2d6
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
gitea.dev/actions-proto-go v0.6.0 gitea.dev/actions-proto-go v0.6.0
gitea.dev/sdk v1.1.0 gitea.dev/sdk v1.2.0
github.com/42wim/httpsig v1.2.4 github.com/42wim/httpsig v1.2.4
github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432 github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0
@@ -24,12 +24,12 @@ require (
github.com/PuerkitoBio/goquery v1.12.0 github.com/PuerkitoBio/goquery v1.12.0
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0 github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0
github.com/alecthomas/chroma/v2 v2.27.0 github.com/alecthomas/chroma/v2 v2.27.0
github.com/aws/aws-sdk-go-v2/credentials v1.19.24 github.com/aws/aws-sdk-go-v2/credentials v1.19.28
github.com/aws/aws-sdk-go-v2/service/codecommit v1.34.4 github.com/aws/aws-sdk-go-v2/service/codecommit v1.35.0
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
github.com/blevesearch/bleve/v2 v2.6.0 github.com/blevesearch/bleve/v2 v2.6.0
github.com/bohde/codel v0.2.0 github.com/bohde/codel v0.2.0
github.com/buildkite/terminal-to-html/v3 v3.16.8 github.com/buildkite/terminal-to-html/v3 v3.17.1
github.com/caddyserver/certmagic v0.25.4 github.com/caddyserver/certmagic v0.25.4
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20260309112543-12416315a635 github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20260309112543-12416315a635
github.com/chi-middleware/proxy v1.1.1 github.com/chi-middleware/proxy v1.1.1
@@ -44,20 +44,20 @@ require (
github.com/fsnotify/fsnotify v1.10.1 github.com/fsnotify/fsnotify v1.10.1
github.com/getkin/kin-openapi v0.140.0 github.com/getkin/kin-openapi v0.140.0
github.com/gliderlabs/ssh v0.3.8 github.com/gliderlabs/ssh v0.3.8
github.com/go-chi/chi/v5 v5.3.0 github.com/go-chi/chi/v5 v5.3.1
github.com/go-chi/cors v1.2.2 github.com/go-chi/cors v1.2.2
github.com/go-co-op/gocron/v2 v2.21.2 github.com/go-co-op/gocron/v2 v2.21.2
github.com/go-enry/go-enry/v2 v2.9.6 github.com/go-enry/go-enry/v2 v2.9.6
github.com/go-git/go-billy/v5 v5.9.0 github.com/go-git/go-billy/v5 v5.9.0
github.com/go-git/go-git/v5 v5.19.1 github.com/go-git/go-git/v5 v5.19.1
github.com/go-ldap/ldap/v3 v3.4.13 github.com/go-ldap/ldap/v3 v3.4.13
github.com/go-redsync/redsync/v4 v4.16.0 github.com/go-redsync/redsync/v4 v4.17.0
github.com/go-sql-driver/mysql v1.10.0 github.com/go-sql-driver/mysql v1.10.0
github.com/go-webauthn/webauthn v0.17.4 github.com/go-webauthn/webauthn v0.17.4
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
github.com/golang-jwt/jwt/v5 v5.3.1 github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/go-github/v88 v88.0.0 github.com/google/go-github/v89 v89.0.0
github.com/google/licenseclassifier/v2 v2.0.0 github.com/google/licenseclassifier/v2 v2.0.0
github.com/google/pprof v0.0.0-20260604005048-7023385849c0 github.com/google/pprof v0.0.0-20260604005048-7023385849c0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
@@ -68,8 +68,8 @@ require (
github.com/huandu/xstrings v1.5.0 github.com/huandu/xstrings v1.5.0
github.com/jhillyerd/enmime/v2 v2.4.1 github.com/jhillyerd/enmime/v2 v2.4.1
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/klauspost/compress v1.18.6 github.com/klauspost/compress v1.19.0
github.com/klauspost/cpuid/v2 v2.3.0 github.com/klauspost/cpuid/v2 v2.4.0
github.com/lib/pq v1.12.3 github.com/lib/pq v1.12.3
github.com/markbates/goth v1.82.0 github.com/markbates/goth v1.82.0
github.com/mattn/go-isatty v0.0.22 github.com/mattn/go-isatty v0.0.22
@@ -78,7 +78,7 @@ require (
github.com/mholt/archives v0.1.5 github.com/mholt/archives v0.1.5
github.com/microcosm-cc/bluemonday v1.0.27 github.com/microcosm-cc/bluemonday v1.0.27
github.com/microsoft/go-mssqldb v1.10.0 github.com/microsoft/go-mssqldb v1.10.0
github.com/minio/minio-go/v7 v7.2.0 github.com/minio/minio-go/v7 v7.2.1
github.com/msteinert/pam/v2 v2.1.0 github.com/msteinert/pam/v2 v2.1.0
github.com/niklasfasching/go-org v1.9.1 github.com/niklasfasching/go-org v1.9.1
github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/go-digest v1.0.0
@@ -96,29 +96,29 @@ require (
github.com/tstranex/u2f v1.0.0 github.com/tstranex/u2f v1.0.0
github.com/ulikunitz/xz v0.5.15 github.com/ulikunitz/xz v0.5.15
github.com/urfave/cli-docs/v3 v3.1.0 github.com/urfave/cli-docs/v3 v3.1.0
github.com/urfave/cli/v3 v3.10.0 github.com/urfave/cli/v3 v3.10.1
github.com/wneessen/go-mail v0.7.3 github.com/wneessen/go-mail v0.8.1
github.com/yohcop/openid-go v1.0.1 github.com/yohcop/openid-go v1.0.1
github.com/yuin/goldmark v1.8.2 github.com/yuin/goldmark v1.8.2
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
gitlab.com/gitlab-org/api/client-go/v2 v2.42.0 gitlab.com/gitlab-org/api/client-go/v2 v2.46.0
go.yaml.in/yaml/v4 v4.0.0-rc.5 go.yaml.in/yaml/v4 v4.0.0-rc.5
golang.org/x/crypto v0.53.0 golang.org/x/crypto v0.54.0
golang.org/x/image v0.43.0 golang.org/x/image v0.44.0
golang.org/x/mod v0.37.0 golang.org/x/mod v0.38.0
golang.org/x/net v0.56.0 golang.org/x/net v0.57.0
golang.org/x/oauth2 v0.36.0 golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.21.0 golang.org/x/sync v0.22.0
golang.org/x/sys v0.46.0 golang.org/x/sys v0.47.0
golang.org/x/text v0.38.0 golang.org/x/text v0.40.0
google.golang.org/grpc v1.81.1 google.golang.org/grpc v1.82.0
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.11
gopkg.in/ini.v1 v1.67.3 gopkg.in/ini.v1 v1.67.3
modernc.org/sqlite v1.53.0 modernc.org/sqlite v1.53.0
mvdan.cc/xurls/v2 v2.6.0 mvdan.cc/xurls/v2 v2.6.0
strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab
xorm.io/builder v0.3.13 xorm.io/builder v0.3.13
xorm.io/xorm v1.3.11 xorm.io/xorm v1.4.1
) )
require ( require (
@@ -133,10 +133,10 @@ require (
github.com/andybalholm/brotli v1.2.1 // indirect github.com/andybalholm/brotli v1.2.1 // indirect
github.com/andybalholm/cascadia v1.3.4 // indirect github.com/andybalholm/cascadia v1.3.4 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/aws/aws-sdk-go-v2 v1.42.0 // indirect github.com/aws/aws-sdk-go-v2 v1.42.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 // indirect
github.com/aws/smithy-go v1.27.2 // indirect github.com/aws/smithy-go v1.27.3 // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.5 // indirect github.com/bits-and-blooms/bitset v1.24.5 // indirect
@@ -199,9 +199,7 @@ require (
github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/inbucket/html2text v1.0.0 // indirect github.com/inbucket/html2text v1.0.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
@@ -269,7 +267,7 @@ require (
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/time v0.15.0 // indirect golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.45.0 // indirect golang.org/x/tools v0.47.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

131
go.sum
View File

@@ -18,8 +18,8 @@ gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g=
gitea.com/go-chi/cache v0.2.1/go.mod h1:Qic0HZ8hOHW62ETGbonpwz8WYypj9NieU9659wFUJ8Q= gitea.com/go-chi/cache v0.2.1/go.mod h1:Qic0HZ8hOHW62ETGbonpwz8WYypj9NieU9659wFUJ8Q=
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo= gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo=
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098/go.mod h1:LjzIOHlRemuUyO7WR12fmm18VZIlCAaOt9L3yKw40pk= gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098/go.mod h1:LjzIOHlRemuUyO7WR12fmm18VZIlCAaOt9L3yKw40pk=
gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e h1:4bugwPyGMLvblEm3pZ8fZProSPVxE4l0UXF2Kv6IJoY= gitea.com/go-chi/session v0.0.0-20260708011333-ebced8a7a2d6 h1:YWzVGeC/8SZThrJS48ZmQYLkzssdeABxHPhbdnxPDIU=
gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e/go.mod h1:KDvcfMUoXfATPHs2mbMoXFTXT45/FAFAS39waz9tPk0= gitea.com/go-chi/session v0.0.0-20260708011333-ebced8a7a2d6/go.mod h1:KDvcfMUoXfATPHs2mbMoXFTXT45/FAFAS39waz9tPk0=
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:+wWBi6Qfruqu7xJgjOIrKVQGiLUZdpKYCZewJ4clqhw= gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:+wWBi6Qfruqu7xJgjOIrKVQGiLUZdpKYCZewJ4clqhw=
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY= gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY=
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o= gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o=
@@ -28,8 +28,8 @@ gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGq
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
gitea.dev/actions-proto-go v0.6.0 h1:gjllYQ5vmwlkqOeofTQu5qKTZpmf7kWsafoHvoPCSzY= gitea.dev/actions-proto-go v0.6.0 h1:gjllYQ5vmwlkqOeofTQu5qKTZpmf7kWsafoHvoPCSzY=
gitea.dev/actions-proto-go v0.6.0/go.mod h1:p4RX+D9oqiEEzzkPMXscw2CmaGuYFPWFc6xIOmDNDqs= gitea.dev/actions-proto-go v0.6.0/go.mod h1:p4RX+D9oqiEEzzkPMXscw2CmaGuYFPWFc6xIOmDNDqs=
gitea.dev/sdk v1.1.0 h1:wLlz03WkLEiXa2bQpO1JQBTlYf7tQI2neYtZK1kU+TE= gitea.dev/sdk v1.2.0 h1:avRtJl/nKCGispgSalo9czoZM9Rto1awnE0caNAoXGo=
gitea.dev/sdk v1.1.0/go.mod h1:Zfl+EZXdsGGCLkryDfsmvYrQo6GKMl4U3BJA8Beu+cs= gitea.dev/sdk v1.2.0/go.mod h1:rfh5oNdIK24cbCREwIn1tqWKQW+IICXFGWJyebuOAOE=
github.com/42wim/httpsig v1.2.4 h1:mI5bH0nm4xn7K18fo1K3okNDRq8CCJ0KbBYWyA6r8lU= github.com/42wim/httpsig v1.2.4 h1:mI5bH0nm4xn7K18fo1K3okNDRq8CCJ0KbBYWyA6r8lU=
github.com/42wim/httpsig v1.2.4/go.mod h1:yKsYfSyTBEohkPik224QPFylmzEBtda/kjyIAJjh3ps= github.com/42wim/httpsig v1.2.4/go.mod h1:yKsYfSyTBEohkPik224QPFylmzEBtda/kjyIAJjh3ps=
github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432 h1:3Fcz1QzlS7Jv4FT2KI3cHNSZL+KPN3dXxurn9f3YL/Y= github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432 h1:3Fcz1QzlS7Jv4FT2KI3cHNSZL+KPN3dXxurn9f3YL/Y=
@@ -92,18 +92,18 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuW
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA= github.com/aws/aws-sdk-go-v2 v1.42.1 h1:9eOTgu1z/dVtYpNZ3/8/XbbaX0x/BqE3HUzAzs6K0ek=
github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= github.com/aws/aws-sdk-go-v2 v1.42.1/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM=
github.com/aws/aws-sdk-go-v2/credentials v1.19.24 h1:2hQqYCV9yqyePQ9o6dCrZc/zO8U3TwPr9mIKlZnPu/I= github.com/aws/aws-sdk-go-v2/credentials v1.19.28 h1:zTXJSsNcoO91/mTXsZoYf0AK8dvNPiA58/VtyGXR+wM=
github.com/aws/aws-sdk-go-v2/credentials v1.19.24/go.mod h1:IDwpACtwqHLISdzfwUUNq4P9DsB/h5BLg4FwJPNfqFY= github.com/aws/aws-sdk-go-v2/credentials v1.19.28/go.mod h1:Kd9E0JzDBW/q1xbsHFrev/GnbAf5J0Ng8xoyc7HZ91Q=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 h1:xM/Is9cKMHa8Jj8zkvWhvrFkZsXJV9E+BB4g0HW0duQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30/go.mod h1:WueJeNDZvK1fMYEWJIkcivBfEzUkTpBhzlrUKKY8EuA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 h1:jn46zC9LdsVR/ZpMIJqMqb8hHv31BlLx3ulVqNspUOk=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30/go.mod h1:1hTMsAgbdS/AtUi4bw8+gUuh1pceo+eXRLfpSuSQj3M=
github.com/aws/aws-sdk-go-v2/service/codecommit v1.34.4 h1:Uu+wqrOXozYYvaxcNIqjFsMTjoIJIZDN3R0f70ZIjyQ= github.com/aws/aws-sdk-go-v2/service/codecommit v1.35.0 h1:w/iPWj+VObIbyo1WYN5010Bi61OkkqOiFZpNDAP1Rfc=
github.com/aws/aws-sdk-go-v2/service/codecommit v1.34.4/go.mod h1:pYrBdL1tMTZO7PaKRsa1cTUB8HtQh3fFM3zJHGhTQcE= github.com/aws/aws-sdk-go-v2/service/codecommit v1.35.0/go.mod h1:uH156Pb0jnMXuCSD9irU0Qz6UNhYUF2qbowFqIwyLQ8=
github.com/aws/smithy-go v1.27.2 h1:y9NPmSE6am6LjEFPfqHqG/jJk7AauQvhCJONKh7kpzk= github.com/aws/smithy-go v1.27.3 h1:F3Zb497UhhskkfpJmfkXswyo+t0sh9OTBnIHjogWbVY=
github.com/aws/smithy-go v1.27.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aws/smithy-go v1.27.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -185,8 +185,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/buildkite/terminal-to-html/v3 v3.16.8 h1:QN/daUob6cmK8GcdKnwn9+YTlPr1vNj+oeAIiJK6fPc= github.com/buildkite/terminal-to-html/v3 v3.17.1 h1:gs9WhKUwxK27YHf3hijj4tG4wuDLfCANYbRPd5ltMdI=
github.com/buildkite/terminal-to-html/v3 v3.16.8/go.mod h1:+k1KVKROZocrTLsEQ9PEf9A+8+X8uaVV5iO1ZIOwKYM= github.com/buildkite/terminal-to-html/v3 v3.17.1/go.mod h1:0pv+Z75FC+NvIju2o+Cw+YHIDkueeudIoeyfVm8i8GQ=
github.com/caddyserver/certmagic v0.25.4 h1:8eIXh0HC3MsGnNo8One+BCxMGTbe5zb/oz+2KsxBFQg= github.com/caddyserver/certmagic v0.25.4 h1:8eIXh0HC3MsGnNo8One+BCxMGTbe5zb/oz+2KsxBFQg=
github.com/caddyserver/certmagic v0.25.4/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA= github.com/caddyserver/certmagic v0.25.4/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA=
github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE= github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE=
@@ -285,8 +285,8 @@ github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1T
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM= github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8=
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-co-op/gocron/v2 v2.21.2 h1:bD8/YwkojYHgXFr3iEulL148KBdTbKVxUZzFKpXcdbY= github.com/go-co-op/gocron/v2 v2.21.2 h1:bD8/YwkojYHgXFr3iEulL148KBdTbKVxUZzFKpXcdbY=
@@ -319,8 +319,8 @@ github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOr
github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-redsync/redsync/v4 v4.16.0 h1:bNcOzeHH9d3s6pghU9NJFMPrQa41f5Nx3L4YKr3BdEU= github.com/go-redsync/redsync/v4 v4.17.0 h1:FFJ+uxZs44y4Sq10//IFKic9T94AYl+u3Sog98AHzBo=
github.com/go-redsync/redsync/v4 v4.16.0/go.mod h1:V4gagqgyASWBZuwx4xGzu72aZNb/6Mo05byUa3mVmKQ= github.com/go-redsync/redsync/v4 v4.17.0/go.mod h1:CKVA6qwT07S/916i+Yd9h1/8YFQhCCpPYTQhvvYytJo=
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw= github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
@@ -376,8 +376,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v88 v88.0.0 h1:dZA9IKkPK1eXZj4ypngnpRj5FwdpTv4whix2PrQMP7M= github.com/google/go-github/v89 v89.0.0 h1:35bEK5XoEcF3PZrlVbl9XN63f5BcJRA/UGkxeC9xPg0=
github.com/google/go-github/v88 v88.0.0/go.mod h1:rufTDgn2N45wjhukLTyxmvc9nilSp3mr3Rgtt6b1MPw= github.com/google/go-github/v89 v89.0.0/go.mod h1:QLcbU0ipeAqQuR5KSg8c2lql4Qk1EwJ2dWz/0rP4Nho=
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
@@ -413,17 +413,10 @@ github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pw
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/graph-gophers/graphql-go v1.10.2 h1:HXu6Wu5klCH4ALn1fQHVI20cjEIa4wftavHIgbLA4Fo=
github.com/graph-gophers/graphql-go v1.10.2/go.mod h1:AsADheC4CCFwd8n1/QbkduTlHgYYMsRgtPihYVAlEsk=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
@@ -471,12 +464,12 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY= github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
@@ -534,8 +527,8 @@ github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.2.0 h1:RCJM0R1XOsRs+A3x3UCaf3ZYbByDaLjFeAi+YCQEPhs= github.com/minio/minio-go/v7 v7.2.1 h1:PfBfwvKB/MmqyN8Vb1G9voWisaM9OrLv+WwOvMwS9Dw=
github.com/minio/minio-go/v7 v7.2.0/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0= github.com/minio/minio-go/v7 v7.2.1/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0=
github.com/minio/minlz v1.1.1 h1:OGmft1V6AnI/Wme332U6bhG54nxEan+VFgkD7lat4KM= github.com/minio/minlz v1.1.1 h1:OGmft1V6AnI/Wme332U6bhG54nxEan+VFgkD7lat4KM=
github.com/minio/minlz v1.1.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= github.com/minio/minlz v1.1.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -620,10 +613,10 @@ github.com/quasoft/websspi v1.1.2/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKc
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E= github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E=
github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/redis/rueidis v1.0.71 h1:pODtnAR5GAB7j4ekhldZ29HKOxe4Hph0GTDGk1ayEQY= github.com/redis/rueidis v1.0.76 h1:RdDWuvlYBSp+bTrBvaXqJnNEL3VVzsnjo+0psPFgLc4=
github.com/redis/rueidis v1.0.71/go.mod h1:lfdcZzJ1oKGKL37vh9fO3ymwt+0TdjkkUCJxbgpmcgQ= github.com/redis/rueidis v1.0.76/go.mod h1:UsfHPSbomB6QAVMk4iiFkzRy0nh9o7scDGa+SitvBY4=
github.com/redis/rueidis/rueidiscompat v1.0.71 h1:wNZ//kEjMZgBM0KCk7ncOX8KmAgROU2kDdDNpwheG4w= github.com/redis/rueidis/rueidiscompat v1.0.76 h1:7LikbiqCQqCsZXeZ+akgZMnjIV/J0VHih9PIX4gGZC4=
github.com/redis/rueidis/rueidiscompat v1.0.71/go.mod h1:esmCLJvaRzZoKlgB82G1bY7Iky5TnO9Rz+NlhbEccFI= github.com/redis/rueidis/rueidiscompat v1.0.76/go.mod h1:UatQQLVj4QMIsZtpvRWY28qm6r2d72idhcS+C/RM+Zg=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rhysd/actionlint v1.7.12 h1:vQ4GeJN86C0QH+gTUQcs8McmK62OLT3kmakPMtEWYnY= github.com/rhysd/actionlint v1.7.12 h1:vQ4GeJN86C0QH+gTUQcs8McmK62OLT3kmakPMtEWYnY=
@@ -709,11 +702,11 @@ github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs=
github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM= github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw= github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw=
github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to= github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to=
github.com/urfave/cli/v3 v3.10.0 h1:0aU8yOObVDMkM13Cj4G+zb4P0PdeJMec65f81Ak1ioM= github.com/urfave/cli/v3 v3.10.1 h1:7Kx9H50hrHbRbyxgO1KP6/BcbiGRz0uYh5YyQ30JEEY=
github.com/urfave/cli/v3 v3.10.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/urfave/cli/v3 v3.10.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/wneessen/go-mail v0.7.3 h1:g3DravXC5SMlVdboFrQA8Jx95A8sOzoBeS5F+vzNRK0= github.com/wneessen/go-mail v0.8.1 h1:tVcncj02/QySVFw3zr/kXOzZcuFQqBNT6K+Rbgm/pcM=
github.com/wneessen/go-mail v0.7.3/go.mod h1:QGhBX0yNbc1J+Mkjcu7z2rpj4B4l+BmDY8gYznPC9sk= github.com/wneessen/go-mail v0.8.1/go.mod h1:dWZ61zadzCIyvB4y1/YzC5O7MrbbzBfPkARmbosdf8w=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
@@ -740,8 +733,8 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
gitlab.com/gitlab-org/api/client-go/v2 v2.42.0 h1:Bq5YIYgUJVbt4Hbh7ibBwNR4SNEafsyDVhIXl7dXDdg= gitlab.com/gitlab-org/api/client-go/v2 v2.46.0 h1:t9QSeMck3kuHu9R6C7h8cYk4qBI4ZD4SQ0jspxKYs2E=
gitlab.com/gitlab-org/api/client-go/v2 v2.42.0/go.mod h1:SKUbKSS59KPt6WeGNJoYF8HDaf/rFMUSITlftj/HkLg= gitlab.com/gitlab-org/api/client-go/v2 v2.46.0/go.mod h1:1LK5UOowqo9apkL+zn7sM8t/hCzBV6F1b2RUIHL9+Oo=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
@@ -776,12 +769,12 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY= golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q= golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -790,8 +783,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -807,8 +800,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -822,8 +815,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -854,8 +847,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -865,8 +858,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -877,8 +870,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -890,16 +883,16 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad h1:45WmJvIV6C2+O/jjLkPUH+F3aOj/1miDoU2DD0+NWbg= google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad h1:45WmJvIV6C2+O/jjLkPUH+F3aOj/1miDoU2DD0+NWbg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -964,5 +957,5 @@ strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab h1:3IZDVyI
strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab/go.mod h1:FJGmPh3vz9jSos1L/F91iAgnC/aejc0wIIrF2ZwJxdY= strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab/go.mod h1:FJGmPh3vz9jSos1L/F91iAgnC/aejc0wIIrF2ZwJxdY=
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo= xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/xorm v1.3.11 h1:i4tlVUASogb0ZZFJHA7dZqoRU2pUpUsutnNdaOlFyMI= xorm.io/xorm v1.4.1 h1:m7QlNd0eBGb31IV4Q/ow0Du83rtdC1CiwlvJZGvYde8=
xorm.io/xorm v1.3.11/go.mod h1:cs0ePc8O4a0jD78cNvD+0VFwhqotTvLQZv372QsDw7Q= xorm.io/xorm v1.4.1/go.mod h1:cs0ePc8O4a0jD78cNvD+0VFwhqotTvLQZv372QsDw7Q=

View File

@@ -121,7 +121,7 @@ func (run *ActionRun) PrettyRef() string {
return refName.ShortName() return refName.ShortName()
} }
// RefTooltip return a tooltop of run's ref. For pull request, it's the title of the PR, otherwise it's the ShortName. // RefTooltip return a tooltip of run's ref. For pull request, it's the title of the PR, otherwise it's the ShortName.
func (run *ActionRun) RefTooltip() string { func (run *ActionRun) RefTooltip() string {
payload, err := run.GetPullRequestEventPayload() payload, err := run.GetPullRequestEventPayload()
if err == nil && payload != nil && payload.PullRequest != nil { if err == nil && payload != nil && payload.PullRequest != nil {
@@ -277,6 +277,12 @@ func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, er
return &run, nil return &run, nil
} }
func GetRunsByRepoAndID(ctx context.Context, repoID int64, runIDs []int64) ([]*ActionRun, error) {
var runs []*ActionRun
err := db.GetEngine(ctx).In("id", runIDs).Where("repo_id=?", repoID).Find(&runs)
return runs, err
}
func GetRunByRepoAndIndex(ctx context.Context, repoID, runIndex int64) (*ActionRun, error) { func GetRunByRepoAndIndex(ctx context.Context, repoID, runIndex int64) (*ActionRun, error) {
run, has, err := db.Get[ActionRun](ctx, builder.Eq{"repo_id": repoID, "`index`": runIndex}) run, has, err := db.Get[ActionRun](ctx, builder.Eq{"repo_id": repoID, "`index`": runIndex})
if err != nil { if err != nil {

View File

@@ -99,6 +99,10 @@ type FindRunJobOptions struct {
UpdatedBefore timeutil.TimeStamp UpdatedBefore timeutil.TimeStamp
ConcurrencyGroup string ConcurrencyGroup string
OrderBy db.SearchOrderBy OrderBy db.SearchOrderBy
// AccessibleRepoIDsSubQuery, when non-nil, restricts results to the repo IDs selected by the
// subquery (the caller's accessible repos). A nil value means no restriction. Using a subquery
// instead of a materialized ID slice avoids exceeding DB parameter limits for large owners.
AccessibleRepoIDsSubQuery *builder.Builder
} }
var JobOrderByMap = map[string]map[string]db.SearchOrderBy{ var JobOrderByMap = map[string]map[string]db.SearchOrderBy{
@@ -132,6 +136,9 @@ func (opts FindRunJobOptions) ToConds() builder.Cond {
} }
cond = cond.And(builder.Eq{"`action_run_job`.concurrency_group": opts.ConcurrencyGroup}) cond = cond.And(builder.Eq{"`action_run_job`.concurrency_group": opts.ConcurrencyGroup})
} }
if opts.AccessibleRepoIDsSubQuery != nil {
cond = cond.And(builder.In("`action_run_job`.repo_id", opts.AccessibleRepoIDsSubQuery))
}
return cond return cond
} }

View File

@@ -70,6 +70,10 @@ type FindRunOptions struct {
Status []Status Status []Status
ConcurrencyGroup string ConcurrencyGroup string
CommitSHA string CommitSHA string
// AccessibleRepoIDsSubQuery, when non-nil, restricts results to the repo IDs selected by the
// subquery (the caller's accessible repos). A nil value means no restriction. Using a subquery
// instead of a materialized ID slice avoids exceeding DB parameter limits for large owners.
AccessibleRepoIDsSubQuery *builder.Builder
} }
func (opts FindRunOptions) ToConds() builder.Cond { func (opts FindRunOptions) ToConds() builder.Cond {
@@ -101,6 +105,9 @@ func (opts FindRunOptions) ToConds() builder.Cond {
if opts.CommitSHA != "" { if opts.CommitSHA != "" {
cond = cond.And(builder.Eq{"`action_run`.commit_sha": opts.CommitSHA}) cond = cond.And(builder.Eq{"`action_run`.commit_sha": opts.CommitSHA})
} }
if opts.AccessibleRepoIDsSubQuery != nil {
cond = cond.And(builder.In("`action_run`.repo_id", opts.AccessibleRepoIDsSubQuery))
}
return cond return cond
} }
@@ -135,8 +142,8 @@ type StatusInfo struct {
// GetStatusInfoList returns a slice of StatusInfo // GetStatusInfoList returns a slice of StatusInfo
func GetStatusInfoList(ctx context.Context, lang translation.Locale) []StatusInfo { func GetStatusInfoList(ctx context.Context, lang translation.Locale) []StatusInfo {
// same as those in aggregateJobStatus // same as those in aggregateJobStatus (StatusUnknown excluded; it's the "shouldn't happen" fallback)
allStatus := []Status{StatusSuccess, StatusFailure, StatusWaiting, StatusRunning, StatusCancelling} allStatus := []Status{StatusSuccess, StatusFailure, StatusCancelled, StatusSkipped, StatusWaiting, StatusRunning, StatusBlocked, StatusCancelling}
statusInfoList := make([]StatusInfo, 0, len(allStatus)) statusInfoList := make([]StatusInfo, 0, len(allStatus))
for _, s := range allStatus { for _, s := range allStatus {
statusInfoList = append(statusInfoList, StatusInfo{ statusInfoList = append(statusInfoList, StatusInfo{

View File

@@ -73,8 +73,11 @@ func TestGetStatusInfoList(t *testing.T) {
assert.Equal(t, []StatusInfo{ assert.Equal(t, []StatusInfo{
{Status: int(StatusSuccess), StatusName: StatusSuccess.String(), DisplayedStatus: "actions.status.success"}, {Status: int(StatusSuccess), StatusName: StatusSuccess.String(), DisplayedStatus: "actions.status.success"},
{Status: int(StatusFailure), StatusName: StatusFailure.String(), DisplayedStatus: "actions.status.failure"}, {Status: int(StatusFailure), StatusName: StatusFailure.String(), DisplayedStatus: "actions.status.failure"},
{Status: int(StatusCancelled), StatusName: StatusCancelled.String(), DisplayedStatus: "actions.status.cancelled"},
{Status: int(StatusSkipped), StatusName: StatusSkipped.String(), DisplayedStatus: "actions.status.skipped"},
{Status: int(StatusWaiting), StatusName: StatusWaiting.String(), DisplayedStatus: "actions.status.waiting"}, {Status: int(StatusWaiting), StatusName: StatusWaiting.String(), DisplayedStatus: "actions.status.waiting"},
{Status: int(StatusRunning), StatusName: StatusRunning.String(), DisplayedStatus: "actions.status.running"}, {Status: int(StatusRunning), StatusName: StatusRunning.String(), DisplayedStatus: "actions.status.running"},
{Status: int(StatusBlocked), StatusName: StatusBlocked.String(), DisplayedStatus: "actions.status.blocked"},
{Status: int(StatusCancelling), StatusName: StatusCancelling.String(), DisplayedStatus: "actions.status.cancelling"}, {Status: int(StatusCancelling), StatusName: StatusCancelling.String(), DisplayedStatus: "actions.status.cancelling"},
}, statusInfoList) }, statusInfoList)
} }

View File

@@ -75,8 +75,28 @@ type ActionRunner struct {
const ( const (
RunnerOfflineTime = time.Minute RunnerOfflineTime = time.Minute
RunnerIdleTime = 10 * time.Second RunnerIdleTime = 10 * time.Second
// RunnerHeartbeatInterval is how often last_online is persisted on poll.
// Must stay well below RunnerOfflineTime so runners don't flap to offline.
RunnerHeartbeatInterval = 30 * time.Second
// RunnerActiveInterval is how often last_active is persisted while a runner
// streams task updates and logs. Must stay well below RunnerIdleTime so a
// busy runner keeps showing ACTIVE without a DB write on every RPC.
RunnerActiveInterval = 5 * time.Second
) )
// ShouldPersistLastOnline reports whether last_online is stale enough to be
// worth writing back. Avoids a DB write on every runner poll.
func ShouldPersistLastOnline(last timeutil.TimeStamp, now time.Time) bool {
return now.Sub(last.AsTime()) >= RunnerHeartbeatInterval
}
// ShouldPersistLastActive reports whether last_active is stale enough to be
// worth writing back. Avoids a DB write on every UpdateTask/UpdateLog RPC while
// a runner is actively streaming logs.
func ShouldPersistLastActive(last timeutil.TimeStamp, now time.Time) bool {
return now.Sub(last.AsTime()) >= RunnerActiveInterval
}
// BelongsToOwnerName before calling, should guarantee that all attributes are loaded // BelongsToOwnerName before calling, should guarantee that all attributes are loaded
func (r *ActionRunner) BelongsToOwnerName() string { func (r *ActionRunner) BelongsToOwnerName() string {
if r.RepoID != 0 { if r.RepoID != 0 {
@@ -251,21 +271,24 @@ func (opts FindRunnerOptions) ToConds() builder.Cond {
} }
func (opts FindRunnerOptions) ToOrders() string { func (opts FindRunnerOptions) ToOrders() string {
// A unique tiebreaker (id) is appended so that runners sharing the same
// last_online or name keep a deterministic order across paginated queries,
// otherwise the same runner may appear on more than one page.
switch opts.Sort { switch opts.Sort {
case "online": case "online":
return "last_online DESC" return "last_online DESC, id ASC"
case "offline": case "offline":
return "last_online ASC" return "last_online ASC, id ASC"
case "alphabetically": case "alphabetically":
return "name ASC" return "name ASC, id ASC"
case "reversealphabetically": case "reversealphabetically":
return "name DESC" return "name DESC, id ASC"
case "newest": case "newest":
return "id DESC" return "id DESC"
case "oldest": case "oldest":
return "id ASC" return "id ASC"
} }
return "last_online DESC" return "last_online DESC, id ASC"
} }
// GetRunnerByUUID returns a runner via uuid // GetRunnerByUUID returns a runner via uuid

View File

@@ -0,0 +1,149 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"fmt"
"testing"
"time"
"gitea.dev/models/db"
"gitea.dev/models/unittest"
"gitea.dev/modules/timeutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestShouldPersistLastOnline(t *testing.T) {
now := time.Now()
tests := []struct {
name string
last timeutil.TimeStamp
want bool
}{
{
name: "fresh, skip write",
last: timeutil.TimeStamp(now.Add(-5 * time.Second).Unix()),
want: false,
},
{
name: "exactly at interval, write",
last: timeutil.TimeStamp(now.Add(-RunnerHeartbeatInterval).Unix()),
want: true,
},
{
name: "stale, write",
last: timeutil.TimeStamp(now.Add(-2 * RunnerHeartbeatInterval).Unix()),
want: true,
},
{
name: "zero (never seen), write",
last: 0,
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, ShouldPersistLastOnline(tt.last, now))
})
}
}
func TestShouldPersistLastActive(t *testing.T) {
now := time.Now()
tests := []struct {
name string
last timeutil.TimeStamp
want bool
}{
{
name: "fresh, skip write",
last: timeutil.TimeStamp(now.Add(-1 * time.Second).Unix()),
want: false,
},
{
name: "exactly at interval, write",
last: timeutil.TimeStamp(now.Add(-RunnerActiveInterval).Unix()),
want: true,
},
{
name: "stale, write",
last: timeutil.TimeStamp(now.Add(-2 * RunnerActiveInterval).Unix()),
want: true,
},
{
name: "zero (never seen), write",
last: 0,
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, ShouldPersistLastActive(tt.last, now))
})
}
}
func TestFindRunnerOptions_ToOrders_StableTiebreaker(t *testing.T) {
// Sorts on a non-unique column must end with the unique id tiebreaker so
// pagination is deterministic; without it, runners sharing the same
// last_online or name can appear on more than one page. Sorts already on
// the unique id need no tiebreaker.
expected := map[string]string{
"": "last_online DESC, id ASC",
"online": "last_online DESC, id ASC",
"offline": "last_online ASC, id ASC",
"alphabetically": "name ASC, id ASC",
"reversealphabetically": "name DESC, id ASC",
"newest": "id DESC",
"oldest": "id ASC",
}
for sort, want := range expected {
assert.Equal(t, want, FindRunnerOptions{Sort: sort}.ToOrders(), "sort %q", sort)
}
}
func TestFindRunners_PaginationNoDuplicates(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
// Create several runners that all share the same last_online value so the
// primary sort key (last_online) is tied for all of them.
const ownerID = 1000
const count = 6
for i := range count {
runner := &ActionRunner{
Name: "paginated-runner",
UUID: fmt.Sprintf("PAGINATE-TEST-0000-0000-00000000000%d", i),
TokenHash: fmt.Sprintf("paginate-test-token-hash-%d", i),
OwnerID: ownerID,
RepoID: 0,
LastOnline: 42,
}
require.NoError(t, db.Insert(ctx, runner))
}
// Page through the runners and ensure every id is returned exactly once.
seen := make(map[int64]int)
const pageSize = 2
for page := 1; ; page++ {
runners, err := db.Find[ActionRunner](ctx, FindRunnerOptions{
ListOptions: db.ListOptions{Page: page, PageSize: pageSize},
OwnerID: ownerID,
})
require.NoError(t, err)
if len(runners) == 0 {
break
}
for _, r := range runners {
seen[r.ID]++
}
}
assert.Len(t, seen, count, "each runner should be returned exactly once across all pages")
for id, n := range seen {
assert.Equal(t, 1, n, "runner %d appeared on %d pages", id, n)
}
}

View File

@@ -16,6 +16,7 @@ import (
"gitea.dev/models/db" "gitea.dev/models/db"
"gitea.dev/models/unit" "gitea.dev/models/unit"
"gitea.dev/modules/actions/jobparser" "gitea.dev/modules/actions/jobparser"
"gitea.dev/modules/globallock"
"gitea.dev/modules/log" "gitea.dev/modules/log"
"gitea.dev/modules/setting" "gitea.dev/modules/setting"
"gitea.dev/modules/timeutil" "gitea.dev/modules/timeutil"
@@ -231,6 +232,11 @@ func makeTaskStepDisplayName(step *jobparser.Step, limit int) (name string) {
// another runner won the optimistic-lock race; it is never returned to callers. // another runner won the optimistic-lock race; it is never returned to callers.
var errJobAlreadyClaimed = errors.New("job already claimed by another runner") var errJobAlreadyClaimed = errors.New("job already claimed by another runner")
// pickTaskBatchSize bounds how many waiting jobs each CreateTaskForRunner query loads,
// so a large backlog is not fetched into memory on every runner poll.
// It is a var only so tests can shrink it to exercise pagination cheaply.
var pickTaskBatchSize = 100
// CreateTaskForRunner finds a waiting job that matches the runner's labels and // CreateTaskForRunner finds a waiting job that matches the runner's labels and
// atomically claims it. It iterates through all matching jobs so that a // atomically claims it. It iterates through all matching jobs so that a
// concurrent claim by another runner (which would lose the optimistic lock on // concurrent claim by another runner (which would lose the optimistic lock on
@@ -249,17 +255,30 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
Join("INNER", "repo_unit", "`repository`.id = `repo_unit`.repo_id"). Join("INNER", "repo_unit", "`repository`.id = `repo_unit`.repo_id").
Where(builder.Eq{"`repository`.owner_id": runner.OwnerID, "`repo_unit`.type": unit.TypeActions})) Where(builder.Eq{"`repository`.owner_id": runner.OwnerID, "`repo_unit`.type": unit.TypeActions}))
} }
if jobCond.IsValid() { baseCond := builder.Eq{"task_id": 0, "status": StatusWaiting, "is_reusable_caller": false}.And(jobCond)
jobCond = builder.In("run_id", builder.Select("id").From("action_run").Where(jobCond))
}
var jobs []*ActionRunJob
if err := e.Where("task_id=? AND status=? AND is_reusable_caller=?", 0, StatusWaiting, false).And(jobCond).Asc("updated", "id").Find(&jobs); err != nil {
return nil, false, err
}
// TODO: a more efficient way to filter labels // TODO: a more efficient way to filter labels
log.Trace("runner labels: %v", runner.AgentLabels) log.Trace("runner labels: %v", runner.AgentLabels)
// Page through the waiting jobs oldest-first instead of loading the whole backlog into memory on every poll.
// Keyset pagination on (updated, id) is safe under concurrent claims:
// updated only moves forward, so the advancing cursor never skips a still-waiting job even as claimed jobs drop out.
var cursorUpdated timeutil.TimeStamp
var cursorID int64
for {
cond := baseCond
if cursorID > 0 {
cond = cond.And(builder.Or(
builder.Gt{"updated": cursorUpdated},
builder.And(builder.Eq{"updated": cursorUpdated}, builder.Gt{"id": cursorID}),
))
}
var jobs []*ActionRunJob
if err := e.Where(cond).Asc("updated", "id").Limit(pickTaskBatchSize).Find(&jobs); err != nil {
return nil, false, err
}
for _, v := range jobs { for _, v := range jobs {
if !runner.CanMatchLabels(v.RunsOn) { if !runner.CanMatchLabels(v.RunsOn) {
continue continue
@@ -273,7 +292,14 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
} }
// Another runner claimed this job concurrently; try the next one. // Another runner claimed this job concurrently; try the next one.
} }
// A short page means no waiting jobs remain beyond it.
if len(jobs) < pickTaskBatchSize {
return nil, false, nil return nil, false, nil
}
last := jobs[len(jobs)-1]
cursorUpdated, cursorID = last.Updated, last.ID
}
} }
// claimJobForRunner attempts to atomically claim job for runner inside its own // claimJobForRunner attempts to atomically claim job for runner inside its own
@@ -413,6 +439,18 @@ func UpdateTask(ctx context.Context, task *ActionTask, cols ...string) error {
return err return err
} }
func getRunIDByTaskID(ctx context.Context, taskID int64) (runID int64, _ error) {
if has, err := db.GetEngine(ctx).Cols("action_run_job.run_id").
Table("action_task").
Join("INNER", "action_run_job", "action_run_job.id = action_task.job_id").
Where(builder.Eq{"action_task.id": taskID}).Get(&runID); err != nil {
return runID, err
} else if !has {
return runID, util.ErrNotExist
}
return runID, nil
}
// UpdateTaskByState updates the task by the state. // UpdateTaskByState updates the task by the state.
// It will always update the task if the state is not final, even there is no change. // It will always update the task if the state is not final, even there is no change.
// So it will update ActionTask.Updated to avoid the task being judged as a zombie task. // So it will update ActionTask.Updated to avoid the task being judged as a zombie task.
@@ -422,21 +460,26 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task
stepStates[v.Id] = v stepStates[v.Id] = v
} }
return db.WithTx2(ctx, func(ctx context.Context) (*ActionTask, error) { // Only one request can update the task because the final state needs to be calculated with all job states.
e := db.GetEngine(ctx) // Otherwise, concurrent requests with transaction will make the SQL read stale job state and result in wrong final state.
taskID := state.Id
task := &ActionTask{} runID, err := getRunIDByTaskID(ctx, taskID)
if has, err := e.ID(state.Id).Get(task); err != nil { if err != nil {
return nil, err return nil, err
}
task := &ActionTask{}
err = globallock.LockAndDo(ctx, fmt.Sprintf("UpdateTaskByState-run-%d", runID), func(ctx context.Context) error {
if has, err := db.GetEngine(ctx).ID(taskID).Get(task); err != nil {
return err
} else if !has { } else if !has {
return nil, util.ErrNotExist return util.ErrNotExist
} else if runnerID != task.RunnerID { } else if runnerID != task.RunnerID {
return nil, errors.New("invalid runner for task") return errors.New("invalid runner for task")
} }
if task.Status.IsDone() { if task.Status.IsDone() {
// the state is final, do nothing // the state is final, do nothing
return task, nil return nil
} }
// state.Result is not unspecified means the task is finished // state.Result is not unspecified means the task is finished
@@ -449,7 +492,7 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task
} }
task.Stopped = timeutil.TimeStamp(state.StoppedAt.AsTime().Unix()) task.Stopped = timeutil.TimeStamp(state.StoppedAt.AsTime().Unix())
if err := UpdateTask(ctx, task, "status", "stopped"); err != nil { if err := UpdateTask(ctx, task, "status", "stopped"); err != nil {
return nil, err return err
} }
if _, err := UpdateRunJob(ctx, &ActionRunJob{ if _, err := UpdateRunJob(ctx, &ActionRunJob{
ID: task.JobID, ID: task.JobID,
@@ -457,18 +500,18 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task
Status: task.Status, Status: task.Status,
Stopped: task.Stopped, Stopped: task.Stopped,
}, nil, "status", "stopped"); err != nil { }, nil, "status", "stopped"); err != nil {
return nil, err return err
} }
} else { } else {
// Force update ActionTask.Updated to avoid the task being judged as a zombie task // Force update ActionTask.Updated to avoid the task being judged as a zombie task
task.Updated = timeutil.TimeStampNow() task.Updated = timeutil.TimeStampNow()
if err := UpdateTask(ctx, task, "updated"); err != nil { if err := UpdateTask(ctx, task, "updated"); err != nil {
return nil, err return err
} }
} }
if err := task.LoadAttributes(ctx); err != nil { if err := task.LoadAttributes(ctx); err != nil {
return nil, err return err
} }
for _, step := range task.Steps { for _, step := range task.Steps {
@@ -485,13 +528,13 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task
} else if step.Started != 0 { } else if step.Started != 0 {
step.Status = StatusRunning step.Status = StatusRunning
} }
if _, err := e.ID(step.ID).Update(step); err != nil { if _, err := db.GetEngine(ctx).ID(step.ID).Update(step); err != nil {
return nil, err return err
} }
} }
return nil
return task, nil
}) })
return task, err
} }
func StopTask(ctx context.Context, taskID int64, status Status) error { func StopTask(ctx context.Context, taskID int64, status Status) error {

View File

@@ -371,3 +371,74 @@ func TestReleaseTaskForRunner(t *testing.T) {
unittest.AssertNotExistsBean(t, &ActionTask{ID: task.ID}) unittest.AssertNotExistsBean(t, &ActionTask{ID: task.ID})
unittest.AssertNotExistsBean(t, &ActionTaskStep{TaskID: task.ID}) unittest.AssertNotExistsBean(t, &ActionTaskStep{TaskID: task.ID})
} }
// TestCreateTaskForRunnerPagination verifies that a job sitting beyond the first page is still claimed
func TestCreateTaskForRunnerPagination(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
defer func(orig int) { pickTaskBatchSize = orig }(pickTaskBatchSize)
pickTaskBatchSize = 2
run := &ActionRun{
Title: "pagination-test-run",
RepoID: 1,
OwnerID: 2,
WorkflowID: "test.yaml",
Index: 9903,
TriggerUserID: 2,
Ref: "refs/heads/main",
CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0",
Event: "push",
TriggerEvent: "push",
Status: StatusWaiting,
}
require.NoError(t, db.Insert(t.Context(), run))
// Five waiting jobs the runner cannot run, then one it can.
// With a page size of 2 the matching job only appears on the third page.
for i := range 5 {
mismatch := &ActionRunJob{
RunID: run.ID,
RepoID: run.RepoID,
OwnerID: run.OwnerID,
CommitSHA: run.CommitSHA,
Name: "mismatch-" + string(rune('a'+i)),
Attempt: 1,
JobID: "mismatch-" + string(rune('a'+i)),
Status: StatusWaiting,
RunsOn: []string{"windows-latest"},
}
require.NoError(t, db.Insert(t.Context(), mismatch))
}
target := &ActionRunJob{
RunID: run.ID,
RepoID: run.RepoID,
OwnerID: run.OwnerID,
CommitSHA: run.CommitSHA,
Name: "target-job",
Attempt: 1,
JobID: "target-job",
Status: StatusWaiting,
RunsOn: []string{"ubuntu-latest"},
WorkflowPayload: []byte("on: push\njobs:\n target-job:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n"),
}
require.NoError(t, db.Insert(t.Context(), target))
runner := &ActionRunner{
UUID: "pagination-runner-uuid",
Name: "pagination-runner",
AgentLabels: []string{"ubuntu-latest"},
}
runner.GenerateAndFillToken()
require.NoError(t, db.Insert(t.Context(), runner))
task, ok, err := CreateTaskForRunner(t.Context(), runner)
require.NoError(t, err)
require.True(t, ok)
require.NotNil(t, task)
claimed := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: target.ID})
assert.Equal(t, StatusRunning, claimed.Status)
assert.Equal(t, task.ID, claimed.TaskID)
}

View File

@@ -36,7 +36,18 @@ import (
const ssh2keyStart = "---- BEGIN SSH2 PUBLIC KEY ----" const ssh2keyStart = "---- BEGIN SSH2 PUBLIC KEY ----"
const (
// the longest RSA key ssh-keygen allows to generate is 16384 bits (2048 bytes), we still relax the limit a little here
maxKeyBinaryBytes = 4096
maxKeyContentBase64Bytes = maxKeyBinaryBytes * 4 / 3
maxKeyContentExtraBytes = 4 * 1024 // header, footer, comment
maxKeyContentBytes = maxKeyContentBase64Bytes + maxKeyContentExtraBytes
)
func extractTypeFromBase64Key(key string) (string, error) { func extractTypeFromBase64Key(key string) (string, error) {
if len(key) > maxKeyContentBase64Bytes {
return "", util.NewInvalidArgumentErrorf("SSH public key base64 is too long")
}
b, err := base64.StdEncoding.DecodeString(key) b, err := base64.StdEncoding.DecodeString(key)
if err != nil || len(b) < 4 { if err != nil || len(b) < 4 {
return "", fmt.Errorf("invalid key format: %w", err) return "", fmt.Errorf("invalid key format: %w", err)
@@ -52,6 +63,10 @@ func extractTypeFromBase64Key(key string) (string, error) {
// parseKeyString parses any key string in OpenSSH or SSH2 format to clean OpenSSH string (RFC4253). // parseKeyString parses any key string in OpenSSH or SSH2 format to clean OpenSSH string (RFC4253).
func parseKeyString(content string) (string, error) { func parseKeyString(content string) (string, error) {
if len(content) > maxKeyContentBytes {
return "", util.NewInvalidArgumentErrorf("SSH public key content is too long")
}
// remove whitespace at start and end // remove whitespace at start and end
content = strings.TrimSpace(content) content = strings.TrimSpace(content)
@@ -63,6 +78,8 @@ func parseKeyString(content string) (string, error) {
// Transform all legal line endings to a single "\n". // Transform all legal line endings to a single "\n".
content = strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(content) content = strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(content)
var b strings.Builder
b.Grow(len(content))
lines := strings.Split(content, "\n") lines := strings.Split(content, "\n")
continuationLine := false continuationLine := false
@@ -74,9 +91,10 @@ func parseKeyString(content string) (string, error) {
if continuationLine || strings.ContainsAny(line, ":-") { if continuationLine || strings.ContainsAny(line, ":-") {
continuationLine = strings.HasSuffix(line, "\\") continuationLine = strings.HasSuffix(line, "\\")
} else { } else {
keyContent += line b.WriteString(line)
} }
} }
keyContent = b.String()
t, err := extractTypeFromBase64Key(keyContent) t, err := extractTypeFromBase64Key(keyContent)
if err != nil { if err != nil {

View File

@@ -473,10 +473,20 @@ func runErr(t *testing.T, stdin []byte, args ...string) {
} }
} }
func Test_PublicKeysAreExternallyManaged(t *testing.T) { func TestPublicKeysAreExternallyManaged(t *testing.T) {
key1 := unittest.AssertExistsAndLoadBean(t, &PublicKey{ID: 1}) key1 := unittest.AssertExistsAndLoadBean(t, &PublicKey{ID: 1})
externals, err := PublicKeysAreExternallyManaged(t.Context(), []*PublicKey{key1}) externals, err := PublicKeysAreExternallyManaged(t.Context(), []*PublicKey{key1})
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, externals, 1) assert.Len(t, externals, 1)
assert.False(t, externals[0]) assert.False(t, externals[0])
} }
// TestCheckPublicKeyStringOversized tests if oversized SSH2 public key strings are rejected before triggering costly operations.
func TestCheckPublicKeyStringOversized(t *testing.T) {
_, err := parseKeyString(strings.Repeat("a", maxKeyContentBytes+1))
assert.ErrorContains(t, err, "SSH public key content is too long")
content := "---- BEGIN SSH2 PUBLIC KEY ----\n" + strings.Repeat("a", maxKeyContentBase64Bytes+1) + "\n--- END SSH2 PUBLIC KEY ----"
_, err = parseKeyString(content)
assert.ErrorContains(t, err, "SSH public key base64 is too long")
}

View File

@@ -304,6 +304,36 @@ func (s AccessTokenScope) PublicOnly() (bool, error) {
return bitmap.hasScope(AccessTokenScopePublicOnly) return bitmap.hasScope(AccessTokenScopePublicOnly)
} }
// CanCreateChildScope reports whether a request authenticated by this (parent) scope may mint a token
// carrying the child scope. It rejects any grantable scope the parent does not hold, closing the
// scope-escalation path. public-only is a restriction rather than a grantable permission, so it is
// ignored here (a child may always be public-only); EnforcePublicOnlyFrom handles carrying it down.
func (s AccessTokenScope) CanCreateChildScope(child AccessTokenScope) (bool, error) {
requested := child.StringSlice()
scopes := make([]AccessTokenScope, 0, len(requested))
for _, sc := range requested {
childScope := AccessTokenScope(sc)
if childScope == AccessTokenScopePublicOnly {
continue
}
scopes = append(scopes, childScope)
}
return s.HasScope(scopes...)
}
// EnforcePublicOnlyFrom adds the public-only restriction to s when the authorizing parent scope is
// public-only, so a public-only token cannot mint a child token that drops the restriction.
func (s AccessTokenScope) EnforcePublicOnlyFrom(parent AccessTokenScope) (AccessTokenScope, error) {
publicOnly, err := parent.PublicOnly()
if err != nil {
return "", err
}
if !publicOnly {
return s, nil
}
return AccessTokenScope(string(s) + "," + string(AccessTokenScopePublicOnly)).Normalize()
}
// HasScope returns true if the string has the given scope // HasScope returns true if the string has the given scope
func (s AccessTokenScope) HasScope(scopes ...AccessTokenScope) (bool, error) { func (s AccessTokenScope) HasScope(scopes ...AccessTokenScope) (bool, error) {
bitmap, err := s.parse() bitmap, err := s.parse()

View File

@@ -89,3 +89,26 @@ func TestAccessTokenScope_HasScope(t *testing.T) {
}) })
} }
} }
func TestAccessTokenScope_EnforcePublicOnlyFrom(t *testing.T) {
tests := []struct {
in AccessTokenScope
parent AccessTokenScope
out AccessTokenScope
}{
// public-only parent forces the restriction onto the minted scope
{"write:user", "write:user,public-only", "public-only,write:user"},
// already public-only stays public-only
{"public-only,read:user", "public-only", "public-only,read:user"},
// non-public-only parent leaves the scope untouched
{"write:user", "write:user", "write:user"},
{"all", "all", "all"},
}
for _, test := range tests {
t.Run(string(test.parent)+"->"+string(test.in), func(t *testing.T) {
got, err := test.in.EnforcePublicOnlyFrom(test.parent)
assert.NoError(t, err)
assert.Equal(t, test.out, got)
})
}
}

View File

@@ -195,3 +195,18 @@ func HasTwoFactorOrWebAuthn(ctx context.Context, id int64) (bool, error) {
} }
return HasWebAuthnRegistrationsByUID(ctx, id) return HasWebAuthnRegistrationsByUID(ctx, id)
} }
// DisableTwoFactor removes every two-factor method of the given user atomically,
// returning the number of TOTP records and WebAuthn credentials removed.
// It is a no-op for a user that has no 2FA enrolled.
func DisableTwoFactor(ctx context.Context, uid int64) (totp, webAuthn int64, err error) {
err = db.WithTx(ctx, func(ctx context.Context) error {
var e error
if totp, e = db.GetEngine(ctx).Where("uid = ?", uid).Delete(&TwoFactor{}); e != nil {
return e
}
webAuthn, e = db.GetEngine(ctx).Where("user_id = ?", uid).Delete(&WebAuthnCredential{})
return e
})
return totp, webAuthn, err
}

View File

@@ -10,6 +10,7 @@ import (
auth_model "gitea.dev/models/auth" auth_model "gitea.dev/models/auth"
"gitea.dev/models/unittest" "gitea.dev/models/unittest"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/pquerna/otp/totp" "github.com/pquerna/otp/totp"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -45,3 +46,37 @@ func TestTwoFactorValidateAndConsumeTOTP(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.False(t, ok) assert.False(t, ok)
} }
func TestDisableTwoFactor(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
const uid = 1000 // a uid with no user/2FA fixtures
// Enroll TOTP and register a WebAuthn credential.
tfa := &auth_model.TwoFactor{UID: uid}
require.NoError(t, tfa.SetSecret("test-secret"))
require.NoError(t, auth_model.NewTwoFactor(ctx, tfa))
_, err := auth_model.CreateCredential(ctx, uid, "test-key", &webauthn.Credential{ID: []byte("test-cred-id")})
require.NoError(t, err)
has, err := auth_model.HasTwoFactorOrWebAuthn(ctx, uid)
require.NoError(t, err)
require.True(t, has)
// Both records are removed and counted separately.
totp, webAuthn, err := auth_model.DisableTwoFactor(ctx, uid)
require.NoError(t, err)
assert.EqualValues(t, 1, totp)
assert.EqualValues(t, 1, webAuthn)
has, err = auth_model.HasTwoFactorOrWebAuthn(ctx, uid)
require.NoError(t, err)
assert.False(t, has)
// A second call on a user without 2FA is a no-op.
totp, webAuthn, err = auth_model.DisableTwoFactor(ctx, uid)
require.NoError(t, err)
assert.EqualValues(t, 0, totp)
assert.EqualValues(t, 0, webAuthn)
}

View File

@@ -27,6 +27,7 @@ import (
"gitea.dev/modules/markup" "gitea.dev/modules/markup"
"gitea.dev/modules/optional" "gitea.dev/modules/optional"
"gitea.dev/modules/references" "gitea.dev/modules/references"
"gitea.dev/modules/setting"
"gitea.dev/modules/structs" "gitea.dev/modules/structs"
"gitea.dev/modules/timeutil" "gitea.dev/modules/timeutil"
"gitea.dev/modules/translation" "gitea.dev/modules/translation"
@@ -626,11 +627,18 @@ func UpdateCommentAttachments(ctx context.Context, c *Comment, uuids []string) e
return nil return nil
} }
return db.WithTx(ctx, func(ctx context.Context) error { return db.WithTx(ctx, func(ctx context.Context) error {
issue, err := GetIssueByID(ctx, c.IssueID)
if err != nil {
return err
}
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids) attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids)
if err != nil { if err != nil {
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err) return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err)
} }
for i := range attachments { for i := range attachments {
if err := validateAttachmentForIssue(ctx, issue, attachments[i]); err != nil {
return err
}
attachments[i].IssueID = c.IssueID attachments[i].IssueID = c.IssueID
attachments[i].CommentID = c.ID attachments[i].CommentID = c.ID
if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil {
@@ -643,36 +651,18 @@ func UpdateCommentAttachments(ctx context.Context, c *Comment, uuids []string) e
} }
// LoadAssigneeUserAndTeam if comment.Type is CommentTypeAssignees, then load assignees // LoadAssigneeUserAndTeam if comment.Type is CommentTypeAssignees, then load assignees
func (c *Comment) LoadAssigneeUserAndTeam(ctx context.Context) error { func (c *Comment) LoadAssigneeUserAndTeam(ctx context.Context) (err error) {
var err error
if c.AssigneeID > 0 && c.Assignee == nil { if c.AssigneeID > 0 && c.Assignee == nil {
c.Assignee, err = user_model.GetUserByID(ctx, c.AssigneeID) _, c.Assignee, err = user_model.GetPossibleUserByID(ctx, c.AssigneeID)
if err != nil { if err != nil {
if !user_model.IsErrUserNotExist(err) {
return err return err
} }
c.Assignee = user_model.NewGhostUser()
} }
} else if c.AssigneeTeamID > 0 && c.AssigneeTeam == nil { if c.AssigneeTeamID > 0 && c.AssigneeTeam == nil {
if err = c.LoadIssue(ctx); err != nil { _, c.AssigneeTeam, err = organization.GetPossibleTeamByID(ctx, c.AssigneeTeamID)
if err != nil {
return err return err
} }
if err = c.Issue.LoadRepo(ctx); err != nil {
return err
}
if err = c.Issue.Repo.LoadOwner(ctx); err != nil {
return err
}
if c.Issue.Repo.Owner.IsOrganization() {
c.AssigneeTeam, err = organization.GetTeamByID(ctx, c.AssigneeTeamID)
if err != nil && !organization.IsErrTeamNotExist(err) {
return err
}
}
} }
return nil return nil
} }
@@ -795,8 +785,7 @@ func (c *Comment) MetaSpecialDoerTr(locale translation.Locale) template.HTML {
} }
func (c *Comment) TimelineRequestedReviewTr(locale translation.Locale, createdStr template.HTML) template.HTML { func (c *Comment) TimelineRequestedReviewTr(locale translation.Locale, createdStr template.HTML) template.HTML {
if c.AssigneeID > 0 { if c.Assignee != nil {
// it guarantees LoadAssigneeUserAndTeam has been called, and c.Assignee is Ghost user but not nil if the user doesn't exist
if c.RemovedAssignee { if c.RemovedAssignee {
if c.PosterID == c.AssigneeID { if c.PosterID == c.AssigneeID {
return locale.Tr("repo.issues.review.remove_review_request_self", createdStr) return locale.Tr("repo.issues.review.remove_review_request_self", createdStr)
@@ -805,14 +794,20 @@ func (c *Comment) TimelineRequestedReviewTr(locale translation.Locale, createdSt
} }
return locale.Tr("repo.issues.review.add_review_request", c.Assignee.GetDisplayName(), createdStr) return locale.Tr("repo.issues.review.add_review_request", c.Assignee.GetDisplayName(), createdStr)
} }
teamName := "Ghost Team"
if c.AssigneeTeam != nil { if c.AssigneeTeam != nil {
teamName = c.AssigneeTeam.Name
}
if c.RemovedAssignee { if c.RemovedAssignee {
return locale.Tr("repo.issues.review.remove_review_request", teamName, createdStr) return locale.Tr("repo.issues.review.remove_review_request", c.AssigneeTeam.Name, createdStr)
} }
return locale.Tr("repo.issues.review.add_review_request", teamName, createdStr) return locale.Tr("repo.issues.review.add_review_request", c.AssigneeTeam.Name, createdStr)
}
// impossible fallback
assigneePrompt := fmt.Sprintf("(AssigneeID=%d, AssigneeTeamID=%d)", c.AssigneeID, c.AssigneeTeam.ID)
setting.PanicInDevOrTesting("unknown timeline pull request review event comment: id=%d, %s", c.ID, assigneePrompt)
if c.RemovedAssignee {
return locale.Tr("repo.issues.review.remove_review_request", assigneePrompt, createdStr)
}
return locale.Tr("repo.issues.review.add_review_request", assigneePrompt, createdStr)
} }
// CreateComment creates comment with context // CreateComment creates comment with context

View File

@@ -45,11 +45,26 @@ func TestCreateComment(t *testing.T) {
unittest.AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix)) unittest.AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix))
} }
func TestLoadAssigneeUserAndTeam_DeletedTeamBecomesGhostTeam(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 15})
comment := &issues_model.Comment{
Type: issues_model.CommentTypeAssignees,
IssueID: issue.ID,
AssigneeTeamID: 999999, // non-existing team ID
}
assert.NoError(t, comment.LoadAssigneeUserAndTeam(t.Context()))
assert.NotNil(t, comment.AssigneeTeam)
assert.EqualValues(t, -1, comment.AssigneeTeam.ID)
}
func Test_UpdateCommentAttachment(t *testing.T) { func Test_UpdateCommentAttachment(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1}) comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
attachment := repo_model.Attachment{ attachment := repo_model.Attachment{
RepoID: issue.RepoID, // must match the comment's repo, else the cross-repo guard rejects it
Name: "test.txt", Name: "test.txt",
} }
assert.NoError(t, db.Insert(t.Context(), &attachment)) assert.NoError(t, db.Insert(t.Context(), &attachment))

View File

@@ -263,14 +263,46 @@ func AddDeletePRBranchComment(ctx context.Context, doer *user_model.User, repo *
return err return err
} }
// validateAttachmentForIssue rejects a foreign or already-linked attachment before it is linked to
// issue: a known UUID could otherwise re-link (and expose) another repo's private attachment. A
// legacy attachment predating repo_id-on-upload is adopted into the issue's repo.
func validateAttachmentForIssue(ctx context.Context, issue *Issue, attachment *repo_model.Attachment) error {
if attachment.RepoID == 0 && attachment.CreatedUnix < repo_model.LegacyAttachmentMissingRepoIDCutoff {
attachment.RepoID = issue.RepoID
if err := repo_model.UpdateAttachmentByUUID(ctx, attachment, "repo_id"); err != nil {
return fmt.Errorf("update attachment repo_id [id: %d]: %w", attachment.ID, err)
}
}
if attachment.RepoID != issue.RepoID {
return util.NewPermissionDeniedErrorf("attachment belongs to a different repository")
}
if attachment.IssueID != 0 && attachment.IssueID != issue.ID {
return util.NewPermissionDeniedErrorf("attachment is already linked to another issue")
}
if attachment.ReleaseID != 0 {
return util.NewPermissionDeniedErrorf("attachment is already linked to a release")
}
return nil
}
// UpdateIssueAttachments update attachments by UUIDs for the issue // UpdateIssueAttachments update attachments by UUIDs for the issue
func UpdateIssueAttachments(ctx context.Context, issueID int64, uuids []string) (err error) { func UpdateIssueAttachments(ctx context.Context, issueID int64, uuids []string) (err error) {
if len(uuids) == 0 {
return nil
}
return db.WithTx(ctx, func(ctx context.Context) error { return db.WithTx(ctx, func(ctx context.Context) error {
issue, err := GetIssueByID(ctx, issueID)
if err != nil {
return err
}
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids) attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids)
if err != nil { if err != nil {
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err) return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err)
} }
for i := range attachments { for i := range attachments {
if err := validateAttachmentForIssue(ctx, issue, attachments[i]); err != nil {
return err
}
attachments[i].IssueID = issueID attachments[i].IssueID = issueID
if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil {
return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err)

View File

@@ -0,0 +1,33 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package issues_test
import (
"testing"
issues_model "gitea.dev/models/issues"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
"gitea.dev/modules/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUpdateIssueAttachmentsCrossRepo(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
// attachment id 2 belongs to repo 2 / issue 4; issue 1 lives in repo 1
issue1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
foreign := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 2})
require.NotEqual(t, issue1.RepoID, foreign.RepoID)
// re-linking a foreign repo's attachment by UUID must be rejected
err := issues_model.UpdateIssueAttachments(t.Context(), issue1.ID, []string{foreign.UUID})
assert.ErrorIs(t, err, util.ErrPermissionDenied)
// the foreign attachment must be left untouched
reloaded := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 2})
assert.Equal(t, foreign.IssueID, reloaded.IssueID)
}

View File

@@ -6,6 +6,7 @@ package issues
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"slices" "slices"
"strconv" "strconv"
@@ -27,12 +28,6 @@ type ErrRepoLabelNotExist struct {
RepoID int64 RepoID int64
} }
// IsErrRepoLabelNotExist checks if an error is a RepoErrLabelNotExist.
func IsErrRepoLabelNotExist(err error) bool {
_, ok := err.(ErrRepoLabelNotExist)
return ok
}
func (err ErrRepoLabelNotExist) Error() string { func (err ErrRepoLabelNotExist) Error() string {
return fmt.Sprintf("label does not exist [label_id: %d, repo_id: %d]", err.LabelID, err.RepoID) return fmt.Sprintf("label does not exist [label_id: %d, repo_id: %d]", err.LabelID, err.RepoID)
} }
@@ -312,6 +307,18 @@ func GetLabelInRepoByName(ctx context.Context, repoID int64, labelName string) (
return l, nil return l, nil
} }
// GetLabelInRepoOrOrgByID returns the label with labelID scoped to the repo, falling back to the
// repo's owning organization when ownerIsOrg is set. It returns ErrRepoLabelNotExist /
// ErrOrgLabelNotExist when the label is in neither scope, so a foreign-but-existing label ID is
// indistinguishable from a nonexistent one (no cross-repo enumeration oracle).
func GetLabelInRepoOrOrgByID(ctx context.Context, repoID, ownerID int64, ownerIsOrg bool, labelID int64) (*Label, error) {
label, err := GetLabelInRepoByID(ctx, repoID, labelID)
if err != nil && errors.Is(err, util.ErrNotExist) && ownerIsOrg {
return GetLabelInOrgByID(ctx, ownerID, labelID)
}
return label, err
}
// GetLabelInRepoByID returns a label by ID in given repository. // GetLabelInRepoByID returns a label by ID in given repository.
func GetLabelInRepoByID(ctx context.Context, repoID, labelID int64) (*Label, error) { func GetLabelInRepoByID(ctx context.Context, repoID, labelID int64) (*Label, error) {
if labelID <= 0 || repoID <= 0 { if labelID <= 0 || repoID <= 0 {

View File

@@ -12,6 +12,7 @@ import (
"gitea.dev/models/unittest" "gitea.dev/models/unittest"
user_model "gitea.dev/models/user" user_model "gitea.dev/models/user"
"gitea.dev/modules/timeutil" "gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -94,10 +95,10 @@ func TestGetLabelInRepoByName(t *testing.T) {
assert.Equal(t, "label1", label.Name) assert.Equal(t, "label1", label.Name)
_, err = issues_model.GetLabelInRepoByName(t.Context(), 1, "") _, err = issues_model.GetLabelInRepoByName(t.Context(), 1, "")
assert.True(t, issues_model.IsErrRepoLabelNotExist(err)) assert.ErrorIs(t, err, util.ErrNotExist)
_, err = issues_model.GetLabelInRepoByName(t.Context(), unittest.NonexistentID, "nonexistent") _, err = issues_model.GetLabelInRepoByName(t.Context(), unittest.NonexistentID, "nonexistent")
assert.True(t, issues_model.IsErrRepoLabelNotExist(err)) assert.ErrorIs(t, err, util.ErrNotExist)
} }
func TestGetLabelInRepoByNames(t *testing.T) { func TestGetLabelInRepoByNames(t *testing.T) {
@@ -131,10 +132,10 @@ func TestGetLabelInRepoByID(t *testing.T) {
assert.EqualValues(t, 1, label.ID) assert.EqualValues(t, 1, label.ID)
_, err = issues_model.GetLabelInRepoByID(t.Context(), 1, -1) _, err = issues_model.GetLabelInRepoByID(t.Context(), 1, -1)
assert.True(t, issues_model.IsErrRepoLabelNotExist(err)) assert.ErrorIs(t, err, util.ErrNotExist)
_, err = issues_model.GetLabelInRepoByID(t.Context(), unittest.NonexistentID, unittest.NonexistentID) _, err = issues_model.GetLabelInRepoByID(t.Context(), unittest.NonexistentID, unittest.NonexistentID)
assert.True(t, issues_model.IsErrRepoLabelNotExist(err)) assert.ErrorIs(t, err, util.ErrNotExist)
} }
func TestGetLabelsInRepoByIDs(t *testing.T) { func TestGetLabelsInRepoByIDs(t *testing.T) {

View File

@@ -324,6 +324,59 @@ func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organizatio
return slices.Contains(pb.ApprovalsWhitelistTeamIDs, team.ID), nil return slices.Contains(pb.ApprovalsWhitelistTeamIDs, team.ID), nil
} }
// RecalculateReviewsOfficial re-evaluates the "official" flag of the latest approve
// and reject reviews of an issue against its pull request's current base branch.
// It must be called whenever the target branch changes, otherwise an approval that
// was official on the previous (possibly unprotected) branch would keep satisfying
// the new branch's protection rules.
func RecalculateReviewsOfficial(ctx context.Context, issue *Issue) error {
if err := issue.LoadPullRequest(ctx); err != nil {
return err
}
// Clearing and restoring the official flags must happen atomically, otherwise a
// failure in between would leave the reviews without any official flag set.
return db.WithTx(ctx, func(ctx context.Context) error {
// Only the latest approve/reject review of each reviewer counts as official, so
// clear the flag on all of them first and restore it only where it still applies.
if _, err := db.GetEngine(ctx).
Where("issue_id = ?", issue.ID).
In("type", ReviewTypeApprove, ReviewTypeReject).
Cols("official").
Update(&Review{Official: false}); err != nil {
return err
}
reviews, err := FindLatestReviews(ctx, FindReviewOptions{
Types: []ReviewType{ReviewTypeApprove, ReviewTypeReject},
IssueID: issue.ID,
})
if err != nil {
return err
}
for _, review := range reviews {
if err := review.LoadReviewer(ctx); err != nil {
return err
}
if review.Reviewer == nil {
continue
}
official, err := IsOfficialReviewer(ctx, issue, review.Reviewer)
if err != nil {
return err
}
if official {
if _, err := db.GetEngine(ctx).ID(review.ID).Cols("official").Update(&Review{Official: true}); err != nil {
return err
}
}
}
return nil
})
}
// CreateReview creates a new review based on opts // CreateReview creates a new review based on opts
func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error) { func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error) {
return db.WithTx2(ctx, func(ctx context.Context) (*Review, error) { return db.WithTx2(ctx, func(ctx context.Context) (*Review, error) {

View File

@@ -6,6 +6,8 @@ package issues_test
import ( import (
"testing" "testing"
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues" issues_model "gitea.dev/models/issues"
repo_model "gitea.dev/models/repo" repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest" "gitea.dev/models/unittest"
@@ -386,3 +388,45 @@ func TestAddReviewRequest(t *testing.T) {
assert.NotNil(t, comment.CommentMetaData) assert.NotNil(t, comment.CommentMetaData)
assert.Equal(t, issues_model.SpecialDoerNameCodeOwners, comment.CommentMetaData.SpecialDoerName) assert.Equal(t, issues_model.SpecialDoerNameCodeOwners, comment.CommentMetaData.SpecialDoerName)
} }
func TestRecalculateReviewsOfficial(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
// PR #2 targets repo1's "master" branch. Simulate an approval that became
// official while the PR targeted an unprotected branch.
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
review, err := issues_model.CreateReview(t.Context(), issues_model.CreateReviewOptions{
Type: issues_model.ReviewTypeApprove,
Issue: issue,
Reviewer: reviewer,
Official: true,
})
assert.NoError(t, err)
// Protect the (now current) target branch with an approvals whitelist that
// does not include the reviewer, mirroring a retarget onto a protected branch.
rule := &git_model.ProtectedBranch{
RepoID: issue.RepoID,
RuleName: "master",
EnableApprovalsWhitelist: true,
ApprovalsWhitelistUserIDs: []int64{2},
RequiredApprovals: 1,
}
assert.NoError(t, db.Insert(t.Context(), rule))
// Re-evaluating must strip the stale official flag, otherwise the approval
// would still satisfy the protected branch's required approvals.
assert.NoError(t, issues_model.RecalculateReviewsOfficial(t.Context(), issue))
review = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: review.ID})
assert.False(t, review.Official)
// Once the reviewer is whitelisted, re-evaluating restores the official flag.
rule.ApprovalsWhitelistUserIDs = []int64{2, reviewer.ID}
_, err = db.GetEngine(t.Context()).ID(rule.ID).Cols("approvals_whitelist_user_i_ds").Update(rule)
assert.NoError(t, err)
assert.NoError(t, issues_model.RecalculateReviewsOfficial(t.Context(), issue))
review = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: review.ID})
assert.True(t, review.Official)
}

View File

@@ -6,6 +6,7 @@ package organization
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
@@ -92,6 +93,15 @@ func (t *Team) IsPublic() bool { return t.Visibility.IsPublic() }
func (t *Team) IsLimited() bool { return t.Visibility.IsLimited() } func (t *Team) IsLimited() bool { return t.Visibility.IsLimited() }
func (t *Team) IsPrivate() bool { return t.Visibility.IsPrivate() } func (t *Team) IsPrivate() bool { return t.Visibility.IsPrivate() }
const (
ghostTeamID = -1
ghostTeamName = "(deleted team)"
)
func newGhostTeam() *Team {
return &Team{ID: ghostTeamID, Name: ghostTeamName, LowerName: ghostTeamName}
}
// CanNonMemberReadMeta reports whether a non-member, non-owner doer may read // CanNonMemberReadMeta reports whether a non-member, non-owner doer may read
// the team's metadata, based on the team's visibility tier and the parent org's // the team's metadata, based on the team's visibility tier and the parent org's
// visibility. Privileged callers (site admins, org owners, team members) are // visibility. Privileged callers (site admins, org owners, team members) are
@@ -270,6 +280,17 @@ func GetTeamByID(ctx context.Context, teamID int64) (*Team, error) {
return t, nil return t, nil
} }
func GetPossibleTeamByID(ctx context.Context, teamID int64) (int64, *Team, error) {
t, err := GetTeamByID(ctx, teamID)
if errors.Is(err, util.ErrNotExist) {
t = newGhostTeam()
return t.ID, t, nil
} else if err != nil {
return 0, nil, err
}
return t.ID, t, nil
}
// IncrTeamRepoNum increases the number of repos for the given team by 1 // IncrTeamRepoNum increases the number of repos for the given team by 1
func IncrTeamRepoNum(ctx context.Context, teamID int64) error { func IncrTeamRepoNum(ctx context.Context, teamID int64) error {
_, err := db.GetEngine(ctx).Incr("num_repos").ID(teamID).Update(new(Team)) _, err := db.GetEngine(ctx).Incr("num_repos").ID(teamID).Update(new(Team))

View File

@@ -31,18 +31,28 @@ func GetOrgRepositoryIDs(ctx context.Context, orgID int64) (repoIDs []int64, _ e
type SearchTeamRepoOptions struct { type SearchTeamRepoOptions struct {
db.ListOptions db.ListOptions
TeamID int64 TeamID int64
// PublicOnly restricts the result (and count) to non-private repositories.
PublicOnly bool
}
func (opts *SearchTeamRepoOptions) toCond() builder.Cond {
cond := builder.NewCond()
if opts.TeamID > 0 {
cond = cond.And(builder.In("id",
builder.Select("repo_id").
From("team_repo").
Where(builder.Eq{"team_id": opts.TeamID}),
))
}
if opts.PublicOnly {
cond = cond.And(builder.Eq{"is_private": false})
}
return cond
} }
// GetTeamRepositories returns paginated repositories in team of organization. // GetTeamRepositories returns paginated repositories in team of organization.
func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (RepositoryList, error) { func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (RepositoryList, error) {
sess := db.GetEngine(ctx) sess := db.GetEngine(ctx).Where(opts.toCond())
if opts.TeamID > 0 {
sess = sess.In("id",
builder.Select("repo_id").
From("team_repo").
Where(builder.Eq{"team_id": opts.TeamID}),
)
}
if opts.PageSize > 0 { if opts.PageSize > 0 {
sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
} }
@@ -51,6 +61,11 @@ func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (Repo
Find(&repos) Find(&repos)
} }
// CountTeamRepositories returns the number of repositories in team of organization matching opts.
func CountTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (int64, error) {
return db.GetEngine(ctx).Where(opts.toCond()).Count(new(Repository))
}
// AccessibleReposEnvironment operations involving the repositories that are // AccessibleReposEnvironment operations involving the repositories that are
// accessible to a particular user // accessible to a particular user
type AccessibleReposEnvironment interface { type AccessibleReposEnvironment interface {

View File

@@ -310,11 +310,17 @@ func userOrgTeamRepoBuilder(userID int64) *builder.Builder {
} }
// userOrgTeamUnitRepoBuilder returns repo ids where user's teams can access the special unit. // userOrgTeamUnitRepoBuilder returns repo ids where user's teams can access the special unit.
// A team grants the unit either through an explicit team_unit row (access_mode > none) or by being an
// admin/owner team (team.authorize >= admin), which grants every unit regardless of team_unit rows —
// mirroring the HasAdminAccess() short-circuit in access.GetIndividualUserRepoPermission.
func userOrgTeamUnitRepoBuilder(userID int64, unitType unit.Type) *builder.Builder { func userOrgTeamUnitRepoBuilder(userID int64, unitType unit.Type) *builder.Builder {
return userOrgTeamRepoBuilder(userID). return userOrgTeamRepoBuilder(userID).
Join("INNER", "team_unit", "`team_unit`.team_id = `team_repo`.team_id"). Join("INNER", "team", "`team`.id = `team_repo`.team_id").
Where(builder.Eq{"`team_unit`.`type`": unitType}). Join("LEFT", "team_unit", builder.Expr("`team_unit`.team_id = `team_repo`.team_id AND `team_unit`.`type` = ?", unitType)).
And(builder.Gt{"`team_unit`.`access_mode`": int(perm.AccessModeNone)}) Where(builder.Or(
builder.Gte{"`team`.authorize": int(perm.AccessModeAdmin)},
builder.Gt{"`team_unit`.`access_mode`": int(perm.AccessModeNone)},
))
} }
// userOrgTeamUnitRepoCond returns a condition to select repo ids where user's teams can access the special unit. // userOrgTeamUnitRepoCond returns a condition to select repo ids where user's teams can access the special unit.
@@ -326,7 +332,7 @@ func userOrgTeamUnitRepoCond(idStr string, userID int64, unitType unit.Type) bui
func UserOrgUnitRepoCond(idStr string, userID, orgID int64, unitType unit.Type) builder.Cond { func UserOrgUnitRepoCond(idStr string, userID, orgID int64, unitType unit.Type) builder.Cond {
return builder.In(idStr, return builder.In(idStr,
userOrgTeamUnitRepoBuilder(userID, unitType). userOrgTeamUnitRepoBuilder(userID, unitType).
And(builder.Eq{"`team_unit`.org_id": orgID}), And(builder.Eq{"`team`.org_id": orgID}),
) )
} }
@@ -755,6 +761,40 @@ func FindUserCodeAccessibleOwnerRepoIDs(ctx context.Context, ownerID int64, user
)) ))
} }
// PublicRepoUnderPublicOwnerCond restricts to public repos whose owner is publicly visible: the
// "genuinely public" set a public-only token or an anonymous caller may see (a public repo under a
// limited/private owner is not publicly reachable and must be excluded).
func PublicRepoUnderPublicOwnerCond() builder.Cond {
return builder.And(
builder.Eq{"`repository`.is_private": false},
builder.In("`repository`.owner_id", builder.Select("id").From("`user`").Where(builder.Eq{"visibility": structs.VisibleTypePublic})),
)
}
// UserActionsAccessibleOwnerRepoCond selects the repos owned by ownerID whose Actions `user` may read.
// It is used to list an org/user's Actions runs and jobs (see the callers in routers/api/v1/shared).
// - owner_id = ownerID: only that owner's repos.
// - AccessibleRepositoryCondition(user, TypeActions): only repos whose Actions the user can read
// (admin/owner teams are handled inside it; a site admin is not, callers must skip the filter for one).
// - publicOnly (a public-only token): additionally limit to public repos under a public owner.
func UserActionsAccessibleOwnerRepoCond(ownerID int64, user *user_model.User, publicOnly bool) builder.Cond {
cond := builder.NewCond().And(
builder.Eq{"`repository`.owner_id": ownerID},
AccessibleRepositoryCondition(user, unit.TypeActions),
)
if publicOnly {
cond = cond.And(PublicRepoUnderPublicOwnerCond())
}
return cond
}
// FindUserActionsAccessibleOwnerRepoIDsSubQuery returns a subquery selecting the repository IDs the user
// can see for the given owner. Callers embed it in an `IN (...)` condition so that a large owner does not
// materialize every repo ID into the SQL statement, which could exceed database parameter limits.
func FindUserActionsAccessibleOwnerRepoIDsSubQuery(ownerID int64, user *user_model.User, publicOnly bool) *builder.Builder {
return builder.Select("id").From("repository").Where(UserActionsAccessibleOwnerRepoCond(ownerID, user, publicOnly))
}
// GetUserRepositories returns a list of repositories of given user. // GetUserRepositories returns a list of repositories of given user.
func GetUserRepositories(ctx context.Context, opts SearchRepoOptions) (RepositoryList, int64, error) { func GetUserRepositories(ctx context.Context, opts SearchRepoOptions) (RepositoryList, int64, error) {
if len(opts.OrderBy) == 0 { if len(opts.OrderBy) == 0 {

View File

@@ -9,6 +9,7 @@ import (
"gitea.dev/models/db" "gitea.dev/models/db"
repo_model "gitea.dev/models/repo" repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
"gitea.dev/models/unittest" "gitea.dev/models/unittest"
user_model "gitea.dev/models/user" user_model "gitea.dev/models/user"
"gitea.dev/modules/optional" "gitea.dev/modules/optional"
@@ -466,3 +467,50 @@ func TestSearchRepositoryByTopicName(t *testing.T) {
}) })
} }
} }
func TestFindUserActionsAccessibleOwnerRepoIDs(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
// user2 is on org3's owner team, so it can access org3's private repo3 (which has the actions unit)
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// org3 is a public org owning repo3 (private) and repo32 (public), both with the actions unit
const orgID = 3
all, err := repo_model.SearchRepositoryIDsByCondition(t.Context(), repo_model.UserActionsAccessibleOwnerRepoCond(orgID, user, false))
require.NoError(t, err)
assert.Contains(t, all, int64(3), "without public-only the private repo's actions are listed")
publicOnly, err := repo_model.SearchRepositoryIDsByCondition(t.Context(), repo_model.UserActionsAccessibleOwnerRepoCond(orgID, user, true))
require.NoError(t, err)
assert.NotContains(t, publicOnly, int64(3), "a public-only token must not list a private repo's actions")
assert.Contains(t, publicOnly, int64(32), "a public repo under a public owner stays listed")
}
// TestUserOrgUnitRepoCondTeamAuthorize pins the team.authorize behavior of userOrgTeamUnitRepoBuilder
// (exercised through UserOrgUnitRepoCond): an admin/owner team grants every unit even without an explicit
// team_unit row, while a non-admin team only grants a unit it has an explicit row for. This guards both
// directions — hiding repos from admin-team members, and over-broadening a plain team's access.
func TestUserOrgUnitRepoCondTeamAuthorize(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
accessibleRepoIDs := func(userID, orgID int64, unitType unit.Type) []int64 {
ids, err := repo_model.SearchRepositoryIDsByCondition(t.Context(),
repo_model.UserOrgUnitRepoCond("`repository`.id", userID, orgID, unitType))
require.NoError(t, err)
return ids
}
// Case A: user18 is only on org17's owner team (team5, authorize=owner), linked to the private repo24
// but with no Actions team_unit row. The owner authorize must still grant it, mirroring the runtime
// HasAdminAccess() short-circuit in access.GetIndividualUserRepoPermission.
assert.Contains(t, accessibleRepoIDs(18, 17, unit.TypeActions), int64(24),
"an owner team grants a unit it has no explicit team_unit row for")
// Cases B and C share one subject so the team_unit row is the only difference: user4 is only on org3's
// write team (team2, authorize=write, non-admin), linked to the private repo3. team2 has an explicit
// Projects row but none for Actions.
assert.Contains(t, accessibleRepoIDs(4, 3, unit.TypeProjects), int64(3),
"a non-admin team grants a unit it has an explicit team_unit row for")
assert.NotContains(t, accessibleRepoIDs(4, 3, unit.TypeActions), int64(3),
"a non-admin team must NOT grant a unit it has no team_unit row for")
}

View File

@@ -22,6 +22,9 @@ type StarredReposOptions struct {
StarrerID int64 StarrerID int64
RepoOwnerID int64 RepoOwnerID int64
IncludePrivate bool IncludePrivate bool
// Actor is the user the private repositories are gated on: a private repo is only
// returned when Actor still has access to it, even if it was starred while access was granted.
Actor *user_model.User
} }
func (opts *StarredReposOptions) ApplyPublicOnly(publicOnly bool) { func (opts *StarredReposOptions) ApplyPublicOnly(publicOnly bool) {
@@ -39,10 +42,12 @@ func (opts *StarredReposOptions) ToConds() builder.Cond {
"repository.owner_id": opts.RepoOwnerID, "repository.owner_id": opts.RepoOwnerID,
}) })
} }
if !opts.IncludePrivate { if opts.IncludePrivate {
cond = cond.And(builder.Eq{ // only include private repos the actor can still access, so metadata does not leak after access revocation
"repository.is_private": false, cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid))
}) } else {
// a public repo under a limited/private owner is not publicly reachable, so exclude it too
cond = cond.And(PublicRepoUnderPublicOwnerCond())
} }
return cond return cond
} }
@@ -66,6 +71,9 @@ type WatchedReposOptions struct {
WatcherID int64 WatcherID int64
RepoOwnerID int64 RepoOwnerID int64
IncludePrivate bool IncludePrivate bool
// Actor is the user the private repositories are gated on: a private repo is only
// returned when Actor still has access to it, even if it was watched while access was granted.
Actor *user_model.User
} }
func (opts *WatchedReposOptions) ApplyPublicOnly(publicOnly bool) { func (opts *WatchedReposOptions) ApplyPublicOnly(publicOnly bool) {
@@ -83,10 +91,12 @@ func (opts *WatchedReposOptions) ToConds() builder.Cond {
"repository.owner_id": opts.RepoOwnerID, "repository.owner_id": opts.RepoOwnerID,
}) })
} }
if !opts.IncludePrivate { if opts.IncludePrivate {
cond = cond.And(builder.Eq{ // only include private repos the actor can still access, so metadata does not leak after access revocation
"repository.is_private": false, cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid))
}) } else {
// a public repo under a limited/private owner is not publicly reachable, so exclude it too
cond = cond.And(PublicRepoUnderPublicOwnerCond())
} }
return cond.And(builder.Neq{ return cond.And(builder.Neq{
"watch.mode": WatchModeDont, "watch.mode": WatchModeDont,

View File

@@ -84,3 +84,40 @@ func testUserRepoGetIssuePostersWithSearch(t *testing.T) {
require.Len(t, users, 1) require.Len(t, users, 1)
assert.Equal(t, "user2", users[0].Name) assert.Equal(t, "user2", users[0].Name)
} }
func TestStarredWatchedReposExcludeNonPublicOwners(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
const viewerID = 2
// repo1: public repo under a public owner; repo38: public repo under a limited org (not publicly reachable)
const publicOwnerRepo, limitedOwnerRepo = 1, 38
require.NoError(t, db.Insert(t.Context(), &repo_model.Star{UID: viewerID, RepoID: publicOwnerRepo}))
require.NoError(t, db.Insert(t.Context(), &repo_model.Star{UID: viewerID, RepoID: limitedOwnerRepo}))
require.NoError(t, db.Insert(t.Context(), &repo_model.Watch{UserID: viewerID, RepoID: publicOwnerRepo, Mode: repo_model.WatchModeNormal}))
require.NoError(t, db.Insert(t.Context(), &repo_model.Watch{UserID: viewerID, RepoID: limitedOwnerRepo, Mode: repo_model.WatchModeNormal}))
listOpts := db.ListOptions{Page: 1, PageSize: 50}
starred, err := repo_model.GetStarredRepos(t.Context(), &repo_model.StarredReposOptions{
ListOptions: listOpts, StarrerID: viewerID, IncludePrivate: false,
})
require.NoError(t, err)
assert.NotContains(t, repoIDs(starred), int64(limitedOwnerRepo), "a public repo under a limited owner must be hidden from a public star listing")
assert.Contains(t, repoIDs(starred), int64(publicOwnerRepo), "a public repo under a public owner stays visible")
watched, _, err := repo_model.GetWatchedRepos(t.Context(), &repo_model.WatchedReposOptions{
ListOptions: listOpts, WatcherID: viewerID, IncludePrivate: false,
})
require.NoError(t, err)
assert.NotContains(t, repoIDs(watched), int64(limitedOwnerRepo), "a public repo under a limited owner must be hidden from a public watch listing")
assert.Contains(t, repoIDs(watched), int64(publicOwnerRepo), "a public repo under a public owner stays visible")
}
func repoIDs(repos []*repo_model.Repository) []int64 {
ids := make([]int64, len(repos))
for i, r := range repos {
ids[i] = r.ID
}
return ids
}

View File

@@ -4,6 +4,8 @@
package actions package actions
import ( import (
"context"
"gitea.dev/modules/actions/jobparser" "gitea.dev/modules/actions/jobparser"
"gitea.dev/modules/git" "gitea.dev/modules/git"
"gitea.dev/modules/log" "gitea.dev/modules/log"
@@ -13,8 +15,8 @@ import (
) )
// ListScopedWorkflows lists scoped workflow files (under SCOPED_WORKFLOW_DIRS) at the given commit. // ListScopedWorkflows lists scoped workflow files (under SCOPED_WORKFLOW_DIRS) at the given commit.
func ListScopedWorkflows(commit *git.Commit) (string, git.Entries, error) { func ListScopedWorkflows(ctx context.Context, gitRepo *git.Repository, commit *git.Commit) (string, git.Entries, error) {
return listWorkflowsInDirs(commit, setting.Actions.ScopedWorkflowDirs) return listWorkflowsInDirs(ctx, gitRepo, commit, setting.Actions.ScopedWorkflowDirs)
} }
// ParsedScopedWorkflow is one scoped workflow's source-side parse result // ParsedScopedWorkflow is one scoped workflow's source-side parse result
@@ -26,15 +28,15 @@ type ParsedScopedWorkflow struct {
} }
// ParseScopedWorkflows lists and parses the scoped workflow files at sourceCommit (under SCOPED_WORKFLOW_DIRS). // ParseScopedWorkflows lists and parses the scoped workflow files at sourceCommit (under SCOPED_WORKFLOW_DIRS).
func ParseScopedWorkflows(sourceCommit *git.Commit) ([]*ParsedScopedWorkflow, error) { func ParseScopedWorkflows(ctx context.Context, gitRepo *git.Repository, sourceCommit *git.Commit) ([]*ParsedScopedWorkflow, error) {
_, entries, err := ListScopedWorkflows(sourceCommit) _, entries, err := ListScopedWorkflows(ctx, gitRepo, sourceCommit)
if err != nil { if err != nil {
return nil, err return nil, err
} }
parsed := make([]*ParsedScopedWorkflow, 0, len(entries)) parsed := make([]*ParsedScopedWorkflow, 0, len(entries))
for _, entry := range entries { for _, entry := range entries {
content, err := GetContentFromEntry(entry) content, err := GetContentFromEntry(gitRepo, entry)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -55,29 +57,34 @@ func ParseScopedWorkflows(sourceCommit *git.Commit) ([]*ParsedScopedWorkflow, er
return parsed, nil return parsed, nil
} }
// MatchScopedWorkflows evaluates already-parsed scoped workflows against one consuming event, returning those whose `on:` matches. // MatchScopedWorkflows evaluates already-parsed scoped workflows against one consuming event.
// It returns the workflows whose `on:` matches, and those that matched the event but were excluded by a branch/paths filter (filtered).
func MatchScopedWorkflows( func MatchScopedWorkflows(
parsed []*ParsedScopedWorkflow, parsed []*ParsedScopedWorkflow,
consumerGitRepo *git.Repository, consumerGitRepo *git.Repository,
consumerCommit *git.Commit, consumerCommit *git.Commit,
triggedEvent webhook_module.HookEventType, triggedEvent webhook_module.HookEventType,
payload api.Payloader, payload api.Payloader,
) []*DetectedWorkflow { ) (matched, filtered []*DetectedWorkflow) {
workflows := make([]*DetectedWorkflow, 0, len(parsed))
for _, p := range parsed { for _, p := range parsed {
for _, evt := range p.Events { for _, evt := range p.Events {
if evt.IsSchedule() { if evt.IsSchedule() {
// schedule is a non-target for scoped workflows // schedule is a non-target for scoped workflows
continue continue
} }
if detectMatched(consumerGitRepo, consumerCommit, triggedEvent, payload, evt) { dwf := &DetectedWorkflow{
workflows = append(workflows, &DetectedWorkflow{
EntryName: p.EntryName, EntryName: p.EntryName,
TriggerEvent: evt, TriggerEvent: evt,
Content: p.Content, Content: p.Content,
}) }
switch detectWorkflowMatch(consumerGitRepo, consumerCommit, triggedEvent, payload, evt) {
case detectMatched:
matched = append(matched, dwf)
case detectFilteredOut:
filtered = append(filtered, dwf)
case detectNotApplicable:
} }
} }
} }
return workflows return matched, filtered
} }

View File

@@ -5,6 +5,7 @@ package actions
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"path" "path"
"slices" "slices"
@@ -30,6 +31,14 @@ type DetectedWorkflow struct {
Content []byte Content []byte
} }
type detectResult int
const (
detectMatched detectResult = iota // event matched; run normally
detectNotApplicable // event/type doesn't apply; create nothing
detectFilteredOut // matched but excluded by a branch/paths filter; posts a skipped commit status when the context is a required check
)
func init() { func init() {
model.OnDecodeNodeError = func(node yaml.Node, out any, err error) { model.OnDecodeNodeError = func(node yaml.Node, out any, err error) {
// Log the error instead of panic or fatal. // Log the error instead of panic or fatal.
@@ -61,16 +70,16 @@ func isWorkflowInDirs(path string, dirs []string) bool {
return false return false
} }
func ListWorkflows(commit *git.Commit) (string, git.Entries, error) { func ListWorkflows(ctx context.Context, gitRepo *git.Repository, commit *git.Commit) (string, git.Entries, error) {
return listWorkflowsInDirs(commit, setting.Actions.WorkflowDirs) return listWorkflowsInDirs(ctx, gitRepo, commit, setting.Actions.WorkflowDirs)
} }
func listWorkflowsInDirs(commit *git.Commit, dirs []string) (string, git.Entries, error) { func listWorkflowsInDirs(ctx context.Context, gitRepo *git.Repository, commit *git.Commit, dirs []string) (string, git.Entries, error) {
var tree *git.Tree var tree *git.Tree
var err error var err error
var workflowDir string var workflowDir string
for _, workflowDir = range dirs { for _, workflowDir = range dirs {
tree, err = commit.SubTree(workflowDir) tree, err = commit.SubTree(ctx, gitRepo, workflowDir)
if err == nil { if err == nil {
break break
} }
@@ -82,7 +91,7 @@ func listWorkflowsInDirs(commit *git.Commit, dirs []string) (string, git.Entries
return "", nil, nil return "", nil, nil
} }
entries, err := tree.ListEntriesRecursiveFast() entries, err := tree.ListEntriesRecursiveFast(ctx, gitRepo)
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
@@ -96,8 +105,8 @@ func listWorkflowsInDirs(commit *git.Commit, dirs []string) (string, git.Entries
return workflowDir, ret, nil return workflowDir, ret, nil
} }
func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) { func GetContentFromEntry(gitRepo *git.Repository, entry *git.TreeEntry) ([]byte, error) {
f, err := entry.Blob().DataAsync() f, err := entry.Blob(gitRepo).DataAsync()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -167,23 +176,22 @@ func ShouldEventCreateCommitStatus(event string) bool {
} }
func DetectWorkflows( func DetectWorkflows(
ctx context.Context,
gitRepo *git.Repository, gitRepo *git.Repository,
commit *git.Commit, commit *git.Commit,
triggedEvent webhook_module.HookEventType, triggedEvent webhook_module.HookEventType,
payload api.Payloader, payload api.Payloader,
detectSchedule bool, detectSchedule bool,
) ([]*DetectedWorkflow, []*DetectedWorkflow, error) { ) (workflows, schedules, filtered []*DetectedWorkflow, err error) {
_, entries, err := ListWorkflows(commit) _, entries, err := ListWorkflows(ctx, gitRepo, commit)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, nil, err
} }
workflows := make([]*DetectedWorkflow, 0, len(entries))
schedules := make([]*DetectedWorkflow, 0, len(entries))
for _, entry := range entries { for _, entry := range entries {
content, err := GetContentFromEntry(entry) content, err := GetContentFromEntry(gitRepo, entry)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, nil, err
} }
// one workflow may have multiple events // one workflow may have multiple events
@@ -203,29 +211,35 @@ func DetectWorkflows(
} }
schedules = append(schedules, dwf) schedules = append(schedules, dwf)
} }
} else if detectMatched(gitRepo, commit, triggedEvent, payload, evt) { } else {
dwf := &DetectedWorkflow{ dwf := &DetectedWorkflow{
EntryName: entry.Name(), EntryName: entry.Name(),
TriggerEvent: evt, TriggerEvent: evt,
Content: content, Content: content,
} }
switch detectWorkflowMatch(gitRepo, commit, triggedEvent, payload, evt) {
case detectMatched:
workflows = append(workflows, dwf) workflows = append(workflows, dwf)
case detectFilteredOut:
filtered = append(filtered, dwf)
case detectNotApplicable:
}
} }
} }
} }
return workflows, schedules, nil return workflows, schedules, filtered, nil
} }
func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*DetectedWorkflow, error) { func DetectScheduledWorkflows(ctx context.Context, gitRepo *git.Repository, commit *git.Commit) ([]*DetectedWorkflow, error) {
_, entries, err := ListWorkflows(commit) _, entries, err := ListWorkflows(ctx, gitRepo, commit)
if err != nil { if err != nil {
return nil, err return nil, err
} }
wfs := make([]*DetectedWorkflow, 0, len(entries)) wfs := make([]*DetectedWorkflow, 0, len(entries))
for _, entry := range entries { for _, entry := range entries {
content, err := GetContentFromEntry(entry) content, err := GetContentFromEntry(gitRepo, entry)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -252,9 +266,9 @@ func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*D
return wfs, nil return wfs, nil
} }
func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool { func detectWorkflowMatch(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) detectResult {
if !canGithubEventMatch(evt.Name, triggedEvent) { if !canGithubEventMatch(evt.Name, triggedEvent) {
return false return detectNotApplicable
} }
switch triggedEvent { switch triggedEvent {
@@ -268,25 +282,31 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
log.Warn("Ignore unsupported %s event arguments %v", triggedEvent, evt.Acts()) log.Warn("Ignore unsupported %s event arguments %v", triggedEvent, evt.Acts())
} }
// no special filter parameters for these events, just return true if name matched // no special filter parameters for these events, just return true if name matched
return true return detectMatched
case // push case // push
webhook_module.HookEventPush: webhook_module.HookEventPush:
return matchPushEvent(commit, payload.(*api.PushPayload), evt) return matchPushEvent(gitRepo, commit, payload.(*api.PushPayload), evt)
case // issues case // issues
webhook_module.HookEventIssues, webhook_module.HookEventIssues,
webhook_module.HookEventIssueAssign, webhook_module.HookEventIssueAssign,
webhook_module.HookEventIssueLabel, webhook_module.HookEventIssueLabel,
webhook_module.HookEventIssueMilestone: webhook_module.HookEventIssueMilestone:
return matchIssuesEvent(payload.(*api.IssuePayload), evt) if matchIssuesEvent(payload.(*api.IssuePayload), evt) {
return detectMatched
}
return detectNotApplicable
case // issue_comment case // issue_comment
webhook_module.HookEventIssueComment, webhook_module.HookEventIssueComment,
// `pull_request_comment` is same as `issue_comment` // `pull_request_comment` is same as `issue_comment`
// See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_comment-use-issue_comment // See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_comment-use-issue_comment
webhook_module.HookEventPullRequestComment: webhook_module.HookEventPullRequestComment:
return matchIssueCommentEvent(payload.(*api.IssueCommentPayload), evt) if matchIssueCommentEvent(payload.(*api.IssueCommentPayload), evt) {
return detectMatched
}
return detectNotApplicable
case // pull_request case // pull_request
webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequest,
@@ -300,34 +320,49 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
case // pull_request_review case // pull_request_review
webhook_module.HookEventPullRequestReviewApproved, webhook_module.HookEventPullRequestReviewApproved,
webhook_module.HookEventPullRequestReviewRejected: webhook_module.HookEventPullRequestReviewRejected:
return matchPullRequestReviewEvent(payload.(*api.PullRequestPayload), evt) if matchPullRequestReviewEvent(payload.(*api.PullRequestPayload), evt) {
return detectMatched
}
return detectNotApplicable
case // pull_request_review_comment case // pull_request_review_comment
webhook_module.HookEventPullRequestReviewComment: webhook_module.HookEventPullRequestReviewComment:
return matchPullRequestReviewCommentEvent(payload.(*api.PullRequestPayload), evt) if matchPullRequestReviewCommentEvent(payload.(*api.PullRequestPayload), evt) {
return detectMatched
}
return detectNotApplicable
case // release case // release
webhook_module.HookEventRelease: webhook_module.HookEventRelease:
return matchReleaseEvent(payload.(*api.ReleasePayload), evt) if matchReleaseEvent(payload.(*api.ReleasePayload), evt) {
return detectMatched
}
return detectNotApplicable
case // registry_package case // registry_package
webhook_module.HookEventPackage: webhook_module.HookEventPackage:
return matchPackageEvent(payload.(*api.PackagePayload), evt) if matchPackageEvent(payload.(*api.PackagePayload), evt) {
return detectMatched
}
return detectNotApplicable
case // workflow_run case // workflow_run
webhook_module.HookEventWorkflowRun: webhook_module.HookEventWorkflowRun:
return matchWorkflowRunEvent(payload.(*api.WorkflowRunPayload), evt) if matchWorkflowRunEvent(payload.(*api.WorkflowRunPayload), evt) {
return detectMatched
}
return detectNotApplicable
default: default:
log.Warn("unsupported event %q", triggedEvent) log.Warn("unsupported event %q", triggedEvent)
return false return detectNotApplicable
} }
} }
func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobparser.Event) bool { func matchPushEvent(gitRepo *git.Repository, commit *git.Commit, pushPayload *api.PushPayload, evt *jobparser.Event) detectResult {
// with no special filter parameters // with no special filter parameters
if len(evt.Acts()) == 0 { if len(evt.Acts()) == 0 {
return true return detectMatched
} }
matchTimes := 0 matchTimes := 0
@@ -390,10 +425,11 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
matchTimes++ matchTimes++
break break
} }
filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before) filesChanged, err := commit.GetFilesChangedSinceCommit(gitRepo, pushPayload.Before)
if err != nil { if err != nil {
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)
} else { return detectNotApplicable
}
patterns, err := workflowpattern.CompilePatterns(vals...) patterns, err := workflowpattern.CompilePatterns(vals...)
if err != nil { if err != nil {
break break
@@ -401,16 +437,16 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
if !workflowpattern.Skip(patterns, filesChanged) { if !workflowpattern.Skip(patterns, filesChanged) {
matchTimes++ matchTimes++
} }
}
case "paths-ignore": case "paths-ignore":
if refName.IsTag() { if refName.IsTag() {
matchTimes++ matchTimes++
break break
} }
filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before) filesChanged, err := commit.GetFilesChangedSinceCommit(gitRepo, pushPayload.Before)
if err != nil { if err != nil {
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)
} else { return detectNotApplicable
}
patterns, err := workflowpattern.CompilePatterns(vals...) patterns, err := workflowpattern.CompilePatterns(vals...)
if err != nil { if err != nil {
break break
@@ -418,7 +454,6 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
if !workflowpattern.Filter(patterns, filesChanged) { if !workflowpattern.Filter(patterns, filesChanged) {
matchTimes++ matchTimes++
} }
}
default: default:
log.Warn("push event unsupported condition %q", cond) log.Warn("push event unsupported condition %q", cond)
} }
@@ -427,7 +462,10 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
if hasBranchFilter && hasTagFilter { if hasBranchFilter && hasTagFilter {
matchTimes++ matchTimes++
} }
return matchTimes == len(evt.Acts()) if matchTimes == len(evt.Acts()) {
return detectMatched
}
return detectFilteredOut
} }
func matchIssuesEvent(issuePayload *api.IssuePayload, evt *jobparser.Event) bool { func matchIssuesEvent(issuePayload *api.IssuePayload, evt *jobparser.Event) bool {
@@ -478,7 +516,7 @@ func matchIssuesEvent(issuePayload *api.IssuePayload, evt *jobparser.Event) bool
return matchTimes == len(evt.Acts()) return matchTimes == len(evt.Acts())
} }
func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayload *api.PullRequestPayload, evt *jobparser.Event) bool { func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayload *api.PullRequestPayload, evt *jobparser.Event) detectResult {
acts := evt.Acts() acts := evt.Acts()
activityTypeMatched := false activityTypeMatched := false
matchTimes := 0 matchTimes := 0
@@ -525,7 +563,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
headCommit, err = gitRepo.GetCommit(prPayload.PullRequest.Head.Sha) headCommit, err = gitRepo.GetCommit(prPayload.PullRequest.Head.Sha)
if err != nil { if err != nil {
log.Error("GetCommit [ref: %s]: %v", prPayload.PullRequest.Head.Sha, err) log.Error("GetCommit [ref: %s]: %v", prPayload.PullRequest.Head.Sha, err)
return false return detectNotApplicable
} }
} }
@@ -554,10 +592,11 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
matchTimes++ matchTimes++
} }
case "paths": case "paths":
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase) filesChanged, err := headCommit.GetFilesChangedSinceCommit(gitRepo, prPayload.PullRequest.MergeBase)
if err != nil { if err != nil {
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err) log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
} else { return detectNotApplicable
}
patterns, err := workflowpattern.CompilePatterns(vals...) patterns, err := workflowpattern.CompilePatterns(vals...)
if err != nil { if err != nil {
break break
@@ -565,12 +604,12 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
if !workflowpattern.Skip(patterns, filesChanged) { if !workflowpattern.Skip(patterns, filesChanged) {
matchTimes++ matchTimes++
} }
}
case "paths-ignore": case "paths-ignore":
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase) filesChanged, err := headCommit.GetFilesChangedSinceCommit(gitRepo, prPayload.PullRequest.MergeBase)
if err != nil { if err != nil {
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err) log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
} else { return detectNotApplicable
}
patterns, err := workflowpattern.CompilePatterns(vals...) patterns, err := workflowpattern.CompilePatterns(vals...)
if err != nil { if err != nil {
break break
@@ -578,12 +617,17 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
if !workflowpattern.Filter(patterns, filesChanged) { if !workflowpattern.Filter(patterns, filesChanged) {
matchTimes++ matchTimes++
} }
}
default: default:
log.Warn("pull request event unsupported condition %q", cond) log.Warn("pull request event unsupported condition %q", cond)
} }
} }
return activityTypeMatched && matchTimes == len(evt.Acts()) if !activityTypeMatched {
return detectNotApplicable
}
if matchTimes != len(evt.Acts()) {
return detectFilteredOut
}
return detectMatched
} }
func matchIssueCommentEvent(issueCommentPayload *api.IssueCommentPayload, evt *jobparser.Event) bool { func matchIssueCommentEvent(issueCommentPayload *api.IssueCommentPayload, evt *jobparser.Event) bool {

View File

@@ -101,49 +101,49 @@ func TestDetectMatched(t *testing.T) {
triggedEvent webhook_module.HookEventType triggedEvent webhook_module.HookEventType
payload api.Payloader payload api.Payloader
yamlOn string yamlOn string
expected bool expected detectResult
}{ }{
{ {
desc: "HookEventCreate(create) matches GithubEventCreate(create)", desc: "HookEventCreate(create) matches GithubEventCreate(create)",
triggedEvent: webhook_module.HookEventCreate, triggedEvent: webhook_module.HookEventCreate,
payload: nil, payload: nil,
yamlOn: "on: create", yamlOn: "on: create",
expected: true, expected: detectMatched,
}, },
{ {
desc: "HookEventIssues(issues) `opened` action matches GithubEventIssues(issues)", desc: "HookEventIssues(issues) `opened` action matches GithubEventIssues(issues)",
triggedEvent: webhook_module.HookEventIssues, triggedEvent: webhook_module.HookEventIssues,
payload: &api.IssuePayload{Action: api.HookIssueOpened}, payload: &api.IssuePayload{Action: api.HookIssueOpened},
yamlOn: "on: issues", yamlOn: "on: issues",
expected: true, expected: detectMatched,
}, },
{ {
desc: "HookEventIssues(issues) `milestoned` action matches GithubEventIssues(issues)", desc: "HookEventIssues(issues) `milestoned` action matches GithubEventIssues(issues)",
triggedEvent: webhook_module.HookEventIssues, triggedEvent: webhook_module.HookEventIssues,
payload: &api.IssuePayload{Action: api.HookIssueMilestoned}, payload: &api.IssuePayload{Action: api.HookIssueMilestoned},
yamlOn: "on: issues", yamlOn: "on: issues",
expected: true, expected: detectMatched,
}, },
{ {
desc: "HookEventPullRequestSync(pull_request_sync) matches GithubEventPullRequest(pull_request)", desc: "HookEventPullRequestSync(pull_request_sync) matches GithubEventPullRequest(pull_request)",
triggedEvent: webhook_module.HookEventPullRequestSync, triggedEvent: webhook_module.HookEventPullRequestSync,
payload: &api.PullRequestPayload{Action: api.HookIssueSynchronized}, payload: &api.PullRequestPayload{Action: api.HookIssueSynchronized},
yamlOn: "on: pull_request", yamlOn: "on: pull_request",
expected: true, expected: detectMatched,
}, },
{ {
desc: "HookEventPullRequest(pull_request) `label_updated` action doesn't match GithubEventPullRequest(pull_request) with no activity type", desc: "HookEventPullRequest(pull_request) `label_updated` action doesn't match GithubEventPullRequest(pull_request) with no activity type",
triggedEvent: webhook_module.HookEventPullRequest, triggedEvent: webhook_module.HookEventPullRequest,
payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated}, payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated},
yamlOn: "on: pull_request", yamlOn: "on: pull_request",
expected: false, expected: detectNotApplicable,
}, },
{ {
desc: "HookEventPullRequest(pull_request) `closed` action doesn't match GithubEventPullRequest(pull_request) with no activity type", desc: "HookEventPullRequest(pull_request) `closed` action doesn't match GithubEventPullRequest(pull_request) with no activity type",
triggedEvent: webhook_module.HookEventPullRequest, triggedEvent: webhook_module.HookEventPullRequest,
payload: &api.PullRequestPayload{Action: api.HookIssueClosed}, payload: &api.PullRequestPayload{Action: api.HookIssueClosed},
yamlOn: "on: pull_request", yamlOn: "on: pull_request",
expected: false, expected: detectNotApplicable,
}, },
{ {
desc: "HookEventPullRequest(pull_request) `closed` action doesn't match GithubEventPullRequest(pull_request) with branches", desc: "HookEventPullRequest(pull_request) `closed` action doesn't match GithubEventPullRequest(pull_request) with branches",
@@ -155,56 +155,56 @@ func TestDetectMatched(t *testing.T) {
}, },
}, },
yamlOn: "on:\n pull_request:\n branches: [main]", yamlOn: "on:\n pull_request:\n branches: [main]",
expected: false, expected: detectNotApplicable,
}, },
{ {
desc: "HookEventPullRequest(pull_request) `label_updated` action matches GithubEventPullRequest(pull_request) with `label` activity type", desc: "HookEventPullRequest(pull_request) `label_updated` action matches GithubEventPullRequest(pull_request) with `label` activity type",
triggedEvent: webhook_module.HookEventPullRequest, triggedEvent: webhook_module.HookEventPullRequest,
payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated}, payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated},
yamlOn: "on:\n pull_request:\n types: [labeled]", yamlOn: "on:\n pull_request:\n types: [labeled]",
expected: true, expected: detectMatched,
}, },
{ {
desc: "HookEventPullRequestReviewComment(pull_request_review_comment) matches GithubEventPullRequestReviewComment(pull_request_review_comment)", desc: "HookEventPullRequestReviewComment(pull_request_review_comment) matches GithubEventPullRequestReviewComment(pull_request_review_comment)",
triggedEvent: webhook_module.HookEventPullRequestReviewComment, triggedEvent: webhook_module.HookEventPullRequestReviewComment,
payload: &api.PullRequestPayload{Action: api.HookIssueReviewed}, payload: &api.PullRequestPayload{Action: api.HookIssueReviewed},
yamlOn: "on:\n pull_request_review_comment:\n types: [created]", yamlOn: "on:\n pull_request_review_comment:\n types: [created]",
expected: true, expected: detectMatched,
}, },
{ {
desc: "HookEventPullRequestReviewRejected(pull_request_review_rejected) doesn't match GithubEventPullRequestReview(pull_request_review) with `dismissed` activity type (we don't support `dismissed` at present)", desc: "HookEventPullRequestReviewRejected(pull_request_review_rejected) doesn't match GithubEventPullRequestReview(pull_request_review) with `dismissed` activity type (we don't support `dismissed` at present)",
triggedEvent: webhook_module.HookEventPullRequestReviewRejected, triggedEvent: webhook_module.HookEventPullRequestReviewRejected,
payload: &api.PullRequestPayload{Action: api.HookIssueReviewed}, payload: &api.PullRequestPayload{Action: api.HookIssueReviewed},
yamlOn: "on:\n pull_request_review:\n types: [dismissed]", yamlOn: "on:\n pull_request_review:\n types: [dismissed]",
expected: false, expected: detectNotApplicable,
}, },
{ {
desc: "HookEventRelease(release) `published` action matches GithubEventRelease(release) with `published` activity type", desc: "HookEventRelease(release) `published` action matches GithubEventRelease(release) with `published` activity type",
triggedEvent: webhook_module.HookEventRelease, triggedEvent: webhook_module.HookEventRelease,
payload: &api.ReleasePayload{Action: api.HookReleasePublished}, payload: &api.ReleasePayload{Action: api.HookReleasePublished},
yamlOn: "on:\n release:\n types: [published]", yamlOn: "on:\n release:\n types: [published]",
expected: true, expected: detectMatched,
}, },
{ {
desc: "HookEventPackage(package) `created` action doesn't match GithubEventRegistryPackage(registry_package) with `updated` activity type", desc: "HookEventPackage(package) `created` action doesn't match GithubEventRegistryPackage(registry_package) with `updated` activity type",
triggedEvent: webhook_module.HookEventPackage, triggedEvent: webhook_module.HookEventPackage,
payload: &api.PackagePayload{Action: api.HookPackageCreated}, payload: &api.PackagePayload{Action: api.HookPackageCreated},
yamlOn: "on:\n registry_package:\n types: [updated]", yamlOn: "on:\n registry_package:\n types: [updated]",
expected: false, expected: detectNotApplicable,
}, },
{ {
desc: "HookEventWiki(wiki) matches GithubEventGollum(gollum)", desc: "HookEventWiki(wiki) matches GithubEventGollum(gollum)",
triggedEvent: webhook_module.HookEventWiki, triggedEvent: webhook_module.HookEventWiki,
payload: nil, payload: nil,
yamlOn: "on: gollum", yamlOn: "on: gollum",
expected: true, expected: detectMatched,
}, },
{ {
desc: "HookEventSchedule(schedule) matches GithubEventSchedule(schedule)", desc: "HookEventSchedule(schedule) matches GithubEventSchedule(schedule)",
triggedEvent: webhook_module.HookEventSchedule, triggedEvent: webhook_module.HookEventSchedule,
payload: nil, payload: nil,
yamlOn: "on: schedule", yamlOn: "on: schedule",
expected: true, expected: detectMatched,
}, },
{ {
desc: "push to tag matches workflow with paths condition (should skip paths check)", desc: "push to tag matches workflow with paths condition (should skip paths check)",
@@ -222,7 +222,19 @@ func TestDetectMatched(t *testing.T) {
}, },
commit: nil, commit: nil,
yamlOn: "on:\n push:\n paths:\n - src/**", yamlOn: "on:\n push:\n paths:\n - src/**",
expected: true, expected: detectMatched,
},
{
desc: "push branch filter excludes -> filtered out",
triggedEvent: webhook_module.HookEventPush,
payload: &api.PushPayload{
Ref: "refs/heads/feature/x",
Before: "0000000",
Commits: []*api.PayloadCommit{{ID: "abc", Added: []string{"a.go"}, Message: "x"}},
},
commit: nil,
yamlOn: "on:\n push:\n branches: [main]",
expected: detectFilteredOut,
}, },
} }
@@ -231,7 +243,7 @@ func TestDetectMatched(t *testing.T) {
evts, err := GetEventsFromContent(fullWorkflowContent(tc.yamlOn)) evts, err := GetEventsFromContent(fullWorkflowContent(tc.yamlOn))
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, evts, 1) assert.Len(t, evts, 1)
assert.Equal(t, tc.expected, detectMatched(nil, tc.commit, tc.triggedEvent, tc.payload, evts[0])) assert.Equal(t, tc.expected, detectWorkflowMatch(nil, tc.commit, tc.triggedEvent, tc.payload, evts[0]))
}) })
} }
} }

View File

@@ -4,8 +4,14 @@
package openid package openid
import ( import (
"net/http"
"sync"
"time" "time"
"gitea.dev/modules/hostmatcher"
"gitea.dev/modules/proxy"
"gitea.dev/modules/setting"
"github.com/yohcop/openid-go" "github.com/yohcop/openid-go"
) )
@@ -19,11 +25,23 @@ import (
var ( var (
nonceStore = openid.NewSimpleNonceStore() nonceStore = openid.NewSimpleNonceStore()
discoveryCache = newTimedDiscoveryCache(24 * time.Hour) discoveryCache = newTimedDiscoveryCache(24 * time.Hour)
// openIDInstance does discovery/verification via an SSRF-protected client, so a user-supplied
// OpenID identifier can't reach internal/loopback/reserved addresses. It honors the operator's
// [security] ALLOWED_HOST_LIST (empty defaults to "external"), matching the avatar/webhook/migration
// clients, and validates the proxy path too. Lazy: reads proxy/settings once.
openIDInstance = sync.OnceValue(func() *openid.OpenID {
allowList := hostmatcher.ParseHostMatchList("security.ALLOWED_HOST_LIST", setting.Security.AllowedHostList)
return openid.NewOpenID(&http.Client{
Timeout: 30 * time.Second,
Transport: hostmatcher.NewHTTPTransport("openid", allowList, nil, proxy.Proxy(), setting.Proxy.ProxyURLFixed, nil),
})
})
) )
// Verify handles response from OpenID provider // Verify handles response from OpenID provider
func Verify(fullURL string) (id string, err error) { func Verify(fullURL string) (id string, err error) {
return openid.Verify(fullURL, discoveryCache, nonceStore) return openIDInstance().Verify(fullURL, discoveryCache, nonceStore)
} }
// Normalize normalizes an OpenID URI // Normalize normalizes an OpenID URI
@@ -33,5 +51,5 @@ func Normalize(url string) (id string, err error) {
// RedirectURL redirects browser // RedirectURL redirects browser
func RedirectURL(id, callbackURL, realm string) (string, error) { func RedirectURL(id, callbackURL, realm string) (string, error) {
return openid.RedirectURL(id, callbackURL, realm) return openIDInstance().RedirectURL(id, callbackURL, realm)
} }

View File

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

View File

@@ -3,7 +3,11 @@
package fileicon package fileicon
import "gitea.dev/modules/git" import (
"context"
"gitea.dev/modules/git"
)
type EntryInfo struct { type EntryInfo struct {
BaseName string BaseName string
@@ -12,10 +16,10 @@ type EntryInfo struct {
IsOpen bool IsOpen bool
} }
func EntryInfoFromGitTreeEntry(commit *git.Commit, fullPath string, gitEntry *git.TreeEntry) *EntryInfo { func EntryInfoFromGitTreeEntry(ctx context.Context, gitRepo *git.Repository, commit *git.Commit, fullPath string, gitEntry *git.TreeEntry) *EntryInfo {
ret := &EntryInfo{BaseName: gitEntry.Name(), EntryMode: gitEntry.Mode()} ret := &EntryInfo{BaseName: gitEntry.Name(), EntryMode: gitEntry.Mode()}
if gitEntry.IsLink() { if gitEntry.IsLink() {
if res, err := git.EntryFollowLink(commit, fullPath, gitEntry); err == nil && res.TargetEntry.IsDir() { if res, err := git.EntryFollowLink(ctx, gitRepo, commit, fullPath, gitEntry); err == nil && res.TargetEntry.IsDir() {
ret.SymlinkToMode = res.TargetEntry.Mode() ret.SymlinkToMode = res.TargetEntry.Mode()
} }
} }

View File

@@ -17,17 +17,17 @@ import (
// Commit represents a git commit. // Commit represents a git commit.
type Commit struct { type Commit struct {
Tree // FIXME: bad design, this field can be nil if the commit is from "last commit cache"
CommitMessage CommitMessage
ID ObjectID ID ObjectID
TreeID ObjectID
Parents []ObjectID
Author *Signature // never nil Author *Signature // never nil
Committer *Signature // never nil Committer *Signature // never nil
Signature *CommitSignature Signature *CommitSignature
Parents []ObjectID // ID strings
submoduleCache *ObjectCache[*SubModule] submoduleCache *ObjectCache[*SubModule]
treeCache *Tree
} }
// CommitSignature represents a git commit signature part. // CommitSignature represents a git commit signature part.
@@ -46,12 +46,12 @@ func (c *Commit) ParentID(n int) (ObjectID, error) {
} }
// Parent returns n-th parent (0-based index) of the commit. // Parent returns n-th parent (0-based index) of the commit.
func (c *Commit) Parent(n int) (*Commit, error) { func (c *Commit) Parent(gitRepo *Repository, n int) (*Commit, error) {
id, err := c.ParentID(n) id, err := c.ParentID(n)
if err != nil { if err != nil {
return nil, err return nil, err
} }
parent, err := c.repo.getCommit(id) parent, err := gitRepo.getCommit(id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -65,25 +65,44 @@ func (c *Commit) ParentCount() int {
} }
// GetCommitByPath return the commit of relative path object. // GetCommitByPath return the commit of relative path object.
func (c *Commit) GetCommitByPath(relpath string) (*Commit, error) { func (c *Commit) GetCommitByPath(gitRepo *Repository, relpath string) (*Commit, error) {
if c.repo.LastCommitCache != nil { if gitRepo.LastCommitCache != nil {
return c.repo.LastCommitCache.GetCommitByPath(c.ID.String(), relpath) return gitRepo.LastCommitCache.GetCommitByPath(c.ID.String(), relpath)
} }
return c.repo.getCommitByPathWithID(c.ID, relpath) return gitRepo.getCommitByPathWithID(c.ID, relpath)
}
func (c *Commit) Tree() *Tree {
if c.treeCache == nil {
c.treeCache = newTree(c.TreeID)
}
return c.treeCache
}
func (c *Commit) GetBlobByPath(ctx context.Context, gitRepo *Repository, relpath string) (*Blob, error) {
return c.Tree().GetBlobByPath(ctx, gitRepo, relpath)
}
func (c *Commit) GetTreeEntryByPath(ctx context.Context, gitRepo *Repository, relpath string) (_ *TreeEntry, err error) {
return c.Tree().GetTreeEntryByPath(ctx, gitRepo, relpath)
}
func (c *Commit) SubTree(ctx context.Context, gitRepo *Repository, relpath string) (*Tree, error) {
return c.Tree().SubTree(ctx, gitRepo, relpath)
} }
// CommitsByRange returns the specific page commits before current revision, every page's number default by CommitsRangeSize // CommitsByRange returns the specific page commits before current revision, every page's number default by CommitsRangeSize
func (c *Commit) CommitsByRange(page, pageSize int, not, since, until string) ([]*Commit, error) { func (c *Commit) CommitsByRange(gitRepo *Repository, page, pageSize int, not, since, until string) ([]*Commit, error) {
return c.repo.commitsByRangeWithTime(c.ID, page, pageSize, not, since, until) return gitRepo.commitsByRangeWithTime(c.ID, page, pageSize, not, since, until)
} }
// CommitsBefore returns all the commits before current revision // CommitsBefore returns all the commits before current revision
func (c *Commit) CommitsBefore() ([]*Commit, error) { func (c *Commit) CommitsBefore(gitRepo *Repository) ([]*Commit, error) {
return c.repo.getCommitsBefore(c.ID) return gitRepo.getCommitsBefore(c.ID)
} }
// HasPreviousCommit returns true if a given commitHash is contained in commit's parents // HasPreviousCommit returns true if a given commitHash is contained in commit's parents
func (c *Commit) HasPreviousCommit(objectID ObjectID) (bool, error) { func (c *Commit) HasPreviousCommit(ctx context.Context, gitRepo *Repository, objectID ObjectID) (bool, error) {
this := c.ID.String() this := c.ID.String()
that := objectID.String() that := objectID.String()
@@ -93,8 +112,8 @@ func (c *Commit) HasPreviousCommit(objectID ObjectID) (bool, error) {
_, _, err := gitcmd.NewCommand("merge-base", "--is-ancestor"). _, _, err := gitcmd.NewCommand("merge-base", "--is-ancestor").
AddDynamicArguments(that, this). AddDynamicArguments(that, this).
WithDir(c.repo.Path). WithDir(gitRepo.Path).
RunStdString(c.repo.Ctx) RunStdString(ctx)
if err == nil { if err == nil {
return true, nil return true, nil
} }
@@ -108,8 +127,8 @@ func (c *Commit) HasPreviousCommit(objectID ObjectID) (bool, error) {
} }
// IsForcePush returns true if a push from oldCommitHash to this is a force push // IsForcePush returns true if a push from oldCommitHash to this is a force push
func (c *Commit) IsForcePush(oldCommitID string) (bool, error) { func (c *Commit) IsForcePush(ctx context.Context, gitRepo *Repository, oldCommitID string) (bool, error) {
objectFormat, err := c.repo.GetObjectFormat() objectFormat, err := gitRepo.GetObjectFormat()
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -117,22 +136,22 @@ func (c *Commit) IsForcePush(oldCommitID string) (bool, error) {
return false, nil return false, nil
} }
oldCommit, err := c.repo.GetCommit(oldCommitID) oldCommit, err := gitRepo.GetCommit(oldCommitID)
if err != nil { if err != nil {
return false, err return false, err
} }
hasPreviousCommit, err := c.HasPreviousCommit(oldCommit.ID) hasPreviousCommit, err := c.HasPreviousCommit(ctx, gitRepo, oldCommit.ID)
return !hasPreviousCommit, err return !hasPreviousCommit, err
} }
// CommitsBeforeLimit returns num commits before current revision // CommitsBeforeLimit returns num commits before current revision
func (c *Commit) CommitsBeforeLimit(num int) ([]*Commit, error) { func (c *Commit) CommitsBeforeLimit(gitRepo *Repository, num int) ([]*Commit, error) {
return c.repo.getCommitsBeforeLimit(c.ID, num) return gitRepo.getCommitsBeforeLimit(c.ID, num)
} }
// CommitsBeforeUntil returns the commits in range "[cur, ref)" // CommitsBeforeUntil returns the commits in range "[cur, ref)"
func (c *Commit) CommitsBeforeUntil(ref RefName) ([]*Commit, error) { func (c *Commit) CommitsBeforeUntil(gitRepo *Repository, ref RefName) ([]*Commit, error) {
return c.repo.CommitsBetween(c.ID.RefName(), ref, -1) return gitRepo.CommitsBetween(c.ID.RefName(), ref, -1)
} }
// SearchCommitsOptions specify the parameters for SearchCommits // SearchCommitsOptions specify the parameters for SearchCommits
@@ -175,39 +194,29 @@ func NewSearchCommitsOptions(searchString string, forAllRefs bool) SearchCommits
} }
// SearchCommits returns the commits match the keyword before current revision // SearchCommits returns the commits match the keyword before current revision
func (c *Commit) SearchCommits(opts SearchCommitsOptions) ([]*Commit, error) { func (c *Commit) SearchCommits(gitRepo *Repository, opts SearchCommitsOptions) ([]*Commit, error) {
return c.repo.searchCommits(c.ID, opts) return gitRepo.searchCommits(c.ID, opts)
} }
// GetFilesChangedSinceCommit get all changed file names between pastCommit to current revision // GetFilesChangedSinceCommit get all changed file names between pastCommit to current revision
func (c *Commit) GetFilesChangedSinceCommit(pastCommit string) ([]string, error) { func (c *Commit) GetFilesChangedSinceCommit(gitRepo *Repository, pastCommit string) ([]string, error) {
return c.repo.GetFilesChangedBetween(pastCommit, c.ID.String()) return gitRepo.GetFilesChangedBetween(pastCommit, c.ID.String())
} }
// FileChangedSinceCommit Returns true if the file given has changed since the past commit // FileChangedSinceCommit Returns true if the file given has changed since the past commit
// YOU MUST ENSURE THAT pastCommit is a valid commit ID. // YOU MUST ENSURE THAT pastCommit is a valid commit ID.
func (c *Commit) FileChangedSinceCommit(filename, pastCommit string) (bool, error) { func (c *Commit) FileChangedSinceCommit(gitRepo *Repository, filename, pastCommit string) (bool, error) {
return c.repo.FileChangedBetweenCommits(filename, pastCommit, c.ID.String()) return gitRepo.FileChangedBetweenCommits(filename, pastCommit, c.ID.String())
}
// HasFile returns true if the file given exists on this commit
// This does only mean it's there - it does not mean the file was changed during the commit.
func (c *Commit) HasFile(filename string) (bool, error) {
_, err := c.GetBlobByPath(filename)
if err != nil {
return false, err
}
return true, nil
} }
// GetFileContent reads a file content as a string or returns false if this was not possible // GetFileContent reads a file content as a string or returns false if this was not possible
func (c *Commit) GetFileContent(filename string, limit int) (string, error) { func (c *Commit) GetFileContent(ctx context.Context, gitRepo *Repository, filename string, limit int) (string, error) {
entry, err := c.GetTreeEntryByPath(filename) entry, err := c.GetTreeEntryByPath(ctx, gitRepo, filename)
if err != nil { if err != nil {
return "", err return "", err
} }
r, err := entry.Blob().DataAsync() r, err := entry.Blob(gitRepo).DataAsync()
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@@ -3,6 +3,8 @@
package git package git
import "context"
// CommitInfo describes the first commit with the provided entry // CommitInfo describes the first commit with the provided entry
type CommitInfo struct { type CommitInfo struct {
Entry *TreeEntry Entry *TreeEntry
@@ -10,8 +12,8 @@ type CommitInfo struct {
SubmoduleFile *CommitSubmoduleFile SubmoduleFile *CommitSubmoduleFile
} }
func GetCommitInfoSubmoduleFile(repoLink, fullPath string, commit *Commit, refCommitID ObjectID) (*CommitSubmoduleFile, error) { func GetCommitInfoSubmoduleFile(ctx context.Context, repoLink, fullPath string, gitRepo *Repository, commit *Commit, refCommitID ObjectID) (*CommitSubmoduleFile, error) {
submodule, err := commit.GetSubModule(fullPath) submodule, err := commit.GetSubModule(ctx, gitRepo, fullPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -17,7 +17,7 @@ import (
) )
// GetCommitsInfo gets information of all commits that are corresponding to these entries // GetCommitsInfo gets information of all commits that are corresponding to these entries
func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) { func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, gitRepo *Repository, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
entryPaths := make([]string, len(tes)+1) entryPaths := make([]string, len(tes)+1)
// Get the commit for the treePath itself // Get the commit for the treePath itself
entryPaths[0] = "" entryPaths[0] = ""
@@ -25,25 +25,16 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *
entryPaths[i+1] = entry.Name() entryPaths[i+1] = entry.Name()
} }
commitNodeIndex, commitGraphFile := commit.repo.CommitNodeIndex()
if commitGraphFile != nil {
defer commitGraphFile.Close()
}
c, err := commitNodeIndex.Get(plumbing.Hash(commit.ID.RawValue()))
if err != nil {
return nil, nil, err
}
var revs map[string]*Commit var revs map[string]*Commit
if commit.repo.LastCommitCache != nil { var err error
if gitRepo.LastCommitCache != nil {
var unHitPaths []string var unHitPaths []string
revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, commit.repo.LastCommitCache) revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, gitRepo.LastCommitCache)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if len(unHitPaths) > 0 { if len(unHitPaths) > 0 {
revs2, err := GetLastCommitForPaths(ctx, commit.repo.LastCommitCache, c, treePath, unHitPaths) revs2, err := GetLastCommitForPaths(ctx, gitRepo, commit, treePath, unHitPaths)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -51,13 +42,13 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *
maps.Copy(revs, revs2) maps.Copy(revs, revs2)
} }
} else { } else {
revs, err = GetLastCommitForPaths(ctx, nil, c, treePath, entryPaths) revs, err = GetLastCommitForPaths(ctx, gitRepo, commit, treePath, entryPaths)
} }
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
commit.repo.gogitStorage.Close() gitRepo.gogitStorage.Close()
commitsInfo := make([]CommitInfo, len(tes)) commitsInfo := make([]CommitInfo, len(tes))
for i, entry := range tes { for i, entry := range tes {
@@ -72,7 +63,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *
// If the entry is a submodule, add a submodule file for this // If the entry is a submodule, add a submodule file for this
if entry.IsSubModule() { if entry.IsSubModule() {
commitsInfo[i].SubmoduleFile, err = GetCommitInfoSubmoduleFile(repoLink, path.Join(treePath, entry.Name()), commit, entry.ID) commitsInfo[i].SubmoduleFile, err = GetCommitInfoSubmoduleFile(ctx, repoLink, path.Join(treePath, entry.Name()), gitRepo, commit, entry.ID)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -83,11 +74,10 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *
// get it for free during the tree traversal and it's used for listing // get it for free during the tree traversal and it's used for listing
// pages to display information about newest commit for a given path. // pages to display information about newest commit for a given path.
var treeCommit *Commit var treeCommit *Commit
var ok bool
if treePath == "" { if treePath == "" {
treeCommit = commit treeCommit = commit
} else if treeCommit, ok = revs[""]; ok { } else {
treeCommit.repo = commit.repo treeCommit = revs[""]
} }
return commitsInfo, treeCommit, nil return commitsInfo, treeCommit, nil
} }
@@ -162,7 +152,20 @@ func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cac
} }
// GetLastCommitForPaths returns last commit information // GetLastCommitForPaths returns last commit information
func GetLastCommitForPaths(ctx context.Context, cache *LastCommitCache, c cgobject.CommitNode, treePath string, paths []string) (map[string]*Commit, error) { func GetLastCommitForPaths(ctx context.Context, gitRepo *Repository, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) {
commitNodeIndex, commitGraphFile := gitRepo.CommitNodeIndex()
if commitGraphFile != nil {
defer commitGraphFile.Close()
}
c, err := commitNodeIndex.Get(plumbing.Hash(commit.ID.RawValue()))
if err != nil {
return nil, err
}
return getLastCommitForPathsByCommitNode(ctx, gitRepo, c, treePath, paths)
}
func getLastCommitForPathsByCommitNode(ctx context.Context, gitRepo *Repository, c cgobject.CommitNode, treePath string, paths []string) (map[string]*Commit, error) {
refSha := c.ID().String() refSha := c.ID().String()
// We do a tree traversal with nodes sorted by commit time // We do a tree traversal with nodes sorted by commit time
@@ -243,7 +246,7 @@ heaploop:
// match any of the hashes being merged. This is more common for directories, // match any of the hashes being merged. This is more common for directories,
// but it can also happen if a file is changed through conflict resolution. // but it can also happen if a file is changed through conflict resolution.
resultNodes[pth] = current.commit resultNodes[pth] = current.commit
if err := cache.Put(refSha, path.Join(treePath, pth), current.commit.ID().String()); err != nil { if err := gitRepo.LastCommitCache.Put(refSha, path.Join(treePath, pth), current.commit.ID().String()); err != nil {
return nil, err return nil, err
} }
} }

View File

@@ -15,7 +15,7 @@ import (
) )
// GetCommitsInfo gets information of all commits that are corresponding to these entries // GetCommitsInfo gets information of all commits that are corresponding to these entries
func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) { func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, gitRepo *Repository, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
entryPaths := make([]string, len(tes)+1) entryPaths := make([]string, len(tes)+1)
// Get the commit for the treePath itself // Get the commit for the treePath itself
entryPaths[0] = "" entryPaths[0] = ""
@@ -26,15 +26,15 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *
var err error var err error
var revs map[string]*Commit var revs map[string]*Commit
if commit.repo.LastCommitCache != nil { if gitRepo.LastCommitCache != nil {
var unHitPaths []string var unHitPaths []string
revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, commit.repo.LastCommitCache) revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, gitRepo.LastCommitCache)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if len(unHitPaths) > 0 { if len(unHitPaths) > 0 {
sort.Strings(unHitPaths) sort.Strings(unHitPaths)
commits, err := GetLastCommitForPaths(ctx, commit, treePath, unHitPaths) commits, err := GetLastCommitForPaths(ctx, gitRepo, commit, treePath, unHitPaths)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -43,7 +43,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *
} }
} else { } else {
sort.Strings(entryPaths) sort.Strings(entryPaths)
revs, err = GetLastCommitForPaths(ctx, commit, treePath, entryPaths) revs, err = GetLastCommitForPaths(ctx, gitRepo, commit, treePath, entryPaths)
} }
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -64,7 +64,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *
// If the entry is a submodule, add a submodule file for this // If the entry is a submodule, add a submodule file for this
if entry.IsSubModule() { if entry.IsSubModule() {
commitsInfo[i].SubmoduleFile, err = GetCommitInfoSubmoduleFile(repoLink, path.Join(treePath, entry.Name()), commit, entry.ID) commitsInfo[i].SubmoduleFile, err = GetCommitInfoSubmoduleFile(ctx, repoLink, path.Join(treePath, entry.Name()), gitRepo, commit, entry.ID)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -75,11 +75,10 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *
// get it for free during the tree traversal, and it's used for listing // get it for free during the tree traversal, and it's used for listing
// pages to display information about the newest commit for a given path. // pages to display information about the newest commit for a given path.
var treeCommit *Commit var treeCommit *Commit
var ok bool
if treePath == "" { if treePath == "" {
treeCommit = commit treeCommit = commit
} else if treeCommit, ok = revs[""]; ok { } else {
treeCommit.repo = commit.repo treeCommit = revs[""]
} }
return commitsInfo, treeCommit, nil return commitsInfo, treeCommit, nil
} }
@@ -104,9 +103,9 @@ func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cac
} }
// GetLastCommitForPaths returns last commit information // GetLastCommitForPaths returns last commit information
func GetLastCommitForPaths(ctx context.Context, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) { func GetLastCommitForPaths(ctx context.Context, gitRepo *Repository, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) {
// We read backwards from the commit to obtain all of the commits // We read backwards from the commit to obtain all of the commits
revs, err := walkGitLog(ctx, commit.repo, commit, treePath, paths...) revs, err := walkGitLog(ctx, gitRepo, commit, treePath, paths...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -126,7 +125,7 @@ func GetLastCommitForPaths(ctx context.Context, commit *Commit, treePath string,
continue continue
} }
c, err := commit.repo.GetCommit(commitID) // Ensure the commit exists in the repository c, err := gitRepo.GetCommit(commitID) // Ensure the commit exists in the repository
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -24,7 +24,7 @@ func TestEntries_GetCommitsInfo_ContextErr(t *testing.T) {
commit, err := repo.GetCommit("feaf4ba6bc635fec442f46ddd4512416ec43c2c2") commit, err := repo.GetCommit("feaf4ba6bc635fec442f46ddd4512416ec43c2c2")
require.NoError(t, err) require.NoError(t, err)
entries, err := commit.Tree.ListEntries() entries, err := commit.Tree().ListEntries(t.Context(), repo)
require.NoError(t, err) require.NoError(t, err)
countCommitInfosCommit := func(infos []CommitInfo) (nilCommits, nonNilCommits int) { countCommitInfosCommit := func(infos []CommitInfo) (nilCommits, nonNilCommits int) {
@@ -39,14 +39,14 @@ func TestEntries_GetCommitsInfo_ContextErr(t *testing.T) {
defer test.MockVariableValue(&walkGitLogDebugBeforeNext)() defer test.MockVariableValue(&walkGitLogDebugBeforeNext)()
walkGitLogDebugBeforeNext = cancel walkGitLogDebugBeforeNext = cancel
commitInfos, _, err := entries.GetCommitsInfo(ctx, "/any/repo-link", commit, "") commitInfos, _, err := entries.GetCommitsInfo(ctx, "/any/repo-link", repo, commit, "")
assert.NoError(t, err) assert.NoError(t, err)
nilCommits, nonNilCommits := countCommitInfosCommit(commitInfos) nilCommits, nonNilCommits := countCommitInfosCommit(commitInfos)
assert.Equal(t, 0, nonNilCommits) // no commit info due to canceled (or deadline-exceeded) context assert.Equal(t, 0, nonNilCommits) // no commit info due to canceled (or deadline-exceeded) context
assert.Equal(t, 3, nilCommits) assert.Equal(t, 3, nilCommits)
walkGitLogDebugBeforeNext = nil walkGitLogDebugBeforeNext = nil
commitInfos, _, err = entries.GetCommitsInfo(t.Context(), "/any/repo-link", commit, "") commitInfos, _, err = entries.GetCommitsInfo(t.Context(), "/any/repo-link", repo, commit, "")
assert.NoError(t, err) assert.NoError(t, err)
nilCommits, nonNilCommits = countCommitInfosCommit(commitInfos) nilCommits, nonNilCommits = countCommitInfosCommit(commitInfos)
assert.Equal(t, 3, nonNilCommits) assert.Equal(t, 3, nonNilCommits)

View File

@@ -91,10 +91,9 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) {
continue continue
} }
assert.NotNil(t, commit) assert.NotNil(t, commit)
assert.NotNil(t, commit.Tree) assert.NotNil(t, commit.TreeID)
assert.NotNil(t, commit.Tree.repo)
tree, err := commit.Tree.SubTree(testCase.Path) tree, err := commit.SubTree(t.Context(), repo1, testCase.Path)
if err != nil { if err != nil {
assert.NoError(t, err, "Unable to get subtree: %s of commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err) assert.NoError(t, err, "Unable to get subtree: %s of commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err)
// no point trying to do anything else for this test. // no point trying to do anything else for this test.
@@ -102,9 +101,8 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) {
} }
assert.NotNil(t, tree, "tree is nil for testCase CommitID %s in Path %s", testCase.CommitID, testCase.Path) assert.NotNil(t, tree, "tree is nil for testCase CommitID %s in Path %s", testCase.CommitID, testCase.Path)
assert.NotNil(t, tree.repo, "repo is nil for testCase CommitID %s in Path %s", testCase.CommitID, testCase.Path)
entries, err := tree.ListEntries() entries, err := tree.ListEntries(t.Context(), repo1)
if err != nil { if err != nil {
assert.NoError(t, err, "Unable to get entries of subtree: %s in commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err) assert.NoError(t, err, "Unable to get entries of subtree: %s in commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err)
// no point trying to do anything else for this test. // no point trying to do anything else for this test.
@@ -112,7 +110,7 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) {
} }
// FIXME: Context.TODO() - if graceful has started we should use its Shutdown context otherwise use install signals in TestMain. // FIXME: Context.TODO() - if graceful has started we should use its Shutdown context otherwise use install signals in TestMain.
commitsInfo, treeCommit, err := entries.GetCommitsInfo(t.Context(), "/any/repo-link", commit, testCase.Path) commitsInfo, treeCommit, err := entries.GetCommitsInfo(t.Context(), "/any/repo-link", repo1, commit, testCase.Path)
assert.NoError(t, err, "Unable to get commit information for entries of subtree: %s in commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err) assert.NoError(t, err, "Unable to get commit information for entries of subtree: %s in commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err)
if err != nil { if err != nil {
t.FailNow() t.FailNow()
@@ -127,7 +125,7 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) {
continue continue
} }
assert.Equal(t, expectedInfo.CommitID, commit.ID.String()) assert.Equal(t, expectedInfo.CommitID, commit.ID.String())
assert.Equal(t, expectedInfo.Size, entry.Size(), entry.Name()) assert.Equal(t, expectedInfo.Size, entry.GetSize(t.Context(), repo1), entry.Name())
} }
} }
} }
@@ -155,9 +153,9 @@ func TestEntries_GetCommitsInfo(t *testing.T) {
t.Run("NonExistingSubmoduleAsNil", func(t *testing.T) { t.Run("NonExistingSubmoduleAsNil", func(t *testing.T) {
commit, err := bareRepo1.GetCommit("HEAD") commit, err := bareRepo1.GetCommit("HEAD")
require.NoError(t, err) require.NoError(t, err)
treeEntry, err := commit.GetTreeEntryByPath("file1.txt") treeEntry, err := commit.GetTreeEntryByPath(t.Context(), bareRepo1, "file1.txt")
require.NoError(t, err) require.NoError(t, err)
cisf, err := GetCommitInfoSubmoduleFile("/any/repo-link", "file1.txt", commit, treeEntry.ID) cisf, err := GetCommitInfoSubmoduleFile(t.Context(), "/any/repo-link", "file1.txt", bareRepo1, commit, treeEntry.ID)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, &CommitSubmoduleFile{ assert.Equal(t, &CommitSubmoduleFile{
repoLink: "/any/repo-link", repoLink: "/any/repo-link",
@@ -169,52 +167,3 @@ func TestEntries_GetCommitsInfo(t *testing.T) {
assert.Nil(t, cisf.SubmoduleWebLinkTree(t.Context())) assert.Nil(t, cisf.SubmoduleWebLinkTree(t.Context()))
}) })
} }
func BenchmarkEntries_GetCommitsInfo(b *testing.B) {
type benchmarkType struct {
url string
name string
}
benchmarks := []benchmarkType{
{url: "https://github.com/go-gitea/gitea.git", name: "gitea"},
{url: "https://github.com/ethantkoenig/manyfiles.git", name: "manyfiles"},
{url: "https://github.com/moby/moby.git", name: "moby"},
{url: "https://github.com/golang/go.git", name: "go"},
{url: "https://github.com/torvalds/linux.git", name: "linux"},
}
doBenchmark := func(benchmark benchmarkType) {
var commit *Commit
var entries Entries
var repo *Repository
repoPath, err := cloneRepo(b, benchmark.url)
if err != nil {
b.Fatal(err)
}
if repo, err = OpenRepository(b.Context(), repoPath); err != nil {
b.Fatal(err)
}
defer repo.Close()
if commit, err = repo.GetBranchCommit("master"); err != nil {
b.Fatal(err)
} else if entries, err = commit.Tree.ListEntries(); err != nil {
b.Fatal(err)
}
b.ResetTimer()
b.Run(benchmark.name, func(b *testing.B) {
for b.Loop() {
_, _, err := entries.GetCommitsInfo(b.Context(), "/any/repo-link", commit, "")
if err != nil {
b.Fatal(err)
}
}
})
}
for _, benchmark := range benchmarks {
doBenchmark(benchmark)
}
}

View File

@@ -10,16 +10,22 @@ import (
"sync" "sync"
"gitea.dev/modules/charset" "gitea.dev/modules/charset"
"gitea.dev/modules/container"
"gitea.dev/modules/util" "gitea.dev/modules/util"
) )
// CoAuthoredByTrailer is the canonical token for the `Co-authored-by:` git trailer. // CoAuthoredByTrailer is the canonical token for the `Co-authored-by:` git trailer.
const CoAuthoredByTrailer = "Co-authored-by" const CoAuthoredByTrailer = "Co-authored-by"
const (
commitIdentityRoleAuthor = 1
commitIdentityRoleCommitter = 2
commitIdentityRoleCoAuthor = 3
)
type CommitIdentity struct { type CommitIdentity struct {
Name string Name string
Email string Email string
role int
} }
// CommitMessageTrailerValues keys are all in lower-case // CommitMessageTrailerValues keys are all in lower-case
@@ -34,6 +40,8 @@ type CommitMessage struct {
trailerValues CommitMessageTrailerValues trailerValues CommitMessageTrailerValues
allParticipants []*CommitIdentity allParticipants []*CommitIdentity
committerCoAuthorIdx int
committerCoAuthor *CommitIdentity
} }
func (c *CommitMessage) MessageUTF8() string { func (c *CommitMessage) MessageUTF8() string {
@@ -105,29 +113,57 @@ func (c *Commit) AllParticipantIdentities() []*CommitIdentity {
return c.allParticipants return c.allParticipants
} }
exclude := container.Set[string]{} exclude := map[string]int{}
c.allParticipants = append(c.allParticipants, &CommitIdentity{Name: c.Author.Name, Email: c.Author.Email}) addParticipant := func(name, email string, role int) (existingRole int) {
exclude.Add(strings.ToLower(c.Author.Email))
addParticipant := func(name, email string) {
if name == "" && email == "" { if name == "" && email == "" {
return return 0
} }
emailLower := strings.ToLower(email) emailLower := strings.ToLower(email)
if emailLower != "" && exclude.Contains(emailLower) { if existingRole = exclude[emailLower]; emailLower != "" && existingRole != 0 {
return return existingRole
} }
c.allParticipants = append(c.allParticipants, &CommitIdentity{Name: name, Email: email}) c.allParticipants = append(c.allParticipants, &CommitIdentity{Name: name, Email: email, role: role})
exclude.Add(emailLower) exclude[emailLower] = role
return 0
} }
addParticipant(c.Committer.Name, c.Committer.Email)
c.committerCoAuthorIdx = -1
addParticipant(c.Author.Name, c.Author.Email, commitIdentityRoleAuthor)
addParticipant(c.Committer.Name, c.Committer.Email, commitIdentityRoleCommitter)
for _, coAuthorValue := range c.MessageTrailer()["co-authored-by"] { for _, coAuthorValue := range c.MessageTrailer()["co-authored-by"] {
addr, err := mail.ParseAddress(coAuthorValue) addr, err := mail.ParseAddress(coAuthorValue)
coAuthorName, coAuthorEmail := coAuthorValue, ""
if err == nil { if err == nil {
addParticipant(addr.Name, addr.Address) coAuthorName, coAuthorEmail = addr.Name, addr.Address
} else { }
addParticipant(coAuthorValue, "") existingRole := addParticipant(coAuthorName, coAuthorEmail, commitIdentityRoleCoAuthor)
if existingRole == commitIdentityRoleCommitter && c.committerCoAuthorIdx == -1 {
c.committerCoAuthorIdx = len(c.allParticipants)
c.committerCoAuthor = &CommitIdentity{coAuthorName, coAuthorEmail, commitIdentityRoleCoAuthor}
} }
} }
return c.allParticipants return c.allParticipants
} }
// CoAuthorIdentities returns co-author identities defined by "Co-authored-by:" in the git message trailer
// Only the commit's author is excluded. If committer is declared as co-author, it will be included in the result.
// * Author & Co-author: they changed the code (attribution)
// * Committer: they submitted the commit but didn't change the code (e.g.: maintainer signed a commit)
// So, a committer can also be a co-author if they changed the code.
func (c *Commit) CoAuthorIdentities() (coAuthors []*CommitIdentity) {
all := c.AllParticipantIdentities()
if len(all) <= 1 {
return nil // no co-author list
}
if all[1].role != commitIdentityRoleCommitter {
return all[1:] // no committer, so all after author are co-authors
}
if c.committerCoAuthorIdx == -1 {
return all[2:] // the committer is not in the co-author list, so just return the co-author list
}
// the committer is in the co-author list but de-duplicated, so include them as co-author again
coAuthors = append(coAuthors, all[2:c.committerCoAuthorIdx]...)
coAuthors = append(coAuthors, c.committerCoAuthor)
coAuthors = append(coAuthors, all[c.committerCoAuthorIdx:]...)
return coAuthors
}

View File

@@ -47,36 +47,83 @@ func TestCommitMessageTrailer(t *testing.T) {
} }
} }
func TestCommitMessageAllParticipantIdentities(t *testing.T) { func TestCommitMessageParticipants(t *testing.T) {
sig := func(n, e string) *Signature { return &Signature{Name: n, Email: e} } sig := func(n, e string) *Signature { return &Signature{Name: n, Email: e} }
idt := func(n, e string) *CommitIdentity { return &CommitIdentity{Name: n, Email: e} } idt := func(n, e string, r int) *CommitIdentity { return &CommitIdentity{n, e, r} }
cases := []struct { roleAuthor, roleCommitter, roleCoAuthor := commitIdentityRoleAuthor, commitIdentityRoleCommitter, commitIdentityRoleCoAuthor
type testCase struct {
name string
commit *Commit commit *Commit
participant []*CommitIdentity identities []*CommitIdentity
}{ }
t.Run("AllParticipants", func(t *testing.T) {
cases := []testCase{
{ {
"DifferentUsers",
&Commit{ &Commit{
Author: sig("a", "a@m.com"), Committer: sig("c", "c@m.com"), Author: sig("a", "a@m.com"), Committer: sig("c", "c@m.com"),
CommitMessage: CommitMessage{MessageRaw: "CO-Authored-BY: x@m.com"}, CommitMessage: CommitMessage{MessageRaw: "CO-Authored-BY: x@m.com"},
}, },
[]*CommitIdentity{idt("a", "a@m.com"), idt("c", "c@m.com"), idt("", "x@m.com")}, []*CommitIdentity{idt("a", "a@m.com", roleAuthor), idt("c", "c@m.com", roleCommitter), idt("", "x@m.com", roleCoAuthor)},
}, },
{ {
"SameUser",
&Commit{ &Commit{
Author: sig("a", "a@m.com"), Committer: sig("a", "A@M.com"), Author: sig("a", "a@m.com"), Committer: sig("a", "A@M.com"),
CommitMessage: CommitMessage{MessageRaw: "CO-Authored-BY: a@m.com"}, CommitMessage: CommitMessage{MessageRaw: "CO-Authored-BY: a@m.com"},
}, },
[]*CommitIdentity{idt("a", "a@m.com")}, []*CommitIdentity{idt("a", "a@m.com", roleAuthor)},
}, },
{ {
"NoCommitter",
&Commit{ &Commit{
Author: sig("a", "a@m.com"), Committer: sig("", ""), Author: sig("a", "a@m.com"), Committer: sig("", ""),
CommitMessage: CommitMessage{MessageRaw: "Co-authored-by: Full Name <X@M.com>"}, CommitMessage: CommitMessage{MessageRaw: "Co-authored-by: Full Name <X@M.com>"},
}, },
[]*CommitIdentity{idt("a", "a@m.com"), idt("Full Name", "X@M.com")}, []*CommitIdentity{idt("a", "a@m.com", roleAuthor), idt("Full Name", "X@M.com", roleCoAuthor)},
}, },
} }
for _, c := range cases { for _, c := range cases {
assert.Equal(t, c.participant, c.commit.AllParticipantIdentities()) assert.Equal(t, c.identities, c.commit.AllParticipantIdentities(), "case: %s", c.name)
} }
})
t.Run("CoAuthors", func(t *testing.T) {
cases := []testCase{
{
"GenuineCoAuthor",
&Commit{
Author: sig("a", "a@m.com"), Committer: sig("c", "c@m.com"),
CommitMessage: CommitMessage{MessageRaw: "Co-authored-by: x <x@m.com>"},
},
[]*CommitIdentity{idt("x", "x@m.com", roleCoAuthor)},
},
{
"CoAuthorIsCommitter",
&Commit{
Author: sig("a", "a@m.com"), Committer: sig("c", "c@m.com"),
CommitMessage: CommitMessage{MessageRaw: "Co-authored-by: c <c@m.com>"},
},
[]*CommitIdentity{idt("c", "c@m.com", roleCoAuthor)},
},
{
"CoAuthorIsAuthor",
&Commit{
Author: sig("a", "a@m.com"), Committer: sig("c", "c@m.com"),
CommitMessage: CommitMessage{MessageRaw: "Co-authored-by: a <a@m.com>"},
},
[]*CommitIdentity{},
},
{
"CoAuthorCommitterNameWithIndex", // restore the committer co-author to the co-author list by the index with correct name
&Commit{
Author: sig("a", "a@m.com"), Committer: sig("c", "c@m.com"),
CommitMessage: CommitMessage{MessageRaw: "Co-authored-by: x <x@m.com>\nCo-authored-by: c-other <c@m.com>\nCo-authored-by: y <y@m.com>"},
},
[]*CommitIdentity{idt("x", "x@m.com", roleCoAuthor), idt("c-other", "c@m.com", roleCoAuthor), idt("y", "y@m.com", roleCoAuthor)},
},
}
for _, c := range cases {
assert.Equal(t, c.identities, c.commit.CoAuthorIdentities(), "case: %s", c.name)
}
})
} }

View File

@@ -15,7 +15,7 @@ const (
commitHeaderGpgsigSha256 = "gpgsig-sha256" commitHeaderGpgsigSha256 = "gpgsig-sha256"
) )
func assignCommitFields(gitRepo *Repository, commit *Commit, headerKey string, headerValue []byte) error { func assignCommitFields(commit *Commit, headerKey string, headerValue []byte) error {
if len(headerValue) > 0 && headerValue[len(headerValue)-1] == '\n' { if len(headerValue) > 0 && headerValue[len(headerValue)-1] == '\n' {
headerValue = headerValue[:len(headerValue)-1] // remove trailing newline headerValue = headerValue[:len(headerValue)-1] // remove trailing newline
} }
@@ -25,7 +25,7 @@ func assignCommitFields(gitRepo *Repository, commit *Commit, headerKey string, h
if err != nil { if err != nil {
return fmt.Errorf("invalid tree ID %q: %w", string(headerValue), err) return fmt.Errorf("invalid tree ID %q: %w", string(headerValue), err)
} }
commit.Tree = *NewTree(gitRepo, objID) commit.TreeID = objID
case "parent": case "parent":
objID, err := NewIDFromString(string(headerValue)) objID, err := NewIDFromString(string(headerValue))
if err != nil { if err != nil {
@@ -48,7 +48,7 @@ func assignCommitFields(gitRepo *Repository, commit *Commit, headerKey string, h
// We need this to interpret commits from cat-file or cat-file --batch // We need this to interpret commits from cat-file or cat-file --batch
// //
// If used as part of a cat-file --batch stream you need to limit the reader to the correct size // If used as part of a cat-file --batch stream you need to limit the reader to the correct size
func CommitFromReader(gitRepo *Repository, objectID ObjectID, reader io.Reader) (*Commit, error) { func CommitFromReader(objectID ObjectID, reader io.Reader) (*Commit, error) {
commit := &Commit{ commit := &Commit{
ID: objectID, ID: objectID,
Author: &Signature{}, Author: &Signature{},
@@ -74,7 +74,7 @@ func CommitFromReader(gitRepo *Repository, objectID ObjectID, reader io.Reader)
k, v, _ := bytes.Cut(line, []byte{' '}) k, v, _ := bytes.Cut(line, []byte{' '})
if len(k) != 0 || !inHeader { if len(k) != 0 || !inHeader {
if headerKey != "" { if headerKey != "" {
if err = assignCommitFields(gitRepo, commit, headerKey, headerValue); err != nil { if err = assignCommitFields(commit, headerKey, headerValue); err != nil {
return nil, fmt.Errorf("unable to parse commit %q: %w", objectID.String(), err) return nil, fmt.Errorf("unable to parse commit %q: %w", objectID.String(), err)
} }
} }

View File

@@ -65,7 +65,7 @@ signed commit`
assert.NotNil(t, gitRepo) assert.NotNil(t, gitRepo)
defer gitRepo.Close() defer gitRepo.Close()
commitFromReader, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString)) commitFromReader, err := CommitFromReader(sha, strings.NewReader(commitString))
assert.NoError(t, err) assert.NoError(t, err)
require.NotNil(t, commitFromReader) require.NotNil(t, commitFromReader)
assert.EqualValues(t, sha, commitFromReader.ID) assert.EqualValues(t, sha, commitFromReader.ID)
@@ -93,7 +93,7 @@ committer Adam Majer <amajer@suse.de> 1698676906 +0100
signed commit`, commitFromReader.Signature.Payload) signed commit`, commitFromReader.Signature.Payload)
assert.Equal(t, "Adam Majer <amajer@suse.de>", commitFromReader.Author.String()) assert.Equal(t, "Adam Majer <amajer@suse.de>", commitFromReader.Author.String())
commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n")) commitFromReader2, err := CommitFromReader(sha, strings.NewReader(commitString+"\n\n"))
assert.NoError(t, err) assert.NoError(t, err)
commitFromReader.CommitMessage.MessageRaw += "\n\n" commitFromReader.CommitMessage.MessageRaw += "\n\n"
commitFromReader.Signature.Payload += "\n\n" commitFromReader.Signature.Payload += "\n\n"
@@ -118,15 +118,15 @@ func TestHasPreviousCommitSha256(t *testing.T) {
assert.Equal(t, objectFormat, parentSHA.Type()) assert.Equal(t, objectFormat, parentSHA.Type())
assert.Equal(t, "sha256", objectFormat.Name()) assert.Equal(t, "sha256", objectFormat.Name())
haz, err := commit.HasPreviousCommit(parentSHA) haz, err := commit.HasPreviousCommit(t.Context(), repo, parentSHA)
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, haz) assert.True(t, haz)
hazNot, err := commit.HasPreviousCommit(notParentSHA) hazNot, err := commit.HasPreviousCommit(t.Context(), repo, notParentSHA)
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, hazNot) assert.False(t, hazNot)
selfNot, err := commit.HasPreviousCommit(commit.ID) selfNot, err := commit.HasPreviousCommit(t.Context(), repo, commit.ID)
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, selfNot) assert.False(t, selfNot)
} }

View File

@@ -3,17 +3,19 @@
package git package git
import "context"
type SubmoduleWebLink struct { type SubmoduleWebLink struct {
RepoWebLink, CommitWebLink string RepoWebLink, CommitWebLink string
} }
// GetSubModules get all the submodules of current revision git tree // GetSubModules get all the submodules of current revision git tree
func (c *Commit) GetSubModules() (*ObjectCache[*SubModule], error) { func (c *Commit) GetSubModules(ctx context.Context, gitRepo *Repository) (*ObjectCache[*SubModule], error) {
if c.submoduleCache != nil { if c.submoduleCache != nil {
return c.submoduleCache, nil return c.submoduleCache, nil
} }
entry, err := c.GetTreeEntryByPath(".gitmodules") entry, err := c.GetTreeEntryByPath(ctx, gitRepo, ".gitmodules")
if err != nil { if err != nil {
if _, ok := err.(ErrNotExist); ok { if _, ok := err.(ErrNotExist); ok {
return nil, nil //nolint:nilnil // return nil to indicate that the submodule does not exist return nil, nil //nolint:nilnil // return nil to indicate that the submodule does not exist
@@ -21,7 +23,7 @@ func (c *Commit) GetSubModules() (*ObjectCache[*SubModule], error) {
return nil, err return nil, err
} }
rd, err := entry.Blob().DataAsync() rd, err := entry.Blob(gitRepo).DataAsync()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -37,8 +39,8 @@ func (c *Commit) GetSubModules() (*ObjectCache[*SubModule], error) {
// GetSubModule gets the submodule by the entry name. // GetSubModule gets the submodule by the entry name.
// It returns "nil, nil" if the submodule does not exist, caller should always remember to check the "nil" // It returns "nil, nil" if the submodule does not exist, caller should always remember to check the "nil"
func (c *Commit) GetSubModule(entryName string) (*SubModule, error) { func (c *Commit) GetSubModule(ctx context.Context, gitRepo *Repository, entryName string) (*SubModule, error) {
modules, err := c.GetSubModules() modules, err := c.GetSubModules(ctx, gitRepo)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -61,7 +61,7 @@ empty commit`
assert.NotNil(t, gitRepo) assert.NotNil(t, gitRepo)
defer gitRepo.Close() defer gitRepo.Close()
commitFromReader, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString)) commitFromReader, err := CommitFromReader(sha, strings.NewReader(commitString))
assert.NoError(t, err) assert.NoError(t, err)
require.NotNil(t, commitFromReader) require.NotNil(t, commitFromReader)
assert.EqualValues(t, sha, commitFromReader.ID) assert.EqualValues(t, sha, commitFromReader.ID)
@@ -89,7 +89,7 @@ committer silverwind <me@silverwind.io> 1563741793 +0200
empty commit`, commitFromReader.Signature.Payload) empty commit`, commitFromReader.Signature.Payload)
assert.Equal(t, "silverwind <me@silverwind.io>", commitFromReader.Author.String()) assert.Equal(t, "silverwind <me@silverwind.io>", commitFromReader.Author.String())
commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n")) commitFromReader2, err := CommitFromReader(sha, strings.NewReader(commitString+"\n\n"))
assert.NoError(t, err) assert.NoError(t, err)
commitFromReader.CommitMessage.MessageRaw += "\n\n" commitFromReader.CommitMessage.MessageRaw += "\n\n"
commitFromReader.Signature.Payload += "\n\n" commitFromReader.Signature.Payload += "\n\n"
@@ -125,7 +125,7 @@ ISO-8859-1`
assert.NotNil(t, gitRepo) assert.NotNil(t, gitRepo)
defer gitRepo.Close() defer gitRepo.Close()
commitFromReader, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString)) commitFromReader, err := CommitFromReader(sha, strings.NewReader(commitString))
assert.NoError(t, err) assert.NoError(t, err)
require.NotNil(t, commitFromReader) require.NotNil(t, commitFromReader)
assert.EqualValues(t, sha, commitFromReader.ID) assert.EqualValues(t, sha, commitFromReader.ID)
@@ -152,7 +152,7 @@ encoding ISO-8859-1
ISO-8859-1`, commitFromReader.Signature.Payload) ISO-8859-1`, commitFromReader.Signature.Payload)
assert.Equal(t, "KN4CK3R <admin@oldschoolhack.me>", commitFromReader.Author.String()) assert.Equal(t, "KN4CK3R <admin@oldschoolhack.me>", commitFromReader.Author.String())
commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n")) commitFromReader2, err := CommitFromReader(sha, strings.NewReader(commitString+"\n\n"))
assert.NoError(t, err) assert.NoError(t, err)
commitFromReader.CommitMessage.MessageRaw += "\n\n" commitFromReader.CommitMessage.MessageRaw += "\n\n"
commitFromReader.Signature.Payload += "\n\n" commitFromReader.Signature.Payload += "\n\n"
@@ -172,15 +172,15 @@ func TestHasPreviousCommit(t *testing.T) {
parentSHA := MustIDFromString("8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2") parentSHA := MustIDFromString("8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2")
notParentSHA := MustIDFromString("2839944139e0de9737a044f78b0e4b40d989a9e3") notParentSHA := MustIDFromString("2839944139e0de9737a044f78b0e4b40d989a9e3")
haz, err := commit.HasPreviousCommit(parentSHA) haz, err := commit.HasPreviousCommit(t.Context(), repo, parentSHA)
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, haz) assert.True(t, haz)
hazNot, err := commit.HasPreviousCommit(notParentSHA) hazNot, err := commit.HasPreviousCommit(t.Context(), repo, notParentSHA)
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, hazNot) assert.False(t, hazNot)
selfNot, err := commit.HasPreviousCommit(commit.ID) selfNot, err := commit.HasPreviousCommit(t.Context(), repo, commit.ID)
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, selfNot) assert.False(t, selfNot)
} }

View File

@@ -75,7 +75,7 @@ func getRepoRawDiffForFileCmd(_ context.Context, repo *Repository, startCommit,
} else if commit.ParentCount() == 0 { } else if commit.ParentCount() == 0 {
cmd.AddArguments("show").AddDynamicArguments(endCommit).AddDashesAndList(files...) cmd.AddArguments("show").AddDynamicArguments(endCommit).AddDashesAndList(files...)
} else { } else {
c, err := commit.Parent(0) c, err := commit.Parent(repo, 0)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -90,7 +90,7 @@ func getRepoRawDiffForFileCmd(_ context.Context, repo *Repository, startCommit,
} else if commit.ParentCount() == 0 { } else if commit.ParentCount() == 0 {
cmd.AddArguments("format-patch", "--no-signature", "--stdout", "--root").AddDynamicArguments(endCommit).AddDashesAndList(files...) cmd.AddArguments("format-patch", "--no-signature", "--stdout", "--root").AddDynamicArguments(endCommit).AddDashesAndList(files...)
} else { } else {
c, err := commit.Parent(0) c, err := commit.Parent(repo, 0)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -7,6 +7,7 @@ package languagestats
import ( import (
"bytes" "bytes"
"context"
"io" "io"
"gitea.dev/modules/analyze" "gitea.dev/modules/analyze"
@@ -21,7 +22,7 @@ import (
) )
// GetLanguageStats calculates language stats for git repository at specified commit // GetLanguageStats calculates language stats for git repository at specified commit
func GetLanguageStats(repo *git_module.Repository, commitID string) (map[string]int64, error) { func GetLanguageStats(_ context.Context, repo *git_module.Repository, commitID string) (map[string]int64, error) {
r, err := git.PlainOpen(repo.Path) r, err := git.PlainOpen(repo.Path)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -7,6 +7,7 @@ package languagestats
import ( import (
"bytes" "bytes"
"context"
"io" "io"
"gitea.dev/modules/analyze" "gitea.dev/modules/analyze"
@@ -19,10 +20,10 @@ import (
) )
// GetLanguageStats calculates language stats for git repository at specified commit // GetLanguageStats calculates language stats for git repository at specified commit
func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64, error) { func GetLanguageStats(ctx context.Context, repo *git.Repository, commitID string) (map[string]int64, error) {
// We will feed the commit IDs in order into cat-file --batch, followed by blobs as necessary. // We will feed the commit IDs in order into cat-file --batch, followed by blobs as necessary.
// so let's create a batch stdin and stdout // so let's create a batch stdin and stdout
batch, cancel, err := repo.CatFileBatch(repo.Ctx) batch, cancel, err := repo.CatFileBatch(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -43,7 +44,7 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64,
return nil, git.ErrNotExist{ID: commitID} return nil, git.ErrNotExist{ID: commitID}
} }
commit, err := git.CommitFromReader(repo, sha, io.LimitReader(batchReader, commitInfo.Size)) commit, err := git.CommitFromReader(sha, io.LimitReader(batchReader, commitInfo.Size))
if err != nil { if err != nil {
log.Debug("Unable to get commit for: %s. Err: %v", commitID, err) log.Debug("Unable to get commit for: %s. Err: %v", commitID, err)
return nil, err return nil, err
@@ -52,9 +53,7 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64,
return nil, err return nil, err
} }
tree := commit.Tree entries, err := commit.Tree().ListEntriesRecursiveWithSize(ctx, repo)
entries, err := tree.ListEntriesRecursiveWithSize()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -81,13 +80,15 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64,
select { select {
case <-repo.Ctx.Done(): case <-repo.Ctx.Done():
return sizes, repo.Ctx.Err() return sizes, repo.Ctx.Err()
case <-ctx.Done():
return sizes, ctx.Err()
default: default:
} }
contentBuf.Reset() contentBuf.Reset()
content = contentBuf.Bytes() content = contentBuf.Bytes()
if f.Size() == 0 { if f.GetSize(ctx, repo) == 0 {
continue continue
} }
@@ -124,7 +125,7 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64,
} }
// this language will always be added to the size // this language will always be added to the size
sizes[language] += f.Size() sizes[language] += f.GetSize(ctx, repo)
continue continue
} }
} }
@@ -138,7 +139,7 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64,
// If content can not be read or file is too big just do detection by filename // If content can not be read or file is too big just do detection by filename
if f.Size() <= bigFileSize { if f.GetSize(ctx, repo) <= bigFileSize {
info, _, err := batch.QueryContent(f.ID.String()) info, _, err := batch.QueryContent(f.ID.String())
if err != nil { if err != nil {
return nil, err return nil, err
@@ -192,10 +193,10 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64,
includedLanguage[language] = included includedLanguage[language] = included
} }
if included || isDetectable.ValueOrDefault(false) { if included || isDetectable.ValueOrDefault(false) {
sizes[language] += f.Size() sizes[language] += f.GetSize(ctx, repo)
} else if len(sizes) == 0 && (firstExcludedLanguage == "" || firstExcludedLanguage == language) { } else if len(sizes) == 0 && (firstExcludedLanguage == "" || firstExcludedLanguage == language) {
firstExcludedLanguage = language firstExcludedLanguage = language
firstExcludedLanguageSize += f.Size() firstExcludedLanguageSize += f.GetSize(ctx, repo)
} }
} }

View File

@@ -22,7 +22,7 @@ func TestRepository_GetLanguageStats(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
defer gitRepo.Close() defer gitRepo.Close()
stats, err := GetLanguageStats(gitRepo, "8fee858da5796dfb37704761701bb8e800ad9ef3") stats, err := GetLanguageStats(t.Context(), gitRepo, "8fee858da5796dfb37704761701bb8e800ad9ef3")
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, map[string]int64{ assert.Equal(t, map[string]int64{

View File

@@ -13,26 +13,26 @@ import (
) )
// CacheCommit will cache the commit from the gitRepository // CacheCommit will cache the commit from the gitRepository
func (c *Commit) CacheCommit(ctx context.Context) error { func (c *Commit) CacheCommit(ctx context.Context, gitRepo *Repository) error {
if c.repo.LastCommitCache == nil { if gitRepo.LastCommitCache == nil {
return nil return nil
} }
commitNodeIndex, _ := c.repo.CommitNodeIndex() commitNodeIndex, _ := gitRepo.CommitNodeIndex()
index, err := commitNodeIndex.Get(plumbing.Hash(c.ID.RawValue())) index, err := commitNodeIndex.Get(plumbing.Hash(c.ID.RawValue()))
if err != nil { if err != nil {
return err return err
} }
return c.recursiveCache(ctx, index, &c.Tree, "", 1) return c.recursiveCache(ctx, gitRepo, index, c.Tree(), "", 1)
} }
func (c *Commit) recursiveCache(ctx context.Context, index cgobject.CommitNode, tree *Tree, treePath string, level int) error { func (c *Commit) recursiveCache(ctx context.Context, gitRepo *Repository, index cgobject.CommitNode, tree *Tree, treePath string, level int) error {
if level == 0 { if level == 0 {
return nil return nil
} }
entries, err := tree.ListEntries() entries, err := tree.ListEntries(ctx, gitRepo)
if err != nil { if err != nil {
return err return err
} }
@@ -44,18 +44,18 @@ func (c *Commit) recursiveCache(ctx context.Context, index cgobject.CommitNode,
entryMap[entry.Name()] = entry entryMap[entry.Name()] = entry
} }
commits, err := GetLastCommitForPaths(ctx, c.repo.LastCommitCache, index, treePath, entryPaths) commits, err := getLastCommitForPathsByCommitNode(ctx, gitRepo, index, treePath, entryPaths)
if err != nil { if err != nil {
return err return err
} }
for entry := range commits { for entry := range commits {
if entryMap[entry].IsDir() { if entryMap[entry].IsDir() {
subTree, err := tree.SubTree(entry) subTree, err := tree.SubTree(ctx, gitRepo, entry)
if err != nil { if err != nil {
return err return err
} }
if err := c.recursiveCache(ctx, index, subTree, entry, level-1); err != nil { if err := c.recursiveCache(ctx, gitRepo, index, subTree, entry, level-1); err != nil {
return err return err
} }
} }

View File

@@ -10,19 +10,18 @@ import (
) )
// CacheCommit will cache the commit from the gitRepository // CacheCommit will cache the commit from the gitRepository
func (c *Commit) CacheCommit(ctx context.Context) error { func (c *Commit) CacheCommit(ctx context.Context, gitRepo *Repository) error {
if c.repo.LastCommitCache == nil { if gitRepo.LastCommitCache == nil {
return nil return nil
} }
return c.recursiveCache(ctx, &c.Tree, "", 1) return c.recursiveCache(ctx, gitRepo, c.Tree(), "", 1)
} }
func (c *Commit) recursiveCache(ctx context.Context, tree *Tree, treePath string, level int) error { func (c *Commit) recursiveCache(ctx context.Context, gitRepo *Repository, tree *Tree, treePath string, level int) error {
if level == 0 { if level == 0 {
return nil return nil
} }
entries, err := tree.ListEntries(ctx, gitRepo)
entries, err := tree.ListEntries()
if err != nil { if err != nil {
return err return err
} }
@@ -32,7 +31,7 @@ func (c *Commit) recursiveCache(ctx context.Context, tree *Tree, treePath string
entryPaths[i] = entry.Name() entryPaths[i] = entry.Name()
} }
_, err = walkGitLog(ctx, c.repo, c, treePath, entryPaths...) _, err = walkGitLog(ctx, gitRepo, c, treePath, entryPaths...)
if err != nil { if err != nil {
return err return err
} }
@@ -40,11 +39,11 @@ func (c *Commit) recursiveCache(ctx context.Context, tree *Tree, treePath string
for _, treeEntry := range entries { for _, treeEntry := range entries {
// entryMap won't contain "" therefore skip this. // entryMap won't contain "" therefore skip this.
if treeEntry.IsDir() { if treeEntry.IsDir() {
subTree, err := tree.SubTree(treeEntry.Name()) subTree, err := tree.SubTree(ctx, gitRepo, treeEntry.Name())
if err != nil { if err != nil {
return err return err
} }
if err := c.recursiveCache(ctx, subTree, treeEntry.Name(), level-1); err != nil { if err := c.recursiveCache(ctx, gitRepo, subTree, treeEntry.Name(), level-1); err != nil {
return err return err
} }
} }

View File

@@ -263,13 +263,12 @@ var walkGitLogDebugBeforeNext func() // is used to simulate various edge git pro
// walkGitLog walks the git log --name-status for the head commit in the provided treepath and files // walkGitLog walks the git log --name-status for the head commit in the provided treepath and files
func walkGitLog(ctx context.Context, repo *Repository, head *Commit, treepath string, paths ...string) (map[string]string, error) { func walkGitLog(ctx context.Context, repo *Repository, head *Commit, treepath string, paths ...string) (map[string]string, error) {
headRef := head.ID.String() headRef := head.ID.String()
tree, err := head.SubTree(ctx, repo, treepath)
tree, err := head.SubTree(treepath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
entries, err := tree.ListEntries() entries, err := tree.ListEntries(ctx, repo)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -3,6 +3,14 @@
package git package git
import (
"context"
"io"
"strings"
"gitea.dev/modules/log"
)
// NotesRef is the git ref where Gitea will look for git-notes data. // NotesRef is the git ref where Gitea will look for git-notes data.
// The value ("refs/notes/commits") is the default ref used by git-notes. // The value ("refs/notes/commits") is the default ref used by git-notes.
const NotesRef = "refs/notes/commits" const NotesRef = "refs/notes/commits"
@@ -12,3 +20,80 @@ type Note struct {
Message []byte Message []byte
Commit *Commit Commit *Commit
} }
// GetNote retrieves the git-notes data for a given commit.
// FIXME: Add LastCommitCache support
func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) error {
log.Trace("Searching for git note corresponding to the commit %q in the repository %q", commitID, repo.Path)
notes, err := repo.GetCommit(NotesRef)
if err != nil {
if IsErrNotExist(err) {
return err
}
log.Error("Unable to get commit from ref %q. Error: %v", NotesRef, err)
return err
}
path := ""
tree := notes.Tree()
log.Trace("Found tree with ID %q while searching for git note corresponding to the commit %q", tree.ID, commitID)
var entry *TreeEntry
originalCommitID := commitID
for len(commitID) > 2 {
entry, err = tree.GetTreeEntryByPath(ctx, repo, commitID)
if err == nil {
path += commitID
break
}
if IsErrNotExist(err) {
tree, err = tree.SubTree(ctx, repo, commitID[0:2])
path += commitID[0:2] + "/"
commitID = commitID[2:]
}
if err != nil {
// Err may have been updated by the SubTree we need to recheck if it's again an ErrNotExist
if !IsErrNotExist(err) {
log.Error("Unable to find git note corresponding to the commit %q. Error: %v", originalCommitID, err)
}
return err
}
}
blob := entry.Blob(repo)
dataRc, err := blob.DataAsync()
if err != nil {
log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err)
return err
}
closed := false
defer func() {
if !closed {
_ = dataRc.Close()
}
}()
d, err := io.ReadAll(dataRc)
if err != nil {
log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err)
return err
}
_ = dataRc.Close()
closed = true
note.Message = d
treePath := ""
if idx := strings.LastIndex(path, "/"); idx > -1 {
treePath = path[:idx]
path = path[idx+1:]
}
lastCommits, err := GetLastCommitForPaths(ctx, repo, notes, treePath, []string{path})
if err != nil {
log.Error("Unable to get the commit for the path %q. Error: %v", treePath, err)
return err
}
note.Commit = lastCommits[path]
return nil
}

View File

@@ -1,95 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"context"
"fmt"
"io"
"strings"
"gitea.dev/modules/log"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
)
// GetNote retrieves the git-notes data for a given commit.
// FIXME: Add LastCommitCache support
func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) error {
log.Trace("Searching for git note corresponding to the commit %q in the repository %q", commitID, repo.Path)
notes, err := repo.GetCommit(NotesRef)
if err != nil {
if IsErrNotExist(err) {
return err
}
log.Error("Unable to get commit from ref %q. Error: %v", NotesRef, err)
return err
}
remainingCommitID := commitID
var path strings.Builder
currentTree, err := notes.Tree.gogitTreeObject()
if err != nil {
return fmt.Errorf("unable to get tree object for notes commit %q: %w", notes.ID.String(), err)
}
log.Trace("Found tree with ID %q while searching for git note corresponding to the commit %q", currentTree.Entries[0].Name, commitID)
var file *object.File
for len(remainingCommitID) > 2 {
file, err = currentTree.File(remainingCommitID)
if err == nil {
path.WriteString(remainingCommitID)
break
}
if err == object.ErrFileNotFound {
currentTree, err = currentTree.Tree(remainingCommitID[0:2])
path.WriteString(remainingCommitID[0:2] + "/")
remainingCommitID = remainingCommitID[2:]
}
if err != nil {
if err == object.ErrDirectoryNotFound {
return ErrNotExist{ID: remainingCommitID, RelPath: path.String()}
}
log.Error("Unable to find git note corresponding to the commit %q. Error: %v", commitID, err)
return err
}
}
blob := file.Blob
dataRc, err := blob.Reader()
if err != nil {
log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err)
return err
}
defer dataRc.Close()
d, err := io.ReadAll(dataRc)
if err != nil {
log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err)
return err
}
note.Message = d
commitNodeIndex, commitGraphFile := repo.CommitNodeIndex()
if commitGraphFile != nil {
defer commitGraphFile.Close()
}
commitNode, err := commitNodeIndex.Get(plumbing.Hash(notes.ID.RawValue()))
if err != nil {
return err
}
lastCommits, err := GetLastCommitForPaths(ctx, nil, commitNode, "", []string{path.String()})
if err != nil {
log.Error("Unable to get the commit for the path %q. Error: %v", path.String(), err)
return err
}
note.Commit = lastCommits[path.String()]
return nil
}

View File

@@ -1,91 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"context"
"io"
"strings"
"gitea.dev/modules/log"
)
// GetNote retrieves the git-notes data for a given commit.
// FIXME: Add LastCommitCache support
func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) error {
log.Trace("Searching for git note corresponding to the commit %q in the repository %q", commitID, repo.Path)
notes, err := repo.GetCommit(NotesRef)
if err != nil {
if IsErrNotExist(err) {
return err
}
log.Error("Unable to get commit from ref %q. Error: %v", NotesRef, err)
return err
}
path := ""
tree := &notes.Tree
log.Trace("Found tree with ID %q while searching for git note corresponding to the commit %q", tree.ID, commitID)
var entry *TreeEntry
originalCommitID := commitID
for len(commitID) > 2 {
entry, err = tree.GetTreeEntryByPath(commitID)
if err == nil {
path += commitID
break
}
if IsErrNotExist(err) {
tree, err = tree.SubTree(commitID[0:2])
path += commitID[0:2] + "/"
commitID = commitID[2:]
}
if err != nil {
// Err may have been updated by the SubTree we need to recheck if it's again an ErrNotExist
if !IsErrNotExist(err) {
log.Error("Unable to find git note corresponding to the commit %q. Error: %v", originalCommitID, err)
}
return err
}
}
blob := entry.Blob()
dataRc, err := blob.DataAsync()
if err != nil {
log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err)
return err
}
closed := false
defer func() {
if !closed {
_ = dataRc.Close()
}
}()
d, err := io.ReadAll(dataRc)
if err != nil {
log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err)
return err
}
_ = dataRc.Close()
closed = true
note.Message = d
treePath := ""
if idx := strings.LastIndex(path, "/"); idx > -1 {
treePath = path[:idx]
path = path[idx+1:]
}
lastCommits, err := GetLastCommitForPaths(ctx, notes, treePath, []string{path})
if err != nil {
log.Error("Unable to get the commit for the path %q. Error: %v", treePath, err)
return err
}
note.Commit = lastCommits[path]
return nil
}

View File

@@ -10,10 +10,10 @@ import (
"strconv" "strconv"
) )
// sha1Pattern can be used to determine if a string is an valid sha // sha1Pattern can be used to determine if a string is a valid sha
var sha1Pattern = regexp.MustCompile(`^[0-9a-f]{4,40}$`) var sha1Pattern = regexp.MustCompile(`^[0-9a-f]{4,40}$`)
// sha256Pattern can be used to determine if a string is an valid sha // sha256Pattern can be used to determine if a string is a valid sha
var sha256Pattern = regexp.MustCompile(`^[0-9a-f]{4,64}$`) var sha256Pattern = regexp.MustCompile(`^[0-9a-f]{4,64}$`)
type ObjectFormat interface { type ObjectFormat interface {

View File

@@ -72,7 +72,7 @@ func findLFSFileFunc(repo *git.Repository, objectID git.ObjectID, revListReader
continue continue
case "commit": case "commit":
// Read in the commit to get its tree and in case this is one of the last used commits // Read in the commit to get its tree and in case this is one of the last used commits
curCommit, err = git.CommitFromReader(repo, git.MustIDFromString(commitID), io.LimitReader(batchReader, info.Size)) curCommit, err = git.CommitFromReader(git.MustIDFromString(commitID), io.LimitReader(batchReader, info.Size))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -80,7 +80,7 @@ func findLFSFileFunc(repo *git.Repository, objectID git.ObjectID, revListReader
return nil, err return nil, err
} }
if info, _, err = batch.QueryContent(curCommit.Tree.ID.String()); err != nil { if info, _, err = batch.QueryContent(curCommit.TreeID.String()); err != nil {
return nil, err return nil, err
} }
curPath = "" curPath = ""

View File

@@ -92,15 +92,14 @@ func (repo *Repository) getCommit(id ObjectID) (*Commit, error) {
} }
commit := convertCommit(gogitCommit) commit := convertCommit(gogitCommit)
commit.repo = repo
tree, err := gogitCommit.Tree() tree, err := gogitCommit.Tree()
if err != nil { if err != nil {
return nil, err return nil, err
} }
commit.Tree.ID = ParseGogitHash(tree.Hash) commit.TreeID = ParseGogitHash(tree.Hash)
commit.Tree.resolvedGogitTreeObject = tree commit.Tree().resolvedGogitTreeObject = tree
return commit, nil return commit, nil
} }

View File

@@ -88,7 +88,7 @@ func (repo *Repository) getCommitWithBatch(batch CatFileBatch, id ObjectID) (*Co
} }
return repo.getCommitWithBatch(batch, tag.Object) return repo.getCommitWithBatch(batch, tag.Object)
case "commit": case "commit":
commit, err := CommitFromReader(repo, id, io.LimitReader(rd, info.Size)) commit, err := CommitFromReader(id, io.LimitReader(rd, info.Size))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -149,5 +149,5 @@ func (repo *Repository) WriteTree() (*Tree, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewTree(repo, id), nil return newTree(id), nil
} }

View File

@@ -25,7 +25,7 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) {
return nil, err return nil, err
} }
tree := NewTree(repo, id) tree := newTree(id)
tree.resolvedGogitTreeObject = gogitTree tree.resolvedGogitTreeObject = gogitTree
return tree, nil return tree, nil
} }
@@ -53,7 +53,6 @@ func (repo *Repository) GetTree(idStr string) (*Tree, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
resolvedID := id
commitObject, err := repo.gogitRepo.CommitObject(plumbing.Hash(id.RawValue())) commitObject, err := repo.gogitRepo.CommitObject(plumbing.Hash(id.RawValue()))
if err == nil { if err == nil {
id = ParseGogitHash(commitObject.TreeHash) id = ParseGogitHash(commitObject.TreeHash)
@@ -62,6 +61,5 @@ func (repo *Repository) GetTree(idStr string) (*Tree, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
treeObject.ResolvedID = resolvedID
return treeObject, nil return treeObject, nil
} }

View File

@@ -23,7 +23,6 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) {
switch info.Type { switch info.Type {
case "tag": case "tag":
resolvedID := id
data, err := io.ReadAll(io.LimitReader(rd, info.Size)) data, err := io.ReadAll(io.LimitReader(rd, info.Size))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -37,21 +36,20 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
commit.Tree.ResolvedID = resolvedID tree := commit.Tree()
return &commit.Tree, nil return tree, nil
case "commit": case "commit":
commit, err := CommitFromReader(repo, id, io.LimitReader(rd, info.Size)) commit, err := CommitFromReader(id, io.LimitReader(rd, info.Size))
if err != nil { if err != nil {
return nil, err return nil, err
} }
if _, err := rd.Discard(1); err != nil { if _, err := rd.Discard(1); err != nil {
return nil, err return nil, err
} }
commit.Tree.ResolvedID = commit.ID tree := commit.Tree()
return &commit.Tree, nil return tree, nil
case "tree": case "tree":
tree := NewTree(repo, id) tree := newTree(id)
tree.ResolvedID = id
objectFormat, err := repo.GetObjectFormat() objectFormat, err := repo.GetObjectFormat()
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -6,6 +6,7 @@ package git
import ( import (
"bytes" "bytes"
"context"
"strings" "strings"
"gitea.dev/modules/git/gitcmd" "gitea.dev/modules/git/gitcmd"
@@ -13,24 +14,14 @@ import (
type TreeCommon struct { type TreeCommon struct {
ID ObjectID ID ObjectID
ResolvedID ObjectID
repo *Repository
ptree *Tree // parent tree
} }
// NewTree create a new tree according the repository and tree id func newTree(id ObjectID) *Tree {
func NewTree(repo *Repository, id ObjectID) *Tree { return &Tree{TreeCommon: TreeCommon{ID: id}}
return &Tree{
TreeCommon: TreeCommon{
ID: id,
repo: repo,
},
}
} }
// SubTree get a subtree by the sub dir path // SubTree get a subtree by the sub dir path
func (t *Tree) SubTree(rpath string) (*Tree, error) { func (t *Tree) SubTree(ctx context.Context, gitRepo *Repository, rpath string) (*Tree, error) {
if len(rpath) == 0 { if len(rpath) == 0 {
return t, nil return t, nil
} }
@@ -43,16 +34,15 @@ func (t *Tree) SubTree(rpath string) (*Tree, error) {
te *TreeEntry te *TreeEntry
) )
for _, name := range paths { for _, name := range paths {
te, err = p.GetTreeEntryByPath(name) te, err = p.GetTreeEntryByPath(ctx, gitRepo, name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
g, err = t.repo.getTree(te.ID) g, err = gitRepo.getTree(te.ID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
g.ptree = p
p = g p = g
} }
return g, nil return g, nil

View File

@@ -4,15 +4,17 @@
package git package git
import "context"
// GetBlobByPath get the blob object according the path // GetBlobByPath get the blob object according the path
func (t *Tree) GetBlobByPath(relpath string) (*Blob, error) { func (t *Tree) GetBlobByPath(ctx context.Context, gitRepo *Repository, relpath string) (*Blob, error) {
entry, err := t.GetTreeEntryByPath(relpath) entry, err := t.GetTreeEntryByPath(ctx, gitRepo, relpath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !entry.IsDir() && !entry.IsSubModule() { if !entry.IsDir() && !entry.IsSubModule() {
return entry.Blob(), nil return entry.Blob(gitRepo), nil
} }
return nil, ErrNotExist{"", relpath} return nil, ErrNotExist{"", relpath}

View File

@@ -7,6 +7,7 @@
package git package git
import ( import (
"context"
"path" "path"
"strings" "strings"
@@ -14,7 +15,7 @@ import (
) )
// GetTreeEntryByPath get the tree entries according the sub dir // GetTreeEntryByPath get the tree entries according the sub dir
func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) { func (t *Tree) GetTreeEntryByPath(ctx context.Context, gitRepo *Repository, relpath string) (*TreeEntry, error) {
if len(relpath) == 0 { if len(relpath) == 0 {
return &TreeEntry{ return &TreeEntry{
ID: t.ID, ID: t.ID,
@@ -30,7 +31,7 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
tree := t tree := t
for i, name := range parts { for i, name := range parts {
if i == len(parts)-1 { if i == len(parts)-1 {
entries, err := tree.ListEntries() entries, err := tree.ListEntries(ctx, gitRepo)
if err != nil { if err != nil {
if err == plumbing.ErrObjectNotFound { if err == plumbing.ErrObjectNotFound {
return nil, ErrNotExist{ return nil, ErrNotExist{
@@ -45,7 +46,7 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
} }
} }
} else { } else {
tree, err = tree.SubTree(name) tree, err = tree.SubTree(ctx, gitRepo, name)
if err != nil { if err != nil {
if err == plumbing.ErrObjectNotFound { if err == plumbing.ErrObjectNotFound {
return nil, ErrNotExist{ return nil, ErrNotExist{

View File

@@ -6,12 +6,13 @@
package git package git
import ( import (
"context"
"path" "path"
"strings" "strings"
) )
// GetTreeEntryByPath get the tree entries according the sub dir // GetTreeEntryByPath get the tree entries according the sub dir
func (t *Tree) GetTreeEntryByPath(relpath string) (_ *TreeEntry, err error) { func (t *Tree) GetTreeEntryByPath(ctx context.Context, gitRepo *Repository, relpath string) (_ *TreeEntry, err error) {
if len(relpath) == 0 { if len(relpath) == 0 {
return &TreeEntry{ return &TreeEntry{
ptree: t, ptree: t,
@@ -26,14 +27,14 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (_ *TreeEntry, err error) {
tree := t tree := t
for _, name := range parts[:len(parts)-1] { for _, name := range parts[:len(parts)-1] {
tree, err = tree.SubTree(name) tree, err = tree.SubTree(ctx, gitRepo, name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
name := parts[len(parts)-1] name := parts[len(parts)-1]
entries, err := tree.ListEntries() entries, err := tree.ListEntries(ctx, gitRepo)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -5,6 +5,7 @@
package git package git
import ( import (
"context"
"path" "path"
"slices" "slices"
"strings" "strings"
@@ -78,18 +79,18 @@ type EntryFollowResult struct {
TargetEntry *TreeEntry TargetEntry *TreeEntry
} }
func EntryFollowLink(commit *Commit, fullPath string, te *TreeEntry) (*EntryFollowResult, error) { func EntryFollowLink(ctx context.Context, gitRepo *Repository, commit *Commit, fullPath string, te *TreeEntry) (*EntryFollowResult, error) {
if !te.IsLink() { if !te.IsLink() {
return nil, util.ErrorWrap(util.ErrUnprocessableContent, "%q is not a symlink", fullPath) return nil, util.ErrorWrap(util.ErrUnprocessableContent, "%q is not a symlink", fullPath)
} }
// git's filename max length is 4096, hopefully a link won't be longer than multiple of that // git's filename max length is 4096, hopefully a link won't be longer than multiple of that
const maxSymlinkSize = 20 * 4096 const maxSymlinkSize = 20 * 4096
if te.Blob().Size() > maxSymlinkSize { if te.Blob(gitRepo).Size() > maxSymlinkSize {
return nil, util.ErrorWrap(util.ErrUnprocessableContent, "%q content exceeds symlink limit", fullPath) return nil, util.ErrorWrap(util.ErrUnprocessableContent, "%q content exceeds symlink limit", fullPath)
} }
link, err := te.Blob().GetBlobContent(maxSymlinkSize) link, err := te.Blob(gitRepo).GetBlobContent(maxSymlinkSize)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -99,18 +100,18 @@ func EntryFollowLink(commit *Commit, fullPath string, te *TreeEntry) (*EntryFoll
} }
targetFullPath := path.Join(path.Dir(fullPath), link) targetFullPath := path.Join(path.Dir(fullPath), link)
targetEntry, err := commit.GetTreeEntryByPath(targetFullPath) targetEntry, err := commit.GetTreeEntryByPath(ctx, gitRepo, targetFullPath)
if err != nil { if err != nil {
return &EntryFollowResult{SymlinkContent: link}, err return &EntryFollowResult{SymlinkContent: link}, err
} }
return &EntryFollowResult{SymlinkContent: link, TargetFullPath: targetFullPath, TargetEntry: targetEntry}, nil return &EntryFollowResult{SymlinkContent: link, TargetFullPath: targetFullPath, TargetEntry: targetEntry}, nil
} }
func EntryFollowLinks(commit *Commit, firstFullPath string, firstTreeEntry *TreeEntry, optLimit ...int) (res *EntryFollowResult, err error) { func EntryFollowLinks(ctx context.Context, gitRepo *Repository, commit *Commit, firstFullPath string, firstTreeEntry *TreeEntry, optLimit ...int) (res *EntryFollowResult, err error) {
limit := util.OptionalArg(optLimit, 10) limit := util.OptionalArg(optLimit, 10)
treeEntry, fullPath := firstTreeEntry, firstFullPath treeEntry, fullPath := firstTreeEntry, firstFullPath
for range limit { for range limit {
res, err = EntryFollowLink(commit, fullPath, treeEntry) res, err = EntryFollowLink(ctx, gitRepo, commit, fullPath, treeEntry)
if err != nil { if err != nil {
return res, err return res, err
} }
@@ -125,28 +126,26 @@ func EntryFollowLinks(commit *Commit, firstFullPath string, firstTreeEntry *Tree
return res, nil return res, nil
} }
// returns the Tree pointed to by this TreeEntry, or nil if this is not a tree func (te *TreeEntry) Tree(gitRepo *Repository) *Tree {
func (te *TreeEntry) Tree() *Tree { t, err := gitRepo.getTree(te.ID)
t, err := te.ptree.repo.getTree(te.ID)
if err != nil { if err != nil {
return nil return nil
} }
t.ptree = te.ptree
return t return t
} }
// GetSubJumpablePathName return the full path of subdirectory jumpable ( contains only one directory ) // GetSubJumpablePathName return the full path of subdirectory jumpable ( contains only one directory )
func (te *TreeEntry) GetSubJumpablePathName() string { func (te *TreeEntry) GetSubJumpablePathName(ctx context.Context, gitRepo *Repository) string {
if te.IsSubModule() || !te.IsDir() { if te.IsSubModule() || !te.IsDir() {
return "" return ""
} }
tree, err := te.ptree.SubTree(te.Name()) tree, err := te.ptree.SubTree(ctx, gitRepo, te.Name())
if err != nil { if err != nil {
return te.Name() return te.Name()
} }
entries, _ := tree.ListEntries() entries, _ := tree.ListEntries(ctx, gitRepo)
if len(entries) == 1 && entries[0].IsDir() { if len(entries) == 1 && entries[0].IsDir() {
name := entries[0].GetSubJumpablePathName() name := entries[0].GetSubJumpablePathName(ctx, gitRepo)
if name != "" { if name != "" {
return te.Name() + "/" + name return te.Name() + "/" + name
} }

View File

@@ -23,12 +23,12 @@ func TestFollowLink(t *testing.T) {
// get the symlink // get the symlink
{ {
lnkFullPath := "foo/bar/link_to_hello" lnkFullPath := "foo/bar/link_to_hello"
lnk, err := commit.Tree.GetTreeEntryByPath("foo/bar/link_to_hello") lnk, err := commit.Tree().GetTreeEntryByPath(t.Context(), r, "foo/bar/link_to_hello")
require.NoError(t, err) require.NoError(t, err)
assert.True(t, lnk.IsLink()) assert.True(t, lnk.IsLink())
// should be able to dereference to target // should be able to dereference to target
res, err := EntryFollowLink(commit, lnkFullPath, lnk) res, err := EntryFollowLink(t.Context(), r, commit, lnkFullPath, lnk)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "hello", res.TargetEntry.Name()) assert.Equal(t, "hello", res.TargetEntry.Name())
assert.Equal(t, "foo/nar/hello", res.TargetFullPath) assert.Equal(t, "foo/nar/hello", res.TargetFullPath)
@@ -38,38 +38,38 @@ func TestFollowLink(t *testing.T) {
{ {
// should error when called on a normal file // should error when called on a normal file
entry, err := commit.Tree.GetTreeEntryByPath("file1.txt") entry, err := commit.Tree().GetTreeEntryByPath(t.Context(), r, "file1.txt")
require.NoError(t, err) require.NoError(t, err)
res, err := EntryFollowLink(commit, "file1.txt", entry) res, err := EntryFollowLink(t.Context(), r, commit, "file1.txt", entry)
assert.ErrorIs(t, err, util.ErrUnprocessableContent) assert.ErrorIs(t, err, util.ErrUnprocessableContent)
assert.Nil(t, res) assert.Nil(t, res)
} }
{ {
// should error for broken links // should error for broken links
entry, err := commit.Tree.GetTreeEntryByPath("foo/broken_link") entry, err := commit.Tree().GetTreeEntryByPath(t.Context(), r, "foo/broken_link")
require.NoError(t, err) require.NoError(t, err)
assert.True(t, entry.IsLink()) assert.True(t, entry.IsLink())
res, err := EntryFollowLink(commit, "foo/broken_link", entry) res, err := EntryFollowLink(t.Context(), r, commit, "foo/broken_link", entry)
assert.ErrorIs(t, err, util.ErrNotExist) assert.ErrorIs(t, err, util.ErrNotExist)
assert.Equal(t, "nar/broken_link", res.SymlinkContent) assert.Equal(t, "nar/broken_link", res.SymlinkContent)
} }
{ {
// should error for external links // should error for external links
entry, err := commit.Tree.GetTreeEntryByPath("foo/outside_repo") entry, err := commit.Tree().GetTreeEntryByPath(t.Context(), r, "foo/outside_repo")
require.NoError(t, err) require.NoError(t, err)
assert.True(t, entry.IsLink()) assert.True(t, entry.IsLink())
res, err := EntryFollowLink(commit, "foo/outside_repo", entry) res, err := EntryFollowLink(t.Context(), r, commit, "foo/outside_repo", entry)
assert.ErrorIs(t, err, util.ErrNotExist) assert.ErrorIs(t, err, util.ErrNotExist)
assert.Equal(t, "../../outside_repo", res.SymlinkContent) assert.Equal(t, "../../outside_repo", res.SymlinkContent)
} }
{ {
// testing fix for short link bug // testing fix for short link bug
entry, err := commit.Tree.GetTreeEntryByPath("foo/link_short") entry, err := commit.Tree().GetTreeEntryByPath(t.Context(), r, "foo/link_short")
require.NoError(t, err) require.NoError(t, err)
res, err := EntryFollowLink(commit, "foo/link_short", entry) res, err := EntryFollowLink(t.Context(), r, commit, "foo/link_short", entry)
assert.ErrorIs(t, err, util.ErrNotExist) assert.ErrorIs(t, err, util.ErrNotExist)
assert.Equal(t, "a", res.SymlinkContent) assert.Equal(t, "a", res.SymlinkContent)
} }

View File

@@ -7,6 +7,8 @@
package git package git
import ( import (
"context"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/filemode" "github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/object"
@@ -29,15 +31,15 @@ func (te *TreeEntry) toGogitTreeEntry() *object.TreeEntry {
} }
} }
// Size returns the size of the entry // GetSize returns the size of the entry
func (te *TreeEntry) Size() int64 { func (te *TreeEntry) GetSize(ctx context.Context, gitRepo *Repository) int64 {
if te.IsDir() { if te.IsDir() {
return 0 return 0
} else if te.sized { } else if te.sized {
return te.size return te.size
} }
ptreeGogitTree, err := te.ptree.gogitTreeObject() ptreeGogitTree, err := te.ptree.gogitTreeObject(gitRepo)
if err != nil { if err != nil {
return 0 return 0
} }
@@ -52,10 +54,10 @@ func (te *TreeEntry) Size() int64 {
} }
// Blob returns the blob object the entry // Blob returns the blob object the entry
func (te *TreeEntry) Blob() *Blob { func (te *TreeEntry) Blob(gitRepo *Repository) *Blob {
return &Blob{ return &Blob{
ID: te.ID, ID: te.ID,
repo: te.ptree.repo, repo: gitRepo,
name: te.Name(), name: te.Name(),
} }
} }

View File

@@ -5,25 +5,28 @@
package git package git
import "gitea.dev/modules/log" import (
"context"
// Size returns the size of the entry "gitea.dev/modules/log"
func (te *TreeEntry) Size() int64 { )
func (te *TreeEntry) GetSize(ctx context.Context, gitRepo *Repository) int64 {
if te.IsDir() { if te.IsDir() {
return 0 return 0
} else if te.sized { } else if te.sized {
return te.size return te.size
} }
batch, cancel, err := te.ptree.repo.CatFileBatch(te.ptree.repo.Ctx) batch, cancel, err := gitRepo.CatFileBatch(ctx)
if err != nil { if err != nil {
log.Debug("error whilst reading size for %s in %s. Error: %v", te.ID.String(), te.ptree.repo.Path, err) log.Debug("error whilst reading size for %s in %s. Error: %v", te.ID.String(), gitRepo.Path, err)
return 0 return 0
} }
defer cancel() defer cancel()
info, err := batch.QueryInfo(te.ID.String()) info, err := batch.QueryInfo(te.ID.String())
if err != nil { if err != nil {
log.Debug("error whilst reading size for %s in %s. Error: %v", te.ID.String(), te.ptree.repo.Path, err) log.Debug("error whilst reading size for %s in %s. Error: %v", te.ID.String(), gitRepo.Path, err)
return 0 return 0
} }
@@ -33,12 +36,12 @@ func (te *TreeEntry) Size() int64 {
} }
// Blob returns the blob object the entry // Blob returns the blob object the entry
func (te *TreeEntry) Blob() *Blob { func (te *TreeEntry) Blob(gitRepo *Repository) *Blob {
return &Blob{ return &Blob{
ID: te.ID, ID: te.ID,
name: te.Name(), name: te.Name(),
size: te.size, size: te.size,
gotSize: te.sized, gotSize: te.sized,
repo: te.ptree.repo, repo: gitRepo,
} }
} }

View File

@@ -7,6 +7,7 @@
package git package git
import ( import (
"context"
"io" "io"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
@@ -20,9 +21,9 @@ type Tree struct {
resolvedGogitTreeObject *object.Tree resolvedGogitTreeObject *object.Tree
} }
func (t *Tree) gogitTreeObject() (_ *object.Tree, err error) { func (t *Tree) gogitTreeObject(gitRepo *Repository) (_ *object.Tree, err error) {
if t.resolvedGogitTreeObject == nil { if t.resolvedGogitTreeObject == nil {
t.resolvedGogitTreeObject, err = t.repo.gogitRepo.TreeObject(plumbing.Hash(t.ID.RawValue())) t.resolvedGogitTreeObject, err = gitRepo.gogitRepo.TreeObject(plumbing.Hash(t.ID.RawValue()))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -31,8 +32,8 @@ func (t *Tree) gogitTreeObject() (_ *object.Tree, err error) {
} }
// ListEntries returns all entries of current tree. // ListEntries returns all entries of current tree.
func (t *Tree) ListEntries() (Entries, error) { func (t *Tree) ListEntries(_ context.Context, gitRepo *Repository) (Entries, error) {
gogitTree, err := t.gogitTreeObject() gogitTree, err := t.gogitTreeObject(gitRepo)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -50,8 +51,8 @@ func (t *Tree) ListEntries() (Entries, error) {
} }
// ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees // ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees
func (t *Tree) ListEntriesRecursiveWithSize() (entries Entries, _ error) { func (t *Tree) ListEntriesRecursiveWithSize(_ context.Context, gitRepo *Repository) (entries Entries, _ error) {
gogitTree, err := t.gogitTreeObject() gogitTree, err := t.gogitTreeObject(gitRepo)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -76,6 +77,6 @@ func (t *Tree) ListEntriesRecursiveWithSize() (entries Entries, _ error) {
} }
// ListEntriesRecursiveFast is the alias of ListEntriesRecursiveWithSize for the gogit version // ListEntriesRecursiveFast is the alias of ListEntriesRecursiveWithSize for the gogit version
func (t *Tree) ListEntriesRecursiveFast() (Entries, error) { func (t *Tree) ListEntriesRecursiveFast(ctx context.Context, gitRepo *Repository) (Entries, error) {
return t.ListEntriesRecursiveWithSize() return t.ListEntriesRecursiveWithSize(ctx, gitRepo)
} }

View File

@@ -6,6 +6,7 @@
package git package git
import ( import (
"context"
"io" "io"
"gitea.dev/modules/git/gitcmd" "gitea.dev/modules/git/gitcmd"
@@ -20,13 +21,12 @@ type Tree struct {
} }
// ListEntries returns all entries of current tree. // ListEntries returns all entries of current tree.
func (t *Tree) ListEntries() (Entries, error) { func (t *Tree) ListEntries(ctx context.Context, gitRepo *Repository) (Entries, error) {
if t.entriesParsed { if t.entriesParsed {
return t.entries, nil return t.entries, nil
} }
if t.repo != nil { batch, cancel, err := gitRepo.CatFileBatch(ctx)
batch, cancel, err := t.repo.CatFileBatch(t.repo.Ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -60,9 +60,8 @@ func (t *Tree) ListEntries() (Entries, error) {
if err := DiscardFull(rd, info.Size+1); err != nil { if err := DiscardFull(rd, info.Size+1); err != nil {
return nil, err return nil, err
} }
}
stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-l").AddDynamicArguments(t.ID.String()).WithDir(t.repo.Path).RunStdBytes(t.repo.Ctx) stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-l").AddDynamicArguments(t.ID.String()).WithDir(gitRepo.Path).RunStdBytes(ctx)
if runErr != nil { if runErr != nil {
if gitcmd.IsStderr(runErr, gitcmd.StderrNotValidObjectName) || gitcmd.IsStderr(runErr, gitcmd.StderrNotTreeObject) { if gitcmd.IsStderr(runErr, gitcmd.StderrNotValidObjectName) || gitcmd.IsStderr(runErr, gitcmd.StderrNotTreeObject) {
return nil, ErrNotExist{ return nil, ErrNotExist{
@@ -72,7 +71,6 @@ func (t *Tree) ListEntries() (Entries, error) {
return nil, runErr return nil, runErr
} }
var err error
t.entries, err = parseTreeEntries(stdout, t) t.entries, err = parseTreeEntries(stdout, t)
if err == nil { if err == nil {
t.entriesParsed = true t.entriesParsed = true
@@ -83,12 +81,12 @@ func (t *Tree) ListEntries() (Entries, error) {
// listEntriesRecursive returns all entries of current tree recursively including all subtrees // listEntriesRecursive returns all entries of current tree recursively including all subtrees
// extraArgs could be "-l" to get the size, which is slower // extraArgs could be "-l" to get the size, which is slower
func (t *Tree) listEntriesRecursive(extraArgs gitcmd.TrustedCmdArgs) (Entries, error) { func (t *Tree) listEntriesRecursive(ctx context.Context, gitRepo *Repository, extraArgs gitcmd.TrustedCmdArgs) (Entries, error) {
stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-t", "-r"). stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-t", "-r").
AddArguments(extraArgs...). AddArguments(extraArgs...).
AddDynamicArguments(t.ID.String()). AddDynamicArguments(t.ID.String()).
WithDir(t.repo.Path). WithDir(gitRepo.Path).
RunStdBytes(t.repo.Ctx) RunStdBytes(ctx)
if runErr != nil { if runErr != nil {
return nil, runErr return nil, runErr
} }
@@ -99,11 +97,11 @@ func (t *Tree) listEntriesRecursive(extraArgs gitcmd.TrustedCmdArgs) (Entries, e
} }
// ListEntriesRecursiveFast returns all entries of current tree recursively including all subtrees, no size // ListEntriesRecursiveFast returns all entries of current tree recursively including all subtrees, no size
func (t *Tree) ListEntriesRecursiveFast() (Entries, error) { func (t *Tree) ListEntriesRecursiveFast(ctx context.Context, gitRepo *Repository) (Entries, error) {
return t.listEntriesRecursive(nil) return t.listEntriesRecursive(ctx, gitRepo, nil)
} }
// ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees, with size // ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees, with size
func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) { func (t *Tree) ListEntriesRecursiveWithSize(ctx context.Context, gitRepo *Repository) (Entries, error) {
return t.listEntriesRecursive(gitcmd.TrustedCmdArgs{"--long"}) return t.listEntriesRecursive(ctx, gitRepo, gitcmd.TrustedCmdArgs{"--long"})
} }

View File

@@ -20,7 +20,7 @@ func TestSubTree_Issue29101(t *testing.T) {
// old code could produce a different error if called multiple times // old code could produce a different error if called multiple times
for range 10 { for range 10 {
_, err = commit.SubTree("file1.txt") _, err = commit.SubTree(t.Context(), repo, "file1.txt")
assert.Error(t, err) assert.Error(t, err)
assert.True(t, IsErrNotExist(err)) assert.True(t, IsErrNotExist(err))
} }

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