Compare commits

...
111 Commits
Author SHA1 Message Date
e21c37703e fix(repo): centralize repository-scoped authorization (#39063)
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-08-26 20:12:53 +08:00
GiteaBot 90c43e8e78 [skip ci] Updated translations via Crowdin 2026-08-26 00:23:26 +00:00
7668e7c00d chore: repo compare link (#39088)
Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: silverwind <me@silverwind.io>
2026-08-25 17:52:21 +00:00
38747d48fe fix(pull): keep the merged state in sync with git (#39062)
Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-08-26 01:04:49 +08:00
1680ac24e6 chore: anchor golangci exclusion paths to directories (#39116)
Co-authored-by: Claude (Opus 5) <noreply@anthropic.com>
2026-08-25 16:36:45 +00:00
c8660364d9 fix(asymkey): do not verify OpenPGP signatures with an SSH instance key, require git 2.18 (#39073)
With SIGNING_FORMAT = ssh the OpenPGP verification path builds its
GPGSettings from the instance signing key but leaves the format empty,
so it runs `gpg -a --export` on an SSH public key path. Depending on the
local gpg setup that either exports nothing, so an OpenPGP signed commit
reports gpg.error.generate_hash instead of a missing key, or it fails
outright and logs an export error for every such commit.

Both guards are needed. The first covers SIGNING_KEY set to a path with
SIGNING_FORMAT=ssh; the second covers the shipped default
SIGNING_KEY=default, where the format comes from git's own gpg.format
and never gets reconciled with the hardcoded "openpgp". Drop either one
and a working config goes back to broken.

Also raise minimum git version to 2.18 which was already required before this change.

Fixes: https://github.com/go-gitea/gitea/issues/37452
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-08-25 12:02:31 +00:00
yousimuandGitHub d17ccd4434 fix(repo): prevent MarkAsBrokenEmpty when repository is being migrated (#39091) 2026-08-25 12:01:16 +02:00
GiteaBot 9eb4a9afad [skip ci] Updated translations via Crowdin 2026-08-25 00:23:02 +00:00
silverwindandGitHub 0b1067484f chore: bump declaration-strict-value and reconfigure it (#39080)
Bump https://github.com/AndyOGo/stylelint-declaration-strict-value and
configure it to avoid needing these lint exclusions.

Related:
https://github.com/AndyOGo/stylelint-declaration-strict-value/issues/201
2026-08-24 20:05:37 +00:00
Artem LytkinandGitHub 2a17cf7ded fix(git): parse co-author trailers that are not RFC 5322 addresses (#39076)
Bot co-authors like `dependabot[bot]` render as one long string with the
email inside the name, and never get truncated, so they overflow the
column.

Co-author idents are parsed with `net/mail`, but a git ident isn't an
email address. `mail.ParseAddress` rejects the whole value when the name
holds characters RFC 5322 reserves, like a `[bot]` suffix or a comma, so
the error branch keeps the raw trailer as the display name and throws
the address away. No address means no `mailto:` link, and the anchor is
what `.avatar-stack-names` truncates.

So parse the angle-addr ourselves when `net/mail` won't take it.
Splitting on the last `<` is safe because git strips angle brackets from
idents. The bare-name branch gets the class too.

Fixes https://github.com/go-gitea/gitea/issues/38949
2026-08-24 19:06:30 +00:00
Artem LytkinandGitHub 6b929ccb15 fix(pull): name the head repository in default compare links (#39075)
The "New Pull Request" buttons and the `/pulls/new/{branch}` redirect
build their compare link as `{owner}:{branch}`. If a fork and its parent
share an owner, through ALLOW_FORK_INTO_SAME_OWNER, or after a transfer,
that head resolves back to the base repo, so the link compares the base
against itself and 404s on a branch that only exists in the fork.

Switching to `{owner}/{repo}:{branch}` names the head repo
unambiguously, and it's what the compare page's own links already use.

Also clears the 404 in #37649; the archived-parent half of that report
is separate.
2026-08-24 18:46:40 +00:00
silverwindandGitHub 32728fc581 chore: misc go 1.27 tweaks (#39069)
Follow-up to https://github.com/go-gitea/gitea/pull/39068, which
disabled `modernize` entirely.

- re-enable `modernize`, with only the new `embedlit` rule disabled. It
flattens embedded struct literals across ~145 files, and orphans imports
in 6 of them that the fixer does not remove
- apply the rest of the suite: `errors.AsType`, `reflect.TypeAssert`,
`strings.Cut`, and dropping the legacy import comment
- use the new stdlib `uuid` package, `github.com/google/uuid` becomes
indirect
- use `strings.CutLast` in place of manual `LastIndex` slicing in label
scopes, email domains and the diff tree list
- take the header lint skip dirs from the `go.mod` `ignore` directive
and skip dot-directories, instead of hardcoding the list

Assisted-by: Claude Code:claude-opus-5
2026-08-24 18:26:10 +00:00
59a43c8733 fix(packages/npm): use PathEscape for package name in tarball URL (#39061)
## Summary

Replace `url.QueryEscape` with `url.PathEscape` when building
`dist.tarball` in the npm package registry. `QueryEscape` leaves `@`
unescaped, producing `dist.tarball` URLs like `@scope%2Fname` for scoped
packages — which npm clients cannot resolve. `PathEscape` produces the
RFC 3986 path-segment-safe encoding (`%40scope%2Fname`) that the npm
registry URL format requires.

## Reproduction

1. Publish a scoped npm package (`@scope/name@1.0.0`) to a Gitea package
registry.
2. Inspect the `dist.tarball` field in the metadata response.
3. Observe that the package name in the URL is `@scope%2Fname` instead
of `%40scope%2Fname`.
4. `npm install @scope/name` fails because npm rejects the malformed
tarball URL.

## Fix

One-line change in `routers/api/packages/npm/api.go`:

```diff
-Tarball:   fmt.Sprintf("%s/%s/-/%s/%s", registryURL, url.QueryEscape(pd.Package.Name), url.PathEscape(pd.Version.Version), url.PathEscape(pd.Files[0].File.LowerName)),
+Tarball:   fmt.Sprintf("%s/%s/-/%s/%s", registryURL, url.PathEscape(pd.Package.Name), url.PathEscape(pd.Version.Version), url.PathEscape(pd.Files[0].File.LowerName)),
```

## Tests

- `routers/api/packages/npm/api_test.go`: extended
`TestCreatePackageMetadataResponse` to use `Package.Name: "@scope/test"`
and added an `assert.Equal` on `Dist.Tarball` (per review feedback to
consolidate the test instead of adding a new one).
- `tests/integration/api_packages_npm_test.go`: switched three
`url.QueryEscape(packageName)` to `url.PathEscape(packageName)` to match
the new production encoding (lines 125, 126, 446). The `TestPackageNpm`
assert at line 219 against `pmv.Dist.Tarball` now passes for scoped
packages.

The unit test fails on `main` (excluding the `QueryEscape` →
`PathEscape` swap) and passes with the fix.

## Related

Closes #39060.

## Disclosure

This contribution was prepared with assistance from an AI coding
assistant (limited to language polishing in maintainer-facing messages).
The contributor reviewed and validated all changes, including the test
cases.

---------

Signed-off-by: Dmitriy Chudnyi <dmitriy@chudnyi.com>
Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-08-24 18:00:44 +00:00
3a806a58d0 fix(attachments): enforce owning repository path (#39048)
Reject attachment requests routed through a repository other than the
attachment owner.

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-08-24 17:32:23 +00:00
GiteabotandGitHub 4c915ae0c2 chore(deps): update dependencies (#39066) 2026-08-24 17:05:31 +00:00
silverwindandGitHub 9914db8898 chore: add modelmigration to GO_DIRS (#39072)
`modelmigration` was missing here since its creation, leading to those
files not being covered by fmt.
2026-08-24 17:49:16 +02:00
wxiaoguangandGitHub 51b8da8b01 chore: update Go to v1.27 (#39068)
Only made some necessary changes:

1. remove `GOEXPERIMENT`, only use jsonv2
1. `make fmt`
* `SigningKey` and `Signature` were affected due to some bugs in the
toolchain, so rewrote them
1. remove or fix fragile magic numbers and strings
    * the outputs of image/gzip/zlib packages are different
1. update "nolint" comments for the changed lint behaviors
1. add `tls.MLKEM1024`
2026-08-24 07:28:17 +00:00
GiteaBot e50e4ed869 [skip ci] Updated translations via Crowdin 2026-08-24 00:24:14 +00:00
McMichalKandGitHub 2bcf950b78 feat(diff): Add search and extension filter to diff sidebar (#37068)
Adds a search box and a file-extension filter to the pull request diff
sidebar, so reviewers can narrow a large diff down to the files they
care about.

Both filters apply to the file tree and to the diff itself. The
extension menu follows GitHub: extensions sorted alphabetically,
dotfiles and extension-less files in their own buckets, and the
selection kept in the same `file-filters[]` query parameter, so a
filtered view is shareable and survives a reload.

The menu can list every extension in a diff, so `createTippy` gains an
opt-in `limitSizeToViewport` option that caps a popup to the space left
in the viewport and scrolls its content. Popups that do not ask for it
are unchanged.

Closes https://github.com/go-gitea/gitea/issues/27256
Signed-off-by: silverwind <me@silverwind.io>
Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-08-23 18:31:22 +02:00
4852091e85 fix(auth): record last sign-in on reverse proxy login (#38672)
Reverse proxy and SSPI logins establish a session but never recorded
`last_login_unix`, so those users stayed "Never Signed-In" in admin.

The write is folded into the language update that `handleSignIn` already
does, so it stays at one query and only runs when a session is
established.

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

---------

Co-authored-by: roman s <roman.sukach@dust-labs.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-08-23 08:39:44 +00:00
1c16f04bf5 fix(db): make paginated database reads always require "order" option (#39017)
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-08-23 08:18:18 +00:00
0bed1232ee fix(packages): restrict limited owner package access (#39043)
Apply restricted-viewer visibility rules when resolving package access.

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-08-23 15:57:51 +08:00
adc3db1f27 fix(api): enforce organization listing token scope (#39041)
Enforce organization token scope before listing organizations and retain
public-only filtering.

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-08-23 07:23:16 +00:00
55a5f50961 fix(actions): enforce workflow badge token scope (#39044)
Apply repository token-scope and public-only checks to workflow badges.

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-08-23 06:59:58 +00:00
204c0bafd3 fix(repo): require organization owners for team access (#39046)
Require organization ownership before changing repository team
associations when team access is restricted.

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-08-23 06:35:55 +00:00
bedd2afb47 fix(org): hide limited organizations from restricted users (#39047)
Do not expose limited organization memberships to restricted viewers.

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-08-22 23:11:04 -07:00
GiteaBot d8f0e7e679 [skip ci] Updated translations via Crowdin 2026-08-23 00:24:38 +00:00
1fa6465efd feat(actions)!: add RUN_RETENTION_DAYS to delete old action runs (#38855)
Gitea keeps completed Actions runs forever. Artifacts and logs expire on
their own schedule, but the run rows never go away, so `action_run` and
its child tables grow without bound.

Adds `RUN_RETENTION_DAYS` to delete completed runs along with their
jobs, tasks and anything the earlier expiries left behind. It defaults
to 400 days, matching how long GitHub keeps run history browsable. A
dedicated `cleanup_action_runs` cron task performs the cleanup, so
admins can schedule it separately from the nightly artifact and log
sweep.

`0` now means "keep forever" for all three retention settings, where
`LOG_RETENTION_DAYS` and `ARTIFACT_RETENTION_DAYS` previously took it
literally and deleted everything at the next sweep.

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

----

## ⚠️ BREAKING ⚠️

`RUN_RETENTION_DAYS` defaults to 400, so completed runs older than that
are deleted when the cron task next runs at midnight. Set
`RUN_RETENTION_DAYS = 0` before upgrading to keep all runs.

---------

Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-08-22 21:32:59 +00:00
e6af4c341c fix(repo): preserve transfer recipient collaboration (#39042)
Remove temporary recipient access after a transfer ends while preserving
existing collaboration.

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-08-22 19:43:42 +00:00
9251eeb66b fix(markup): enforce same-repository issue access (#39045)
Enforce Issues and Pull Requests access for references within the
current repository.

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-08-22 19:13:53 +00:00
5fc3cec87f fix(actions): verify raw artifact signatures first (#39049)
Validate raw-artifact signatures before resolving the requested
artifact.

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-08-22 18:47:55 +00:00
51e42d4b11 fix: drop queued job updates for deleted runs instead of requeueing forever (#39037)
When a repository is deleted while one of its Actions runs still has a
pending job update in the emitter queue, `checkJobsByRunID` returns an
error because the run no longer exists. The queue handler in
`jobEmitterQueueHandler` treats every error as unhandled and requeues
the item, creating an infinite retry loop that fills the log with error
messages.

### Changes

1. **`services/actions/job_emitter.go`** — swap the `!exist`/`err` check
order so a database error is reported first, then treat a non-existent
run as handled (nil error). The queue consumer drops the item instead of
requeueing it.

2. **`services/actions/job_emitter_test.go`** — add
`Test_checkJobsByRunID_DeletedRunIsHandled`, which verifies that a
deleted run produces nil (handled, not requeued).

### Related issue

Fixes #39034

---------

Co-authored-by: bircni <bircni@icloud.com>
2026-08-22 13:55:08 +00:00
66d6f74cb0 test: speed up tests, fix transaction bug (#39030)
Speed up tests: `make test-backend` 103s to 37s, `make test-integration`
908s to 852s.

Most of it is a detached system notice insert blocking on the SQLite
write lock until the busy timeout expired, and `ExternalServiceHTTP`
re-probing on every call with an untimed `http.Get`.

- fixed one correctness bug with nested transactions: files were deleted
while the outer transaction was open, so a later failure could roll the
database back with the files gone
- git push branch counts were far above the hook batch size
- Fix makefile dependencies so running tests and lint work in fresh
worktrees.

---------

Co-authored-by: Giteabot <teabot@gitea.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-08-22 11:04:35 +00:00
cce2360846 build(release): use native golang toolchain for official release builds (#37828)
Official releases are built by Golang toolchain with CGO disabled.

For packagers who need to cross-compile with CGO, use "build" target
with proper TAGS/LDFLAGS/CGO_CFLAGS to make "$(EXECUTABLE)" target run
the "go build" command.

By the way, drop i386 arch support

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-08-22 10:38:21 +00:00
89c7019a3c fix(repo): limit gitignore template selections (#39027)
Bound gitignore template selections at both web and API request
boundaries before repository initialization.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-08-22 10:03:17 +00:00
84b67d50a6 fix(base): correct natural sort of numbers with leading zeros (#38163)
### Description

`NaturalSortCompare` (`modules/base/natural_sort.go`) compares two
numeric run parts by **raw string length**:

```go
if len(part1) != len(part2) {
    return len(part1) - len(part2)
}
```

"Longer digit string = larger number" only holds without leading zeros.
With zero-padded numbers the comparison inverts:

- `file0001` vs `file2` → claims `file0001 > file2`, but `1 < 2`
- `a08` vs `a9` → claims `a08 > a9`, but `8 < 9`

This affects any natural-ordered listing where zero-padded and shorter
unpadded numbers mix (branch/tag/file names, etc.).

### Fix

Strip leading zeros before comparing digit-count magnitude; on equal
magnitude fall back to collation, then to the original length so fewer
leading zeros sort first. Added a small `naturalSortTrimZeros` helper
(keeps one char so `"000"` → `"0"`).

Signed-off-by: Seonghyun Hong <s3onghyun.hong@gmail.com>
Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-08-22 09:40:49 +00:00
fa0b39a42b fix(release): separate publication time from the release date (#36761)
`published_at` was an alias for `created_at`, so a release created from
an existing tag reported that tag's commit date as its publication time,
and drafts reported one despite never having been published. It is now
stored separately, set when a release is published and null for drafts.

`created_at` in turn means the date of the commit the release points at,
matching what GitHub documents it to be, and the latest release is
selected by it again. Publishing a release for an old commit no longer
takes over the latest badge, and a tag created in the web UI is dated
the same way as one pushed from the CLI.

Fixes https://github.com/go-gitea/gitea/issues/11206
Fixes https://github.com/go-gitea/gitea/issues/38714
Fixes https://github.com/go-gitea/gitea/issues/31789

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-08-22 08:59:37 +00:00
6bb6ce678b fix(api): hide limited users from restricted viewers (#39004)
Use the canonical profile-visibility check for user API content and
prevent restricted users from enumerating public repositories owned by
limited users.

This keeps feeds, heatmaps, keys, and issue search consistent with
profile visibility.

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-08-22 08:08:36 +00:00
f7072b0305 feat(api): list all packages for site administrators (#38968)
Add `GET /admin/packages` so site administrators can review packages
across every owner without querying each owner separately.

It returns the same package version representation as `GET
/packages/{owner}` and supports `page`, `limit`, `type`, and `q`
filters.

---
Assisted by Codet(DeepSeek)

---------

Signed-off-by: bircni <bircni@icloud.com>
Signed-off-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-08-22 09:29:25 +02:00
silverwindandGitHub 2d6fea5bdf enhance: add permalinks to pull request reviews (#38849)
1. Make review threads linkable via `#pullrequestreview-<reviewID>`
2. Improve CSS so username and timestamp go colored on hover.
3. CSS cleanup, remove dead rules, nonexistant class name, make `.suppressed` actually do what it says in the doc above.
2026-08-22 02:13:43 +00:00
bircniandGitHub 1f349eb1eb fix(migrations): cancel GitLab version probes (#39023)
Bind the GitLab version probe to the migration context so a cancelled
migration does not remain blocked on a remote response.
2026-08-22 01:54:07 +00:00
wxiaoguangandGitHub d2bc0097bc fix: make local queue PopItem can be notified (#39011) 2026-08-22 01:30:41 +00:00
bircniandGitHub 5eba4f92ce fix(migrations): bound OneDev version responses (#39024)
Limit OneDev version responses before parsing so a remote server cannot
make a migration retain an unbounded response.
2026-08-22 00:54:20 +00:00
bircniandGitHub aa96725ae7 fix(packages): limit Swift package manifests (#39025)
Bound the number and aggregate size of Swift manifests retained from an
uploaded archive.
2026-08-22 00:11:29 +00:00
bircniandGitHub fa4c0b1cf6 fix(packages): limit Maven checksum uploads (#39028)
Bound checksum uploads to the maximum usable digest length before
buffering their content.
2026-08-21 23:41:12 +00:00
bircniandGitHub 2b81cbbe70 fix(packages): bound Alpine metadata entries (#39026) 2026-08-22 01:17:04 +02:00
bircniandGitHub ad05aaee80 fix(actions): enforce fork pull request trust boundaries (#39005)
Preserve fork pull request restrictions across review-triggered
workflows, reusable workflow access, job scheduling, and filtered
workflow statuses.

This prevents untrusted fork workflow content from bypassing approval,
accessing private reusable workflows, or satisfying protected status
checks.


_Assisted-by: Codex:GPT-5_
2026-08-21 12:35:28 +00:00
bircniandGitHub a52e5f53c0 fix(git): restrict hook permissions (#39008)
Create delegate hook files and directories without group or other write
access, including correcting existing hook directories.

_Assisted-by: Codex:GPT-5_
2026-08-21 07:52:23 +00:00
bircniandGitHub db24633e6d fix(api): enforce repository creation token authorization (#39007)
Reject public-only tokens for repository migrations and require
repository scope for canonical organization repository creation. This
aligns both routes with the existing token authorization boundaries.

_Assisted-by: Codex:GPT-5_
2026-08-21 07:28:23 +00:00
bircniandGitHub 920b5f1e68 fix(api): enforce public-only scope for compare heads (#39006)
Enforce public-only token scope for repositories resolved as compare
heads.

_Assisted-by: Codex:GPT-5_
2026-08-21 07:06:56 +00:00
bircniandGitHub 7306d5aff8 fix(repo): hide repositories of hidden owners (#39009)
Exclude public repositories owned by hidden individual accounts from
broad repository listings, while preserving visibility through explicit
access and ownership.

_Assisted-by: Codex:GPT-5_
2026-08-21 06:32:57 +00:00
GiteaBot fcc23af280 [skip ci] Updated translations via Crowdin 2026-08-21 01:49:11 +00:00
Julian ScholleandGitHub fe567d26c1 fix(actions): allow larger scheduled workflows (#38985)
MySQL stores `action_schedule.content` as `BLOB`, limiting scheduled
workflow definitions to 65,535 bytes. Oversized workflows fail schedule
refresh and can also suppress default-branch push handling.

Store scheduled workflow content as `LONGBLOB`, migrate existing MySQL
columns, and cover the migration by persisting 65,536 bytes.

Fixes https://github.com/go-gitea/gitea/issues/38613
2026-08-21 01:06:44 +02:00
bircniandGitHub ffd982c7ab fix(actions): show "Complete job" logs when the last step is skipped (#38939)
`FullSteps` only gave the synthetic "Complete job" step the remaining
log range when the last step that had run was also the final step of the
job. A skipped step does not count as having run, so any job ending in a
skipped step left the post step with an empty range: its logs were
stored but never rendered, and the duration showed as `0s`.

Reproducible with any job whose last step is skipped, which is common
for failure notifications:

```yaml
steps:
  - run: echo hello
  - run: echo never
    if: failure()
```

The gate now checks whether the final step is done, which preserves the
behaviour from https://github.com/go-gitea/gitea/pull/29926 of showing
the post step as waiting while steps are still pending.
--> Regression from https://github.com/go-gitea/gitea/pull/29926

---------

Signed-off-by: bircni <bircni@icloud.com>
2026-08-20 19:06:21 +00:00
475e51c7e0 fix: avoid enumerating every public repository in issue search (#38992)
Both issue search endpoints resolve their repository filter with
`SearchRepositoryIDs` and pass the result to the indexer as `RepoIDs`.
They mean
to leave public repositories to the indexer, but
`SearchRepoOptions.AllPublic` is
only read when `OwnerID > 0`, so without an `owner` filter the flag does
nothing
and every public repository is enumerated, without a `LIMIT`, into
`repo_id IN (...)`.

Those IDs are redundant, as `allPublic` is passed to the indexer, which
already
matches every public repository. On a large instance this binds tens of
thousands
of parameters and can fail in the driver, making the endpoint return 500
for every
filter. Admins are worst hit, as `SearchRepositoryCondition` skips their
accessible-repository condition and enumerates the whole table.

Restrict the enumeration to private repositories. The result set is
unchanged, as
the dropped IDs are a subset of what `allPublic` matches.

Both endpoints held copies of this block, so it moves to
`routers/common`.

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-08-20 16:29:54 +00:00
wxiaoguangandGitHub 943f026844 refactor: deploy key and private route handlers (#38999)
clean up legacy code, fix various bugs:

* add missing "return"
2026-08-20 15:57:17 +00:00
61be9fcdfa chore: update eslint and stylelint configs and re-sync modern-normalize (#38982)
- update the vendored `modern-normalize` to v3.0.1
- require descriptions for lint disables in TS and CSS, same as we
already have in Go.
- disable core rules covered by `regexp/*` and `unicorn/*`, and ones
that cannot fire
- stop applying vitest rules to the playwright files in `tests/e2e`
- enable 7 stylelint rules, mostly `no-unknown` and `no-invalid` checks
- drop 2 unnecessary vendor prefixes (safari v17+, chrome v120+)
- look up ids via `querySelector` with `CSS.escape` instead of
`getElementById`
- remove stale doc about `@ts-expect-error`, it's forbidden
- misc dev doc fixes

Every declaration that `modern-normalize` v3 removes was checked against
chromium, webkit and firefox defaults first. The `hr` color and the
`:-moz-focusring` outline are kept as documented deviations, dropping
those does change rendering.

---------

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-08-20 09:57:56 -04:00
c778c6c920 enhance: use browser's locale to detect week's first day for the contribution map (#38995)
fix #6058

---------

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: silverwind <me@silverwind.io>
2026-08-20 07:10:15 +00:00
fa5d876171 fix(actions): Fix how jobs in matrixes are grouped (#38980)
The workflow graph decided which job rows belonged to the same matrix by
parsing display names: it stripped a trailing `" (...)"` off `name` and
grouped rows sharing the prefix. That guesses at a string the user
controls, and it fails both ways. `jobparser` only appends the `
(<combination>)` suffix when `name:` contains no `${{ }}`, so a leg
named `E2E on ${{ matrix.browser }}` never grouped, while two unrelated
jobs `build (fast)` and `build (slow)` folded into one bogus matrix
panel.

Matrix legs already have a real identity: expansion clones one row per
combination, all sharing the workflow's `JobID` and differing only in
`Name`. Group on that instead, so a matrix is whatever the backend says
it is. Matrix expansion state is keyed on the graph node id for the same
reason.

Closes https://github.com/go-gitea/gitea/issues/38975, though that
report's own example already groups on main, since `explicit (${{
matrix.leg }})` interpolates to a name that still ends in a suffix. The
interpolated shapes above are the broken ones.

Assisted-by: Claude Code:claude-opus-5
Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-08-20 05:41:14 +00:00
silverwindandGitHub 8f7eb9f161 enhance(ui): forced colors mode enhancements (#38991)
Improve various UI elements while in [forced color
mode](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/forced-colors).
2026-08-20 05:11:57 +00:00
silverwindandGitHub ed4d7ea08d fix: resolve YAML anchors and aliases in Actions workflows (#38984)
Workflows using YAML anchors are rejected as invalid, because a workflow
is split into one document per job and an alias whose anchor lands in
another job's document no longer resolves.

Aliases are now expanded once, right after the workflow is parsed and
before anything reads or splits it, bounded like GitHub's parser so
nested aliases cannot expand without limit. Merge keys stay unsupported,
as they are upstream.

Fixes https://github.com/go-gitea/gitea/issues/38983
Signed-off-by: silverwind <me@silverwind.io>
2026-08-20 05:03:10 +00:00
GiteaBot 89b891b168 [skip ci] Updated translations via Crowdin 2026-08-20 01:44:24 +00:00
IaroslavandGitHub 35786a6ca1 fix(lfs): ensure lock listing paginates with a total order (#38850)
`GetLFSLockByRepoID` applies `LIMIT`/`OFFSET` to a query with no `ORDER
BY`. The order of such a query is unspecified (according to the SQL
standard), so the resulting queryset might be inconsistent.

These locks AFAIK are never updated, so in practice the order is
insertion-based, but that's not guaranteed.
2026-08-19 11:54:21 -07:00
4e813a262f fix: resolve actions commit status permission per repository (#38977)
Various pages did not display the correct action run list tooltips. Fix
those tooltips like here on the `/pulls` page:

`ctx.Repo.Permission` is the zero value outside a repository route, so
on `/pulls`, `/issues`, `/notifications/subscriptions` and the dashboard
repo list the commit status "Details" link was always stripped. The live
job status is looked up from that target URL, so running checks also
rendered as a static pending dot instead of a spinner.

Resolve the Actions unit permission per repository instead.

Also drops the releases page's gate on *loading* statuses, which hid
external CI results from anyone without Actions read; it now loads them
and hides only the URL, like every other page.

Co-authored-by: bircni <bircni@icloud.com>
2026-08-19 20:09:00 +02:00
GiteabotandGitHub e355c39e91 chore(deps): update dependency go to v1.26.7 (#38987) 2026-08-19 16:32:52 +00:00
wxiaoguangandGitHub f261adb53f chore: form binding trim space (#38978)
Use "binding:TrimSpace" instead of fragile IsEmptyString

And fix a bug in locale's `HasKey`: it should also try the default
language if current language doesn't have the translation key, a new
test is added.
2026-08-19 12:50:06 +00:00
c121f02a7e fix: honor environment variables during install (#38974)
Environment variables must be applied to the "install form" config
before the config values are used.

Fixes #38911

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-08-19 15:19:49 +08:00
wxiaoguangandGitHub 6904f6480c refactor: http request binding (#38971)
Better than before, still not good enough (more work can be done in the
future)

And add the missing error handling in the PrivateContext "bind"
middleware.

By the way, picked some "TrimSpace" changes from "fix: trim whitespace
from SMTP address and port - #38934" (fix #38926)
2026-08-19 14:15:42 +08:00
GiteaBot 6c425fae6e [skip ci] Updated translations via Crowdin 2026-08-19 01:45:26 +00:00
5433c23dec enhance(ui): tint toast backgrounds by level (#38919)
Toasts now use the same tinted backgrounds and borders as the flash
messages, replacing the solid full-color style. The first commit reverts
https://github.com/go-gitea/gitea/pull/38842, the second re-applies it
with tinting.

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-08-18 21:13:30 +02:00
silverwindandGitHub 83af7aa92e ci: improve caching (#38958)
- only `cache-seeder` writes caches, every other workflow restores.
Saves were being rejected once the repo went over its cache budget,
leaving main's caches stale and PR runs building cold
- seed the pnpm store and uv caches next to the go ones, so PRs
warm-start on them rather than installing from scratch
- prune keeps a single generation per key, including across go versions,
where a toolchain bump leaves the previous build cache unusable.
Reclaims ~2.6 GB immediately
- prune runs every 6h instead of daily and trims to 6 GB, since CodeQL
writes ~200 MB per push to main from outside this repo's workflows
- pull requests and release branches no longer write pnpm, uv and binfmt
caches, whose ref-scoped copies are never read again

---------

Signed-off-by: silverwind <me@silverwind.io>
2026-08-18 18:26:03 +00:00
wxiaoguangandGitHub c95e3f3b00 refactor: private endpoints (#38964)
1. remove dead code (SetDefaultBranch)
2. remove useless and unsafe code (AddLogger)
2026-08-18 08:51:47 +00:00
c082b9a5ff fix: grant limited-org unit read access to authenticated non-members (#38871)
Fixes #38870

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-08-18 06:58:37 +00:00
e8e1973e16 fix: allow anonymous theme switching when REQUIRE_SIGNIN_VIEW is set (#38956)
Fixes https://github.com/go-gitea/gitea/issues/38950


Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-08-18 04:02:52 +00:00
GiteaBot 19ee791fe7 [skip ci] Updated translations via Crowdin 2026-08-18 01:43:54 +00:00
silverwindandGitHub 3842e021e0 fix(actions): drop wrapper span around the action status icon (#38957)
Fixes https://github.com/go-gitea/gitea/issues/38955
2026-08-17 22:46:49 +00:00
silverwindandGitHub df71d5f5e2 test: run frontend unit tests in browsers (#38860)
Run them in headless [vitest browser
mode](https://vitest.dev/guide/browser/) in chromium and firefox.
Similar UX than current tests, it's about 5 times as slow (goes from 1s
to 5s on my machine), but definitely worth it as it removes all
happy-dom problems.

---------

Signed-off-by: silverwind <me@silverwind.io>
2026-08-17 22:22:54 +00:00
Minjie FangandGitHub 55e7cafcb6 chore(maintainers): add wingsallen as maintainer (#38913)
[PRs](https://gitea.com/gitea/tea/commits/branch/main/search?q=wingsallen&all=)

@wingsallen on gitea.com
2026-08-17 22:03:18 +00:00
ed4a23e893 enhance: inherit team access for all units (#38938)
Admin and write team authorize now grant that mode on every unit,
including units added later, instead of only rows present in
`team_unit`. Granular teams keep `authorize=none` and explicit unit
rows.

Closes the `TEAM-UNIT-PERMISSION` design gap from
https://github.com/go-gitea/gitea/pull/34128.

Maybe also fix #15962 (actually maybe it had been fixed before, the root
cause is out-of-sync "access" table)


## Screenshots

only writing selected:
<img width="1399" height="1007" alt="image"
src="https://github.com/user-attachments/assets/1d1b4c49-a59a-47b6-998f-0464a067395b"
/>


_Created with the help of AI_

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-08-17 20:30:24 +00:00
1cf904f101 feat(repo): add quick repository switcher to repo header (#38188)
Add a GitHub-style quick repo switcher: a caret next to the owner/repo
breadcrumb
opens a dropdown that lists and searches the current owner's
repositories and
navigates to the selected one. The current repository is marked with a
check, and
private/fork repos show an icon.

Also, fix various bugs in fomtantic dropdown remote query

## Screenshots

<img width="505" height="198" alt="image"
src="https://github.com/user-attachments/assets/9f673d1b-fe60-41f0-b9e2-b00dc43720b5"
/>

Fixes #38187

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-08-17 20:08:15 +00:00
GiteabotandGitHub e223c42ee6 fix(deps): update module golang.org/x/mod to v0.40.0 [security] (#38914) 2026-08-17 19:29:07 +00:00
Hsukqi LeeandGitHub 551a6bb3a4 fix(issues): sort scoped labels by exclusive order in dropdowns (#38893)
Closes #38872

Labels in the label selection dropdown (issue/PR sidebar, new issue
form) were always listed alphabetically, so a scoped set like the
default Priority labels showed up as Critical, High, Low, Medium even
though each label carries an exclusive order.

This adds `CompareLabelForDisplay`/`SortLabelsForDisplay` in
`models/issues`: labels are grouped by their exclusive scope and sorted
by exclusive order within a scope (unordered ones last), falling back to
name order. The sorting is applied to the issue page sidebar data and
the shared label filter data, so the filter dropdown on the issue list
gets the same ordering.

Unscoped labels are unaffected and still sort by name. Includes a unit
test covering the default Priority label set.
2026-08-17 18:47:28 +00:00
7857c5f843 feat(user): Personal access tokens can be regenerated (#38907)
Lets users regenerate a personal access token's value in place, keeping
its name and scopes, instead of deleting and recreating it. Useful when
a token was shared with a third party (e.g. an AI agent) and needs to
be invalidated immediately without redoing scope selection.

Follows the same pattern already used for OAuth2 application client
secrets (`GenerateClientSecret`/`RegenerateSecret`).

**Testing**: added a model unit test and a web integration test;
manually
verified in the running dev server that the old token stops
authenticating
and the new one works immediately after regenerating.

<img width="1040" height="245" alt="image"
src="https://github.com/user-attachments/assets/4de0d8b4-1fc4-49cf-a859-95e24d0b2c0a"
/>

Fixes #38683.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-08-17 18:17:16 +00:00
silverwindandGitHub 346e6bab67 ci: install node for renovate post-upgrade tasks (#38953)
Containerbase declares `node` as the parent of `pnpm`, so `install-tool
pnpm` aborts with `MissingParent` (exit 16) when node was never
installed as a containerbase tool. Renovate's npm manager installs node
itself, so this only breaks on branches without an npm update, where the
failed install blocks every post-upgrade command including `make tidy`,
leaving an untidy `go.sum` behind.

Seen on https://github.com/go-gitea/gitea/pull/38914.

Verified in `ghcr.io/renovatebot/renovate:latest`:

```
install-tool pnpm 11.22.0                    exit=16
  FATAL: parent tool not installed  tool: "pnpm"  parent: "node"
install-tool node 22.18.0 && install-tool pnpm 11.22.0   exit=0
```
2026-08-17 10:51:09 -07:00
wxiaoguangandGitHub dea71bb8ba enhance: user-friendly packages setup manual (#38946)
* replace #35564
* fix #36992
2026-08-17 17:43:34 +00:00
GiteabotandGitHub 596b7f7a25 chore(deps): update dependencies (#38947) 2026-08-17 15:10:17 +02:00
1b21c8a1e6 fix(indexer): correct bleve indexer token filters (#38853)
* fix #36228
* fix #37221

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-08-17 07:59:41 +00:00
GiteaBot 3cd6672e48 [skip ci] Updated translations via Crowdin 2026-08-17 00:22:34 +00:00
wxiaoguangandGitHub 63f2918336 fix: make "login_name" field optional for API edit user (#38917) 2026-08-16 13:39:28 +00:00
wxiaoguangandGitHub 5e4d21acd5 chore: fix repo watch (#38921) 2026-08-16 03:00:59 +00:00
GiteaBot 56ad4689ad [skip ci] Updated translations via Crowdin 2026-08-16 00:24:11 +00:00
133a3b8567 fix(deps): update module golang.org/x/image to v0.45.0 [security] (#38930)
Co-authored-by: bircni <bircni@icloud.com>
2026-08-15 15:43:20 +02:00
bircniandGitHub 2b8ea5476c fix(ui): respect FEED_PAGING_NUM on the dashboard feed (#38935)
The dashboard activity feed was paginated with `[ui.user]
REPO_PAGING_NUM`
instead of `[ui] FEED_PAGING_NUM`.

The wrong setting was picked up when the page size was hoisted into a
local
variable in https://github.com/go-gitea/gitea/pull/34994, most likely
copied
from the `dashboardRepoList` block a few lines above. `REPO_PAGING_NUM`
should
only control repository lists.

Fixes https://github.com/go-gitea/gitea/issues/38925
2026-08-15 19:31:10 +08:00
GiteaBot 43ace7cc8a [skip ci] Updated translations via Crowdin 2026-08-15 00:22:56 +00:00
bircniandGitHub a96a73c364 docs: Update CHANGELOG for version 1.27.2 (#38923)
Signed-off-by: bircni <bircni@icloud.com>
2026-08-14 22:06:05 +02:00
Lunny XiaoandGitHub 07843086c2 ci: remove AWS S3 uploads from release workflows (#38928)
Release binaries and downloads have been served from Cloudflare R2 for a
while now, so the AWS S3 upload is redundant.

This removes the `configure aws` and `upload binaries to s3` steps from
the nightly, RC and version release workflows. Since
`configure-aws-credentials` no longer runs in those jobs, the
`AWS_REGION: auto` workaround in the R2 step can be dropped as well.

The `AWS_*` secrets for S3 can be removed from the repository settings
afterwards.
2026-08-14 21:50:27 +02:00
GiteabotandGitHub 5b7b00477a chore(deps): update dependency go to v1.26.6 (#38912) 2026-08-14 10:28:00 -07:00
dbe311197c enhance(admin): show impersonation banner and keep password change with the user (#38924)
Follow-up to https://github.com/go-gitea/gitea/pull/38614

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-08-14 22:23:30 +08:00
wxiaoguangandGitHub b6368965fb refactor: wiki edit form (#38918)
1. the fragile `document.querySelector('.repository.wiki.new
.ui.form')!` is broken (again), rewrite to "data-global-init"
    * regression from #37571 because a new form was added
3. use "form-fetch-action" and JSON response instead of
"RenderWithErrDeprecated"
2026-08-14 11:31:23 +02:00
MitrahsoftandGitHub befeacdf7d docs(api): document 401/403 responses for user key endpoints (#38711) 2026-08-14 02:55:44 +00:00
wxiaoguangandGitHub 72a9debaff refactor: clean up form binding & validation (#38873)
Clarify the "validation" and "error display" logic.

All the copied&pasted `Validate` functions are removed.
2026-08-14 02:15:33 +00:00
GiteaBot 8b40df255b [skip ci] Updated translations via Crowdin 2026-08-14 00:37:15 +00:00
68feaba2ed fix(migrations): use all configured GitHub tokens (#38841)
GitHub migrations accept multiple comma-separated OAuth tokens, but
clients with unknown rate data are never selected. After the first
client is used, every later token stays unknown and can never
participate in quota-aware selection.

Select each client with unknown rate data once before falling back to
the existing highest-remaining-rate choice. The regression test covers
initial probing of all clients and then selection by remaining quota.

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

Assisted-by: Codex:GPT-5

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-08-13 14:46:45 +00:00
wxiaoguangandGitHub d2be79a942 fix: update collaborator access mode (#38894)
There is already AddOrUpdateCollaborator, don't duplicate the code.
2026-08-13 09:54:33 +00:00
01e9febbea fix(actions): keep github.event.inputs as strings for workflow_dispatch (#38899)
`github.event.inputs` must mirror the raw `workflow_dispatch` payload,
where
GitHub keeps every input as a string. Only the separate `inputs` context
preserves declared types, e.g. booleans. A previous fix coerced boolean
inputs in the single map that fed both contexts, so
`github.event.inputs.someBool` became a real boolean and comparisons
like
`== 'true'` stopped matching.

`github.event.inputs` now stays string-only again. The `inputs` context
used
for server-side `if:` evaluation of needs-gated/matrix-deferred jobs
re-coerces booleans independently, from the job's own workflow
declaration,
so that path keeps working correctly.

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

---------

Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-08-13 09:36:41 +02:00
GiteaBot 5287860efb [skip ci] Updated translations via Crowdin 2026-08-13 00:36:53 +00:00
c186cc4b8d fix(actions): let a rerun of selected jobs read the previous attempt's artifacts (#38857)
Fixes #38773

## Background

Artifacts became attempt-scoped in #37119, and the runner-facing
artifact APIs filter strictly by the attempt of the running job. "Re-run
failed jobs" creates a new attempt whose passed-through jobs never
upload their artifacts again, so a re-run job that downloads one of them
fails with "artifact not found".

## Fix

The read paths (v3 and v4 list and download) now resolve artifacts
across the running job's attempt plus the attempts it inherits from, and
an inherited artifact is shadowed by a same-named one from a newer
attempt.

## Note

GitHub's documentation does not document these behaviors. The
conclusions below are based on manual testing, so consistency with
GitHub cannot be guaranteed.

- In a "partial re-run", a job can download artifacts uploaded by an
earlier attempt, every attempt keeps its own copy of a name, and a
lookup by name resolves to the newest one.
- A full "Re-run all jobs" never downloads artifacts from earlier
attempts.

---------

Co-authored-by: bircni <bircni@icloud.com>
2026-08-12 19:55:26 +00:00
wxiaoguangandGitHub 53d7d3f053 refactor: external render (#38885)
make the "command variable replacement" more accurate and
OS-independent, add a test for it.
2026-08-12 19:30:26 +00:00
8161479fde fix(actions): resolve pull_request_target reusable workflows at the base commit (#38886)
For a `pull_request_target` (PRT) run, Gitea loads the top-level
workflow from the trusted base branch, but any local reusable workflow
it calls (`uses: ./...`) was read from the PR **head** commit, which the
fork author controls.

## Fix

**Record the source commit where the content is read.**
`DetectedWorkflow` now carries a `SourceCommitSHA` filled in next to
`Content`, so the PRT detection pass at the base commit records the base
SHA automatically.

**Defense in depth.** `loadReusableWorkflowSource` pins the PR base
commit for a PRT run's local `uses: ./...` rather than trusting the
stored SHA. This also covers runs recorded before this change, whose
rows still hold the head SHA and would otherwise resolve from the fork
on rerun.

Existing run rows are not migrated.

---------

Co-authored-by: Zettat <zettat123@gmail.com>
2026-08-12 19:03:44 +02:00
2551f9949a enhance(repo): add default object format setting (#38877)
Adds `[repository] DEFAULT_OBJECT_FORMAT` to default new repositories to
`sha1` or `sha256`.

Applies the setting to repository creation defaults in the UI and API,
reducing repeated manual selection.

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

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

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-08-12 16:12:57 +00:00
Lunny XiaoandGitHub 3f833fd681 chore: Pre-register a builtin OAuth2 application for the official Gitea mobile app (#38880)
This is a prepare and required step for upcoming Gitea Official Mobile
APP which supports login with OAuth2.

The official Gitea mobile app needs the same mechanism. This adds a
builtin application for it:

| | |
|---|---|
| client ID | `b757811a-05c8-4c76-8d74-a5ee3d2073f2` |
| config name | `gitea-app` |
| display name | `Gitea App` |
| redirect URI | `com.gitea.app://oauth/callback` |

Unlike the existing entries, which are CLIs and can therefore use a
loopback `http://127.0.0.1` redirect, a mobile app authorises through a
system browser session (`ASWebAuthenticationSession` on iOS, Custom Tabs
on Android) that can only receive a custom-scheme callback, hence the
custom scheme here.
2026-08-12 14:34:22 +00:00
624 changed files with 12008 additions and 6783 deletions
+3 -1
View File
@@ -10,7 +10,9 @@ runs:
using: composite
steps:
- uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
with:
cache-image: false
- uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Build regular image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
+1 -1
View File
@@ -31,7 +31,7 @@ runs:
with:
path: ~/go/pkg/mod
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' }}
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
+17 -9
View File
@@ -1,22 +1,30 @@
name: node-setup
description: Set up pnpm and node and restore caches
description: Set up pnpm and node and restore the pnpm store cache
inputs:
cache:
description: Cache pnpm downloads
description: Restore the pnpm store cache
default: "true"
runs:
using: composite
steps:
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 26
- if: ${{ inputs.cache == 'true' }}
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
id: store
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- if: ${{ inputs.cache == 'true' && github.workflow == 'cache-seeder' }}
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
node-version: 26
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- if: ${{ inputs.cache != 'true' }}
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
path: ${{ steps.store.outputs.path }}
key: pnpm-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pnpm-lock.yaml') }}
- if: ${{ inputs.cache == 'true' && github.workflow != 'cache-seeder' }}
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
node-version: 26
path: ${{ steps.store.outputs.path }}
key: pnpm-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: pnpm-${{ runner.os }}-${{ runner.arch }}-
+10
View File
@@ -0,0 +1,10 @@
name: python-setup
description: Set up uv and python and restore the uv cache
runs:
using: composite
steps:
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
python-version: 3.14
save-cache: ${{ github.workflow == 'cache-seeder' }}
+15 -5
View File
@@ -5,7 +5,7 @@ name: cache-prune
on:
schedule:
- cron: "37 2 * * *" # every day at 02:37 UTC
- cron: "37 */6 * * *" # every six hours at :37
workflow_dispatch:
workflow_call:
@@ -24,15 +24,25 @@ jobs:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
steps:
# Deletes least recently used first, the order GitHub itself evicts in, which takes
# superseded generations first as those stop being restored once a newer one exists.
# Keep the newest generation per key, restores never reach the older ones.
- name: delete superseded caches
run: |
gh cache list --limit 1000 --json id,key,ref,createdAt |
jq -r 'group_by([.ref, (.key | sub("(-go[0-9.]+)?-[0-9a-f]{40,64}(-[0-9]+-[0-9]+)?$"; ""))])[]
| sort_by(.createdAt)[:-1][] | "\(.id) \(.key)"' |
while read -r id key; do
echo "deleting $key"
gh cache delete "$id" || true
done
# Deletes least recently used first, the order GitHub itself evicts in.
- name: delete caches over the size limit
run: |
caches=$(gh cache list --limit 1000 --sort last_accessed_at --order asc --json id,key,sizeInBytes)
size=$(jq '[.[].sizeInBytes] | add // 0' <<< "$caches")
echo "cache usage: $((size / 1000000)) MB"
while [ "$size" -gt 6500000000 ] && read -r id bytes key; do
while [ "$size" -gt 6000000000 ] && read -r id bytes key; do
echo "deleting $key"
gh cache delete "$id"
gh cache delete "$id" || true
size=$((size - bytes))
done <<< "$(jq -r '.[] | "\(.id) \(.sizeInBytes) \(.key)"' <<< "$caches")"
+25 -10
View File
@@ -1,9 +1,6 @@
# Populates main's cache scope so PR runs warm-start from it. Saves the go
# module, go build (incl. test compile), and golangci-lint caches.
#
# Caches are ref-scoped: PR runs read their own scope then fall back to the
# base branch. Per .github/actions/go-cache/action.yml, PRs are restore-only,
# so push-to-main is the only opportunity to populate the fallback scope.
# Populates main's cache scope so PR runs warm-start from it. Caches are ref-scoped:
# PR runs read their own scope then fall back to the base branch, and only this
# workflow saves, so push-to-main is the only chance to populate the fallback scope.
name: cache-seeder
@@ -16,8 +13,13 @@ on:
- "go.mod" # a toolchain bump invalidates the build caches
- "go.sum"
- ".golangci.yml"
- "pnpm-lock.yaml"
- "pyproject.toml"
- "uv.lock"
- ".github/actions/go-cache/action.yml"
- ".github/actions/go-setup/action.yml"
- ".github/actions/node-setup/action.yml"
- ".github/actions/python-setup/action.yml"
- ".github/workflows/cache-seeder.yml"
concurrency:
@@ -35,7 +37,7 @@ jobs:
- uses: ./.github/actions/go-setup
- run: make deps-backend deps-tools
- run: TAGS="bindata" make backend
- run: TAGS="bindata gogit" GOEXPERIMENT="" make backend
- run: TAGS="bindata gogit" make backend
- name: warm test compile cache (bindata)
env:
TAGS: bindata
@@ -44,13 +46,12 @@ jobs:
- name: warm test compile cache (bindata gogit)
env:
TAGS: bindata gogit
GOEXPERIMENT:
GOTEST_FLAGS: -race -list=^$$ -count=1
run: make test-backend
- name: warm integration compile cache
run: |
TAGS="bindata" make test-integration-compile
TAGS="bindata gogit" GOEXPERIMENT="" make test-integration-compile
TAGS="bindata gogit" make test-integration-compile
TAGS="bindata gogit" GOTEST_FLAGS="-race" make test-integration-compile
lint:
@@ -74,9 +75,23 @@ jobs:
TAGS: ${{ matrix.tags }}
TARGET: ${{ matrix.target }}
frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: ./.github/actions/node-setup
- run: make deps-frontend
python:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: ./.github/actions/python-setup
- run: make deps-py
# reclaims the caches this run superseded, so the next save still fits in the allowance
prune:
needs: [gobuild, lint]
needs: [gobuild, lint, frontend, python]
permissions:
actions: write
uses: ./.github/workflows/cache-prune.yml
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: renovatebot/github-action@316d7cd859606d6039a2182b7d69199e9b036835 # v46.2.1
- uses: renovatebot/github-action@e09d604f8f803bb527bd8321ed5be06c460b8682 # v46.2.2
with:
renovate-version: ${{ env.RENOVATE_VERSION }}
configurationFile: renovate.json5
+5 -3
View File
@@ -50,7 +50,7 @@ jobs:
shell: ${{ steps.changes.outputs.shell }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
id: changes
with:
filters: |
@@ -73,8 +73,9 @@ jobs:
frontend:
- "*.ts"
- "web_src/**"
- "tools/generate-svg.ts"
- "tools/generate-svg-vscode-extensions.json"
- "tools/**/*.ts"
- "tools/**/*.json"
- "tools/playwright.sh"
- "tsconfig.json"
- "assets/emoji.json"
- "package.json"
@@ -134,6 +135,7 @@ jobs:
e2e:
- "tests/e2e/**"
- "tools/test-e2e.sh"
- "tools/playwright.sh"
- "playwright.config.ts"
shell:
+3 -5
View File
@@ -24,8 +24,7 @@ jobs:
with:
lint-cache: "true"
- run: make deps-backend deps-tools
- run: TAGS="bindata" make generate-go # lint-go also lints with "bindata" tags which requires "_bindata.go"
- run: make lint-backend
- run: TAGS="bindata" make generate-go lint-backend # lint-go can lint with "bindata" tags
lint-on-demand:
needs: files-changed
@@ -42,9 +41,7 @@ jobs:
- run: make lint-spell
- if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true' || needs.files-changed.outputs.actions == 'true'
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
python-version: 3.14
uses: ./.github/actions/python-setup
- if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true'
run: make deps-py lint-templates lint-yaml
@@ -77,6 +74,7 @@ jobs:
- run: make deps-frontend
- run: make lint-frontend
- run: make checks-frontend
- run: make playwright
- run: make test-frontend
- run: make frontend
+4 -7
View File
@@ -21,7 +21,7 @@ jobs:
timeout-minutes: 50
services:
pgsql:
image: postgres:14@sha256:2f439458ab6a57a925825ae14f9d06910e4fe4a41c8d4a0ae06397e65b707e1b
image: postgres:14@sha256:2fdfb9b432d4a73bd3eea3d989752c1e669b68d502347e0bfd2cc6d709f3d6b4
env:
POSTGRES_DB: test
POSTGRES_PASSWORD: postgres
@@ -57,7 +57,7 @@ jobs:
timeout-minutes: 50
services:
pgsql:
image: postgres:14@sha256:2f439458ab6a57a925825ae14f9d06910e4fe4a41c8d4a0ae06397e65b707e1b
image: postgres:14@sha256:2fdfb9b432d4a73bd3eea3d989752c1e669b68d502347e0bfd2cc6d709f3d6b4
env:
POSTGRES_DB: test
POSTGRES_PASSWORD: postgres
@@ -96,7 +96,6 @@ jobs:
- run: make backend
env:
TAGS: bindata gogit
GOEXPERIMENT:
- run: GITEA_TEST_DATABASE=sqlite make test-migration
env:
TAGS: bindata gogit
@@ -107,7 +106,6 @@ jobs:
# sqlite driver can contain large amount of Golang code, so don't use race detector for it, otherwise, extremely slow
GOTEST_FLAGS: -timeout=40m
TAGS: bindata gogit
GOEXPERIMENT:
test-unit:
if: needs.files-changed.outputs.backend == 'true'
@@ -125,13 +123,13 @@ jobs:
ports:
- "9200:9200"
meilisearch:
image: getmeili/meilisearch:v1@sha256:d36e713e8f89483af1ab0d72011bbd503f5ab100b68ccbfad51c39e3f0a0567d
image: getmeili/meilisearch:v1@sha256:8d6643d86d71fad6ad3cba92cde7ccfce9e4d6c384bda67598eb553571c32431
env:
MEILI_ENV: development # disable auth
ports:
- "7700:7700"
redis:
image: redis:latest@sha256:52334768d4a6594d8969f51a1a6fee3ffa7545f6359a4877229cdc754d2def82
image: redis:latest@sha256:1c4405ec7fb6ed58b6b83d26c7e3fc80625d2aca6cfa517ae03b6e963510f7ca
options: >- # wait until redis has started
--health-cmd "redis-cli ping"
--health-interval 5s
@@ -170,7 +168,6 @@ jobs:
env:
GOTEST_FLAGS: -race -timeout=20m
TAGS: bindata gogit
GOEXPERIMENT:
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
GITEA_TEST_CI_SKIP_EXTERNAL: true
- run: make test-check
+4 -19
View File
@@ -28,10 +28,7 @@ jobs:
cache: false
- uses: ./.github/actions/node-setup
- run: make deps-frontend deps-backend
# xgo build
- run: make release
env:
TAGS: bindata
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: import gpg key
@@ -49,7 +46,7 @@ jobs:
cosign sign-blob "$f" --bundle "$f.sigstore.json" --yes
echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f"
done
# clean branch name to get the folder name in S3
# clean branch name to get the folder name in the object storage
- name: Get cleaned branch name
id: clean_name
env:
@@ -58,25 +55,11 @@ jobs:
REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
- name: configure aws
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
with:
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: upload binaries to s3
env:
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
BRANCH: ${{ steps.clean_name.outputs.branch }}
run: |
aws s3 sync dist/release "s3://$AWS_S3_BUCKET/gitea/$BRANCH" --no-progress
# configure-aws-credentials exports AWS_REGION job-wide and it wins over AWS_DEFAULT_REGION, so pin it here
- name: upload binaries to cloudflare r2
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
AWS_REGION: auto
CLOUDFLARE_R2_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_R2_ACCOUNT_ID }}
CLOUDFLARE_R2_BUCKET: ${{ secrets.CLOUDFLARE_R2_BUCKET }}
BRANCH: ${{ steps.clean_name.outputs.branch }}
@@ -94,7 +77,9 @@ jobs:
# 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
- uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
with:
cache-image: false
- uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Get cleaned branch name
id: clean_name
env:
+5 -20
View File
@@ -29,10 +29,7 @@ jobs:
cache: false
- uses: ./.github/actions/node-setup
- run: make deps-frontend deps-backend
# xgo build
- run: make release
env:
TAGS: bindata
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: import gpg key
@@ -50,7 +47,7 @@ jobs:
cosign sign-blob "$f" --bundle "$f.sigstore.json" --yes
echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f"
done
# clean branch name to get the folder name in S3
# clean branch name to get the folder name in the object storage
- name: Get cleaned branch name
id: clean_name
env:
@@ -59,32 +56,18 @@ jobs:
REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//')
echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
- name: configure aws
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
with:
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: upload binaries to s3
env:
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
BRANCH: ${{ steps.clean_name.outputs.branch }}
run: |
aws s3 sync dist/release "s3://$AWS_S3_BUCKET/gitea/$BRANCH" --no-progress
# configure-aws-credentials exports AWS_REGION job-wide and it wins over AWS_DEFAULT_REGION, so pin it here
- name: upload binaries to cloudflare r2
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
AWS_REGION: auto
CLOUDFLARE_R2_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_R2_ACCOUNT_ID }}
CLOUDFLARE_R2_BUCKET: ${{ secrets.CLOUDFLARE_R2_BUCKET }}
BRANCH: ${{ steps.clean_name.outputs.branch }}
run: |
aws s3 sync dist/release "s3://$CLOUDFLARE_R2_BUCKET/gitea/$BRANCH" --endpoint-url "https://$CLOUDFLARE_R2_ACCOUNT_ID.r2.cloudflarestorage.com" --no-progress
- name: Install GH CLI
uses: dev-hanz-ops/install-gh-cli-action@af38ce09b1ec248aeb08eea2b16bbecea9e059f8 # v0.2.1
uses: dev-hanz-ops/install-gh-cli-action@6089bdde54118ad7ca3d22053eb2d69387fd2779 # v0.3.0
with:
gh-cli-version: 2.39.1
- name: create github release
@@ -105,7 +88,9 @@ jobs:
# 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
- uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
with:
cache-image: false
- uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
id: meta
with:
+5 -20
View File
@@ -32,10 +32,7 @@ jobs:
cache: false
- uses: ./.github/actions/node-setup
- run: make deps-frontend deps-backend
# xgo build
- run: make release
env:
TAGS: bindata
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: import gpg key
@@ -53,7 +50,7 @@ jobs:
cosign sign-blob "$f" --bundle "$f.sigstore.json" --yes
echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f"
done
# clean branch name to get the folder name in S3
# clean branch name to get the folder name in the object storage
- name: Get cleaned branch name
id: clean_name
env:
@@ -62,32 +59,18 @@ jobs:
REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//')
echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
- name: configure aws
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
with:
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: upload binaries to s3
env:
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
BRANCH: ${{ steps.clean_name.outputs.branch }}
run: |
aws s3 sync dist/release "s3://$AWS_S3_BUCKET/gitea/$BRANCH" --no-progress
# configure-aws-credentials exports AWS_REGION job-wide and it wins over AWS_DEFAULT_REGION, so pin it here
- name: upload binaries to cloudflare r2
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
AWS_REGION: auto
CLOUDFLARE_R2_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_R2_ACCOUNT_ID }}
CLOUDFLARE_R2_BUCKET: ${{ secrets.CLOUDFLARE_R2_BUCKET }}
BRANCH: ${{ steps.clean_name.outputs.branch }}
run: |
aws s3 sync dist/release "s3://$CLOUDFLARE_R2_BUCKET/gitea/$BRANCH" --endpoint-url "https://$CLOUDFLARE_R2_ACCOUNT_ID.r2.cloudflarestorage.com" --no-progress
- name: Install GH CLI
uses: dev-hanz-ops/install-gh-cli-action@af38ce09b1ec248aeb08eea2b16bbecea9e059f8 # v0.2.1
uses: dev-hanz-ops/install-gh-cli-action@6089bdde54118ad7ca3d22053eb2d69387fd2779 # v0.3.0
with:
gh-cli-version: 2.39.1
- name: create github release
@@ -108,7 +91,9 @@ jobs:
# 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
- uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
with:
cache-image: false
- uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
id: meta
with:
+15 -9
View File
@@ -52,6 +52,8 @@ linters:
desc: do not use the go-chi cache package, use gitea's cache system
- pkg: github.com/pkg/errors
desc: use builtin errors package instead
- pkg: gitea.com/go-chi/binding
desc: use our wrapper
migrations:
files:
- '**/modelmigration/**/*.go'
@@ -60,6 +62,9 @@ linters:
desc: "migrations must not depend on the models package. HINT: MIGRATION-STRUCT-FROZEN"
- pkg: gitea.dev/modules/structs
desc: "migrations must not depend on modules/structs. HINT: MIGRATION-STRUCT-FROZEN"
modernize:
disable:
- embedlit
nolintlint:
allow-unused: false
require-explanation: true
@@ -160,10 +165,10 @@ linters:
- gocritic
text: '(?i)exitAfterDefer:'
paths:
- node_modules
- .venv
- public
- web_src
- ^node_modules/
- ^\.venv/
- ^public/
- ^web_src/
issues:
max-issues-per-linter: 0
max-same-issues: 0
@@ -180,14 +185,15 @@ formatters:
- blank
- default
gofumpt:
extra-rules: true
extra:
group-params: true
exclusions:
generated: lax
paths:
- node_modules
- .venv
- public
- web_src
- ^node_modules/
- ^\.venv/
- ^public/
- ^web_src/
run:
timeout: 10m
+2 -1
View File
@@ -1 +1,2 @@
disable=SC1091,SC2001,SC2002,SC2016,SC2028,SC2046,SC2124,SC2128,SC2129,SC2154,SC2155,SC2164,SC2181,SC2207
# SC2153: false-alert "Possible misspelling: TAGS may not be assigned. Did you mean tags?". We already use strict mode.
disable=SC1091,SC2001,SC2002,SC2016,SC2028,SC2046,SC2124,SC2128,SC2129,SC2153,SC2154,SC2155,SC2164,SC2181,SC2207
+38
View File
@@ -4,6 +4,44 @@ This changelog goes through the changes that have been made in each release
without substantial changes to our git log; to see the highlights of what has
been added to each release, please refer to the [blog](https://blog.gitea.com).
## [1.27.2](https://github.com/go-gitea/gitea/releases/tag/v1.27.2) - 2026-08-14
* SECURITY
* Fix: update collaborator access mode and httpsign (#38894, #38862) (#38895)
* Refactor: external render (#38885) (#38898)
* Fix(actions): resolve pull_request_target reusable workflows at the base commit (#38886) (#38897)
* Refactor: markup render (#38864) (#38869)
* Fix(deps): update dependency mermaid to v11.16.1 (#38816)
* Fix(auth): set WebAuthn user verification per request (#38805) (#38810)
* Fix: render highlight language (#38793) (#38795)
* ENHANCEMENTS
* enhance: add missing npm package metadata properties (#38826) (#38831)
* BUGFIXES
* fix(actions): keep github.event.inputs as strings for workflow_dispatch (#38899) (#38908)
* fix(actions): let a rerun of selected jobs read the previous attempt's artifacts (#38857) (#38901)
* fix(lfs): accept successful transfer responses (#38866) (#38875)
* fix(packages): ignore nested Package.swift (#38788) (#38836)
* fix: drop newline-bearing member names in arch ParsePackage (#38102) (#38830)
* fix(storage): fix Azure Blob dump failing with file does not exist (#38814) (#38828)
* fix(migration): migration deletion returned json redirection (#38796) (#38825)
* fix(ui): change underlines to default browser style (#38819) (#38823)
* fix(actions): allow cancelling runs without running jobs (#35842) (#38812)
* fix(actions): evaluate each `${{ }}` part on its own (#38754) (#38797)
* fix(actions): write an action task report in one transaction (#38792) (#38794)
* fix: markup link (#38764) (#38765)
* fix: set a minio part size when the content size is unknown (#38753) (#38755)
* fix: bad path escape in subpath archive download (#38749) (#38750)
* fix: remove the pull merge box from UI when the refreshed page doesn't contain it (#38742) (#38744)
* fix(markdown): fix double strikethough on code (#38707) (#38729)
* fix(lfs): failed upload deletes a concurrent upload's meta object (#38693) (#38722)
* fix: correct full url when using sub-path (#38712) (#38716)
* fix: avoid markup render panic (#38698) (#38703)
* fix(ui): too many participants shown in commit avatar stacks (#38689) (#38700)
* fix: support HEAD requests on Alpine registry APKINDEX.tar.gz (#38686) (#38688)
* fix(migrations): use all configured GitHub tokens (#38841) (#38846)
## [1.27.1](https://github.com/go-gitea/gitea/releases/tag/v1.27.1) - 2026-07-27
* SECURITY
+2 -2
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
# Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.24 AS frontend-build
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.27-alpine3.24 AS frontend-build
RUN apk --no-cache add build-base git nodejs pnpm
WORKDIR /src
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
@@ -9,7 +9,7 @@ COPY --exclude=.git/ . .
RUN make frontend
# Build backend for each target platform
FROM docker.io/library/golang:1.26-alpine3.24 AS build-env
FROM docker.io/library/golang:1.27-alpine3.24 AS build-env
ARG GITEA_VERSION
ARG TAGS=""
+2 -2
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
# Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.24 AS frontend-build
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.27-alpine3.24 AS frontend-build
RUN apk --no-cache add build-base git nodejs pnpm
WORKDIR /src
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
@@ -9,7 +9,7 @@ COPY --exclude=.git/ . .
RUN make frontend
# Build backend for each target platform
FROM docker.io/library/golang:1.26-alpine3.24 AS build-env
FROM docker.io/library/golang:1.27-alpine3.24 AS build-env
ARG GITEA_VERSION
ARG TAGS=""
+1
View File
@@ -64,3 +64,4 @@ Christopher Homberger <christopher.homberger@web.de> (@ChristopherHX)
Tobias Balle-Petersen <tobiasbp@gmail.com> (@tobiasbp)
TheFox <thefox0x7@gmail.com> (@TheFox0x7)
Nicolas <bircni@icloud.com> (@bircni)
Minjie Fang <wingsallen@gmail.com> (@wingsallen)
+46 -51
View File
@@ -1,29 +1,24 @@
DIST := dist
DIST_DIRS := $(DIST)/binaries $(DIST)/release
# By default use go's 1.25 experimental json v2 library when building
# TODO: remove when no longer experimental
export GOEXPERIMENT ?= jsonv2
GO ?= go
SHASUM ?= shasum -a 256
COMMA := ,
XGO_VERSION := go-1.26.x
AIR_PACKAGE ?= github.com/air-verse/air@v1.67.4 # renovate: datasource=go
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.9.0 # renovate: datasource=go
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 # renovate: datasource=go
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.11.1 # renovate: datasource=go
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.1 # renovate: datasource=go
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.16 # 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.36.1 # renovate: datasource=go
XGO_PACKAGE ?= src.techknowlogick.com/xgo@v1.9.0 # renovate: datasource=go
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.6.0 # renovate: datasource=go
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.36.4 # renovate: datasource=go
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.7.0 # renovate: datasource=go
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 # renovate: datasource=go
SHELLCHECK_IMAGE ?= docker.io/koalaman/shellcheck:v0.11.0@sha256:61862eba1fcf09a484ebcc6feea46f1782532571a34ed51fedf90dd25f925a8d # renovate: datasource=docker
CONTAINER_RUNTIME ?= $(shell hash docker >/dev/null 2>&1 && echo docker || echo podman)
PLAYWRIGHT_BROWSERS ?= chromium firefox
PLAYWRIGHT_FLAGS ?=
HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes)
ifeq ($(HAS_GO), yes)
CGO_EXTRA_CFLAGS := -DSQLITE_MAX_VARIABLE_NUMBER=32766
@@ -42,18 +37,13 @@ endif
TAGS ?=
TAGS_EVIDENCE := $(MAKE_EVIDENCE_DIR)/tags
CGO_TAGS := sqlite_mattn pam
CGO_ENABLED ?= 0
ifneq (,$(findstring sqlite_mattn,$(TAGS))$(findstring pam,$(TAGS)))
ifneq ($(strip $(filter $(CGO_TAGS),$(TAGS))),)
CGO_ENABLED = 1
endif
STATIC ?=
EXTLDFLAGS ?=
ifneq ($(STATIC),)
EXTLDFLAGS = -extldflags "-static"
endif
ifeq ($(GOOS),windows)
IS_WINDOWS := yes
else ifeq ($(patsubst Windows%,Windows,$(OS)),Windows)
@@ -62,14 +52,13 @@ else ifeq ($(patsubst Windows%,Windows,$(OS)),Windows)
endif
endif
# GOFLAGS and EXTRA_GOFLAGS are for the 'go build' command only
ifeq ($(IS_WINDOWS),yes)
GOFLAGS := -v -buildmode=exe
EXECUTABLE ?= gitea.exe
else
GOFLAGS := -v
EXECUTABLE ?= gitea
endif
# EXTRA_GOFLAGS is for the 'go build' command only
EXTRA_GOFLAGS ?=
ifeq ($(shell sed --version 2>/dev/null | grep -q GNU && echo gnu),gnu)
@@ -86,14 +75,20 @@ STORED_VERSION_FILE := VERSION
GITHUB_REF_TYPE ?= branch
GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
ifneq ($(GITHUB_REF_TYPE),branch)
# VERSION: the branch name for the build and filenames, e.g.: "feature/foo-bar", "main"
# branch name "release/v1.27.2" is stripped to "1.27.2".
# GITEA_VERSION: the Gitea's internal version for display, e.g. "1.28.0+dev-356-ge47d0b66ea"
ifeq ($(GITHUB_REF_TYPE),tag)
# convert tag "v1.2.3" to "1.2.3"
VERSION ?= $(subst v,,$(GITHUB_REF_NAME))
GITEA_VERSION ?= $(VERSION)
else
else ifeq ($(GITHUB_REF_TYPE),branch)
ifneq ($(GITHUB_REF_NAME),)
# convert branch "release/v1.2" to "1.2-nightly"
VERSION ?= $(subst release/v,,$(GITHUB_REF_NAME))-nightly
else
VERSION ?= main
# no branch name info, use git ref name "HEAD" instead
VERSION ?= HEAD
endif
STORED_VERSION=$(shell cat $(STORED_VERSION_FILE) 2>/dev/null)
@@ -102,16 +97,17 @@ else
else
GITEA_VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
endif
else
$(error unsupported ref type $(GITHUB_REF_TYPE))
endif
# if version = "main" then update version to "nightly"
# if version == "main" then add "-nightly" to the version for nightly builds: "main-nightly"
ifeq ($(VERSION),main)
VERSION := main-nightly
endif
LDFLAGS := $(LDFLAGS) -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)"
LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/riscv64
RELEASE_ENV = GO="$(GO)" TAGS="$(TAGS)" LDFLAGS="$(LDFLAGS)" DIST="$(DIST)" VERSION="$(VERSION)"
GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list gitea.dev/modelmigration/...) gitea.dev/tests/integration/migration-test gitea.dev/tests gitea.dev/tests/integration,$(shell $(GO) list ./... | grep -v /vendor/))
MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list gitea.dev/modelmigration/...)
@@ -135,7 +131,7 @@ GO_LICENSE_FILE := assets/go-licenses.json
TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR)
GO_DIRS := build cmd models modules routers services tests tools
GO_DIRS := build cmd modelmigration models modules routers services tests tools
WEB_DIRS := web_src/js web_src/css
ESLINT_FILES := web_src/js tools *.ts tests/e2e
@@ -389,7 +385,7 @@ test-backend: ## test backend files
@$(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' $(GO_TEST_PACKAGES)
.PHONY: test-frontend
test-frontend: node_modules ## test frontend files
test-frontend: playwright ## test frontend files
pnpm exec vitest
.PHONY: test-check
@@ -451,7 +447,7 @@ $(GO_LICENSE_FILE): go.mod go.sum
GO=$(GO) $(GO) run build/generate-go-licenses.go $(GO_LICENSE_FILE)
.PHONY: test-integration
test-integration:
test-integration: $(EXECUTABLE)
@# Use a compiled binary: testlogger forwards gitea logs to t.Log, so `go test -v`
@# would flood output per passing test. testcache can't help these tests anyway —
@# they mutate the work directory, so cache inputs change between runs.
@@ -463,7 +459,7 @@ test-integration-compile:
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -c -o /dev/null gitea.dev/tests/integration
.PHONY: test-integration\#%
test-integration\#%:
test-integration\#%: $(EXECUTABLE)
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -run $(subst .,/,$*) gitea.dev/tests/integration
.PHONY: test-migration
@@ -484,11 +480,11 @@ migrations.individual.test\#%:
.PHONY: playwright
playwright: deps-frontend
@CONTAINER_RUNTIME=$(CONTAINER_RUNTIME) ./tools/test-e2e.sh install
@./tools/playwright.sh $(PLAYWRIGHT_FLAGS) $(PLAYWRIGHT_BROWSERS)
.PHONY: test-e2e
test-e2e: playwright frontend backend
@CONTAINER_RUNTIME=$(CONTAINER_RUNTIME) EXECUTABLE=$(EXECUTABLE) ./tools/test-e2e.sh run $(GITEA_TEST_E2E_FLAGS)
@CONTAINER_RUNTIME=$(CONTAINER_RUNTIME) EXECUTABLE=$(EXECUTABLE) ./tools/test-e2e.sh $(GITEA_TEST_E2E_FLAGS)
.PHONY: build
build: frontend backend ## build everything
@@ -513,38 +509,38 @@ generate-go: $(TAGS_PREREQ)
.PHONY: security-check
security-check:
GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./... || true
go run $(GOVULNCHECK_PACKAGE) -show color ./... || true
$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),)
$(error pam support set via TAGS does not support static builds)
endif
CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@
.PHONY: release
release: frontend generate release-windows release-linux release-darwin release-freebsd release-copy release-compress vendor release-sources release-check
CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build -v $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@
$(DIST_DIRS):
mkdir -p $(DIST_DIRS)
# Release builds always use Go's native cross compilation. To cross-compile with CGO,
# use "build" target with proper TAGS/LDFLAGS/CGO_CFLAGS to make "$(EXECUTABLE)" target run the "go build" command.
.PHONY: release
release: frontend release-binaries release-copy release-compress vendor release-sources release-check
.PHONY: release-binaries
release-binaries: | $(DIST_DIRS)
@$(RELEASE_ENV) ./tools/build-release.sh
.PHONY: release-windows
release-windows: | $(DIST_DIRS)
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
ifeq (,$(findstring gogit,$(TAGS)))
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit .
endif
@$(RELEASE_ENV) ./tools/build-release.sh windows
.PHONY: release-linux
release-linux: | $(DIST_DIRS)
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out gitea-$(VERSION) .
@$(RELEASE_ENV) ./tools/build-release.sh linux
.PHONY: release-darwin
release-darwin: | $(DIST_DIRS)
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets 'darwin-10.12/amd64,darwin-10.12/arm64' -out gitea-$(VERSION) .
@$(RELEASE_ENV) ./tools/build-release.sh darwin
.PHONY: release-freebsd
release-freebsd: | $(DIST_DIRS)
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) .
@$(RELEASE_ENV) ./tools/build-release.sh freebsd
.PHONY: release-copy
release-copy: | $(DIST_DIRS)
@@ -564,7 +560,7 @@ release-sources: | $(DIST_DIRS)
# bsdtar needs a ^ to prevent matching subdirectories
$(eval EXCL := --exclude=$(shell tar --help | grep -q bsdtar && echo "^")./)
# use transform to a add a release-folder prefix; in bsdtar the transform parameter equivalent is -s
$(eval TRANSFORM := $(shell tar --help | grep -q bsdtar && echo "-s '/^./gitea-src-$(VERSION)/'" || echo "--transform 's|^./|gitea-src-$(VERSION)/|'"))
$(eval TRANSFORM := $(shell tar --help | grep -q bsdtar && echo "-s '|^./|gitea-src-$(VERSION)/|'" || echo "--transform 's|^./|gitea-src-$(VERSION)/|'"))
tar $(addprefix $(EXCL),$(TAR_EXCLUDES)) $(TRANSFORM) -czf $(DIST)/release/gitea-src-$(VERSION).tar.gz .
rm -f $(STORED_VERSION_FILE)
@@ -589,7 +585,6 @@ deps-tools: ## install tool dependencies
$(GO) install $(GXZ_PACKAGE) & \
$(GO) install $(MISSPELL_PACKAGE) & \
$(GO) install $(SWAGGER_PACKAGE) & \
$(GO) install $(XGO_PACKAGE) & \
$(GO) install $(GOVULNCHECK_PACKAGE) & \
$(GO) install $(ACTIONLINT_PACKAGE) & \
wait
+15 -10
View File
@@ -184,6 +184,11 @@
"path": "github.com/aws/smithy-go/internal/sync/singleflight/LICENSE",
"licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n"
},
{
"name": "github.com/aws/smithy-go/transport/http/protocol/internal/json/internal/stdlib",
"path": "github.com/aws/smithy-go/transport/http/protocol/internal/json/internal/stdlib/LICENSE",
"licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
},
{
"name": "github.com/aymerick/douceur",
"path": "github.com/aymerick/douceur/LICENSE",
@@ -944,11 +949,6 @@
"path": "github.com/pkg/errors/LICENSE",
"licenseText": "Copyright (c) 2015, Dave Cheney \u003cdave@cheney.net\u003e\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
},
{
"name": "github.com/pmezard/go-difflib",
"path": "github.com/pmezard/go-difflib/LICENSE",
"licenseText": "Copyright (c) 2013, Patrick Mezard\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n Redistributions in binary form must reproduce the above copyright\nnotice, this list of conditions and the following disclaimer in the\ndocumentation and/or other materials provided with the distribution.\n The names of its contributors may not be used to endorse or promote\nproducts derived from this software without specific prior written\npermission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS\nIS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED\nTO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A\nPARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nHOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED\nTO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
},
{
"name": "github.com/pquerna/otp",
"path": "github.com/pquerna/otp/LICENSE",
@@ -1059,6 +1059,16 @@
"path": "github.com/stretchr/testify/LICENSE",
"licenseText": "MIT License\n\nCopyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
},
{
"name": "github.com/stretchr/testify/internal/difflib",
"path": "github.com/stretchr/testify/internal/difflib/LICENSE",
"licenseText": "Copyright (c) 2013, Patrick Mezard\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n Redistributions in binary form must reproduce the above copyright\nnotice, this list of conditions and the following disclaimer in the\ndocumentation and/or other materials provided with the distribution.\n The names of its contributors may not be used to endorse or promote\nproducts derived from this software without specific prior written\npermission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS\nIS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED\nTO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A\nPARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nHOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED\nTO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
},
{
"name": "github.com/stretchr/testify/internal/spew",
"path": "github.com/stretchr/testify/internal/spew/LICENSE",
"licenseText": "ISC License\n\nCopyright (c) 2012-2016 Dave Collins \u003cdave@davec.name\u003e\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted, provided that the above\ncopyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\nWITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\nANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\nWHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\nACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\nOR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n"
},
{
"name": "github.com/syndtr/goleveldb",
"path": "github.com/syndtr/goleveldb/LICENSE",
@@ -1254,11 +1264,6 @@
"path": "gopkg.in/warnings.v0/LICENSE",
"licenseText": "Copyright (c) 2016 Péter Surányi.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
},
{
"name": "gopkg.in/yaml.v3",
"path": "gopkg.in/yaml.v3/LICENSE",
"licenseText": "\nThis project is covered by two different licenses: MIT and Apache.\n\n#### MIT License ####\n\nThe following files were ported to Go from C files of libyaml, and thus\nare still covered by their original MIT license, with the additional\ncopyright staring in 2011 when the project was ported over:\n\n apic.go emitterc.go parserc.go readerc.go scannerc.go\n writerc.go yamlh.go yamlprivateh.go\n\nCopyright (c) 2006-2010 Kirill Simonov\nCopyright (c) 2006-2011 Kirill Simonov\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n### Apache License ###\n\nAll the remaining project files are covered by the Apache license:\n\nCopyright (c) 2011-2019 Canonical Ltd\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
},
{
"name": "modernc.org/libc",
"path": "modernc.org/libc/LICENSE",
+1 -1
View File
@@ -752,7 +752,7 @@ func writeFlushPktLine(ctx context.Context, out io.Writer) error {
func writeDataPktLine(ctx context.Context, out io.Writer, data []byte) error {
hexchar := []byte("0123456789abcdef")
hex := func(n uint64) byte {
return hexchar[(n)&15]
return hexchar[n&15]
}
length := uint64(len(data) + 4)
-233
View File
@@ -5,60 +5,14 @@ package cmd
import (
"context"
"errors"
"fmt"
"os"
"gitea.dev/modules/log"
"gitea.dev/modules/private"
"github.com/urfave/cli/v3"
)
func defaultLoggingFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "logger",
Usage: `Logger name - will default to "default"`,
},
&cli.StringFlag{
Name: "writer",
Usage: "Name of the log writer - will default to mode",
},
&cli.StringFlag{
Name: "level",
Usage: "Logging level for the new logger",
},
&cli.StringFlag{
Name: "stacktrace-level",
Aliases: []string{"L"},
Usage: "Stacktrace logging level",
},
&cli.StringFlag{
Name: "flags",
Aliases: []string{"F"},
Usage: "Flags for the logger",
},
&cli.StringFlag{
Name: "expression",
Aliases: []string{"e"},
Usage: "Matching expression for the logger",
},
&cli.StringFlag{
Name: "prefix",
Aliases: []string{"p"},
Usage: "Prefix for the logger",
},
&cli.BoolFlag{
Name: "color",
Usage: "Use color in the logs",
},
&cli.BoolFlag{
Name: "debug",
},
}
}
func newLoggingCommand() *cli.Command {
return &cli.Command{
Name: "logging",
@@ -91,92 +45,6 @@ func newLoggingCommand() *cli.Command {
},
},
Action: runReleaseReopenLogging,
}, {
Name: "remove",
Usage: "Remove a logger",
ArgsUsage: "[name] Name of logger to remove",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "debug",
}, &cli.StringFlag{
Name: "logger",
Usage: `Logger name - will default to "default"`,
},
},
Action: runRemoveLogger,
}, {
Name: "add",
Usage: "Add a logger",
Commands: []*cli.Command{
{
Name: "file",
Usage: "Add a file logger",
Flags: append(defaultLoggingFlags(), []cli.Flag{
&cli.StringFlag{
Name: "filename",
Aliases: []string{"f"},
Usage: "Filename for the logger - this must be set.",
},
&cli.BoolFlag{
Name: "rotate",
Aliases: []string{"r"},
Usage: "Rotate logs",
},
&cli.Int64Flag{
Name: "max-size",
Aliases: []string{"s"},
Usage: "Maximum size in bytes before rotation",
},
&cli.BoolFlag{
Name: "daily",
Aliases: []string{"d"},
Usage: "Rotate logs daily",
},
&cli.IntFlag{
Name: "max-days",
Aliases: []string{"D"},
Usage: "Maximum number of daily logs to keep",
},
&cli.BoolFlag{
Name: "compress",
Aliases: []string{"z"},
Usage: "Compress rotated logs",
},
&cli.IntFlag{
Name: "compression-level",
Aliases: []string{"Z"},
Usage: "Compression level to use",
},
}...),
Action: runAddFileLogger,
}, {
Name: "conn",
Usage: "Add a net conn logger",
Flags: append(defaultLoggingFlags(), []cli.Flag{
&cli.BoolFlag{
Name: "reconnect-on-message",
Aliases: []string{"R"},
Usage: "Reconnect to host for every message",
},
&cli.BoolFlag{
Name: "reconnect",
Aliases: []string{"r"},
Usage: "Reconnect to host when connection is dropped",
},
&cli.StringFlag{
Name: "protocol",
Aliases: []string{"P"},
Usage: "Set protocol to use: tcp, unix, or udp (defaults to tcp)",
},
&cli.StringFlag{
Name: "address",
Aliases: []string{"a"},
Usage: "Host address and port to connect to (defaults to :7020)",
},
}...),
Action: runAddConnLogger,
},
},
}, {
Name: "log-sql",
Usage: "Set LogSQL",
@@ -195,107 +63,6 @@ func newLoggingCommand() *cli.Command {
}
}
func runRemoveLogger(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
logger := c.String("logger")
if len(logger) == 0 {
logger = log.DEFAULT
}
writer := c.Args().First()
extra := private.RemoveLogger(ctx, logger, writer)
return handleCliResponseExtra(extra)
}
func runAddConnLogger(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
vals := map[string]any{}
mode := "conn"
vals["net"] = "tcp"
if c.IsSet("protocol") {
switch c.String("protocol") {
case "udp":
vals["net"] = "udp"
case "unix":
vals["net"] = "unix"
}
}
if c.IsSet("address") {
vals["address"] = c.String("address")
} else {
vals["address"] = ":7020"
}
if c.IsSet("reconnect") {
vals["reconnect"] = c.Bool("reconnect")
}
if c.IsSet("reconnect-on-message") {
vals["reconnectOnMsg"] = c.Bool("reconnect-on-message")
}
return commonAddLogger(ctx, c, mode, vals)
}
func runAddFileLogger(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
vals := map[string]any{}
mode := "file"
if c.IsSet("filename") {
vals["filename"] = c.String("filename")
} else {
return errors.New("filename must be set when creating a file logger")
}
if c.IsSet("rotate") {
vals["rotate"] = c.Bool("rotate")
}
if c.IsSet("max-size") {
vals["maxsize"] = c.Int64("max-size")
}
if c.IsSet("daily") {
vals["daily"] = c.Bool("daily")
}
if c.IsSet("max-days") {
vals["maxdays"] = c.Int("max-days")
}
if c.IsSet("compress") {
vals["compress"] = c.Bool("compress")
}
if c.IsSet("compression-level") {
vals["compressionLevel"] = c.Int("compression-level")
}
return commonAddLogger(ctx, c, mode, vals)
}
func commonAddLogger(ctx context.Context, c *cli.Command, mode string, vals map[string]any) error {
if len(c.String("level")) > 0 {
vals["level"] = log.LevelFromString(c.String("level")).String()
}
if len(c.String("stacktrace-level")) > 0 {
vals["stacktraceLevel"] = log.LevelFromString(c.String("stacktrace-level")).String()
}
if len(c.String("expression")) > 0 {
vals["expression"] = c.String("expression")
}
if len(c.String("prefix")) > 0 {
vals["prefix"] = c.String("prefix")
}
if len(c.String("flags")) > 0 {
vals["flags"] = log.FlagsFromString(c.String("flags"))
}
if c.IsSet("color") {
vals["colorize"] = c.Bool("color")
}
logger := log.DEFAULT
if c.IsSet("logger") {
logger = c.String("logger")
}
writer := mode
if c.IsSet("writer") {
writer = c.String("writer")
}
extra := private.AddLogger(ctx, logger, writer, mode, vals)
return handleCliResponseExtra(extra)
}
func runPauseLogging(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
userMsg := private.PauseLogging(ctx)
+1
View File
@@ -36,6 +36,7 @@ var curveStringMap = map[string]tls.CurveID{
"p256": tls.CurveP256,
"p384": tls.CurveP384,
"p521": tls.CurveP521,
"mlkem1024": tls.MLKEM1024,
"x25519mlkem768": tls.X25519MLKEM768,
"secp256r1mlkem768": tls.SecP256r1MLKEM768,
"secp384r1mlkem1024": tls.SecP384r1MLKEM1024,
+24 -5
View File
@@ -608,7 +608,8 @@ ENABLED = true
;; * https://github.com/hickford/git-credential-oauth
;; * https://github.com/git-ecosystem/git-credential-manager
;; * https://gitea.com/gitea/tea
;DEFAULT_APPLICATIONS = git-credential-oauth, git-credential-manager, tea
;; * Gitea App (the official Gitea mobile app)
;DEFAULT_APPLICATIONS = git-credential-oauth, git-credential-manager, tea, gitea-app
;;
;; By default, OAuth2 applications can only use "http" and "https" as their redirect URI schemes.
;; If you need to use other schemes (e.g. for desktop applications), you can specify them here as a comma-separated list.
@@ -1111,6 +1112,9 @@ LEVEL = Info
;; The default branch name of new repositories
;DEFAULT_BRANCH = main
;;
;; The default Git object format of new repositories. Available values: sha1, sha256.
;DEFAULT_OBJECT_FORMAT = sha1
;;
;; Allow adoption of unadopted repositories
;ALLOW_ADOPTION_OF_UNADOPTED_REPOSITORIES = false
;;
@@ -1124,7 +1128,6 @@ LEVEL = Info
;ALLOW_FORK_WITHOUT_MAXIMUM_LIMIT = true
;; Allow to fork repositories into the same owner (user or organization)
;; This feature is experimental, not fully tested, and may be changed in the future
;ALLOW_FORK_INTO_SAME_OWNER = false
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -2284,6 +2287,18 @@ LEVEL = Info
;RUN_AT_START = true
;SCHEDULE = @midnight
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Delete action runs older than RUN_RETENTION_DAYS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;[cron.cleanup_action_runs]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Deletes nothing while RUN_RETENTION_DAYS is 0
;ENABLED = true
;RUN_AT_START = false
;SCHEDULE = @midnight
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Clean-up deleted branches
@@ -3004,16 +3019,20 @@ LEVEL = Info
;;
;; Default platform to get action plugins, `github` for `https://github.com`, `self` for the current Gitea instance.
;DEFAULT_ACTIONS_URL = github
;; Logs retention time in days. Old logs will be deleted after this period.
;LOG_RETENTION_DAYS = 365
;; Log compression type, `none` for no compression, `zstd` for zstd compression.
;; Other compression types like `gzip` are NOT supported, since seekable stream is required for log view.
;; It's always recommended to use compression when using local disk as log storage if CPU or memory is not a bottleneck.
;; And for object storage services like S3, which is billed for requests, it would cause extra 2 times of get requests for each log view.
;; But it will save storage space and network bandwidth, so it's still recommended to use compression.
;LOG_COMPRESSION = zstd
;; Default artifact retention time in days. Artifacts could have their own retention periods by setting the `retention-days` option in `actions/upload-artifact` step.
;; Days to keep logs. Old logs will be deleted after this period. 0 means keep forever.
;LOG_RETENTION_DAYS = 365
;; Days to keep artifacts. Old artifacts will be deleted after this period. 0 means keep forever.
;; Changes only apply to newly uploaded artifacts, existing ones keep the expiry stored when they were uploaded.
;; Artifacts could have their own retention periods by setting the `retention-days` option in `actions/upload-artifact` step.
;ARTIFACT_RETENTION_DAYS = 90
;; Days to keep completed runs. Old runs and everything under them will be deleted after this period. 0 means keep forever.
;RUN_RETENTION_DAYS = 400
;; Timeout to stop the task which have running status, but haven't been updated for a long time
;ZOMBIE_TASK_TIMEOUT = 10m
;; Timeout to stop the tasks which have running status and continuous updates, but don't end for a long time
+9 -6
View File
@@ -6,7 +6,7 @@ and testing see [development.md](development.md) and [testing.md](testing.md).
## Background
The frontend uses [Vue 3](https://vuejs.org/), [Fomantic-UI](https://fomantic-ui.com/) (built on jQuery)
The frontend uses [Vue 3](https://vuejs.org/), hard-forked Fomantic-UI (built on jQuery)
and [Tailwind CSS](https://tailwindcss.com/). Pages are rendered with Go HTML templates.
Source files live in:
@@ -44,8 +44,10 @@ Gitea uses Vue 3 **without** JSX to keep HTML and JavaScript separate.
## Gitea-specific conventions
- Keep features in their own files or directories.
- Use kebab-case for HTML `id`s and classes, ideally with 2-3 feature keywords.
- Use kebab-case for HTML `id`s and classes with 2-3 feature keywords.
- Prefix classes to avoid short-name conflicts between different frameworks.
- Our framework can automatically link "input" and "label" if they are the children of a `.field` element,
no need to write `id`/`for` attributes for them unless there are reasons to do so.
- Create a new class name when overriding framework styles instead of editing the framework's own classes,
or fix the framework's source to fix all cases.
- Prefer semantic elements such as `<button>` over generic `<div>`s.
@@ -68,17 +70,18 @@ Write class attributes as a single readable unit in templates:
## TypeScript
- Use `import type` for type-only imports.
- Prefer `@ts-expect-error` over `@ts-ignore`.
- Use the `!` non-null assertion (rather than `?.`/`??`) when a value is known to always exist.
- Only mark a function `async` when it actually uses `await` or returns a `Promise`.
Avoid async event listeners; if unavoidable, call `e.preventDefault()` before the
first `await`. For a deliberately un-awaited call, assign it: `const _promise = asyncFoo()`.
Avoid async event listeners; if unavoidable, call `e.preventDefault()` before the first `await`.
## Data fetching
Use the `GET`, `POST`, `PUT`, `PATCH`, and `DELETE` wrappers from
[`web_src/js/modules/fetch.ts`](../web_src/js/modules/fetch.ts).
Prefer to use our [`fetch-action.ts`](../web_src/js/modules/fetch-action.ts) framework
for form submissions, button clicks and network requests, which provides a consistent UX and error handling.
## DOM attributes
Avoid `node.dataset` because of its camel-casing behavior; use `node.getAttribute`
@@ -86,7 +89,7 @@ in new code. Never bind user-provided data directly onto DOM nodes.
## Showing and hiding elements
- In Vue, use `v-if` and `v-show`.
- In Vue, use `v-if` and `v-show`. If an element contains unmanaged DOM, use `v-show` to avoid losing the DOM state.
- In Go templates and plain JavaScript, use the `.tw-hidden` class together with the
`showElem()`, `hideElem()`, and `toggleElem()` helpers from
[`web_src/js/utils/dom.ts`](../web_src/js/utils/dom.ts).
+1 -1
View File
@@ -23,7 +23,7 @@ go test -run '^TestName$' ./modulepath/
make test-backend#TestName
```
Frontend unit tests run with [Vitest](https://vitest.dev/):
Frontend unit tests run with [Vitest](https://vitest.dev/) browser mode:
```bash
make test-frontend
+8 -7
View File
@@ -79,7 +79,7 @@ export default defineConfig([
'@eslint-community/eslint-comments/no-unlimited-disable': [2],
'@eslint-community/eslint-comments/no-unused-enable': [2],
'@eslint-community/eslint-comments/no-use': [0],
'@eslint-community/eslint-comments/require-description': [0],
'@eslint-community/eslint-comments/require-description': [2, {ignore: ['eslint', 'eslint-enable', 'eslint-env', 'exported', 'global', 'globals']}],
'@stylistic/array-bracket-newline': [0],
'@stylistic/array-bracket-spacing': [2, 'never'],
'@stylistic/array-element-newline': [0],
@@ -258,7 +258,7 @@ export default defineConfig([
'@typescript-eslint/prefer-function-type': [2],
'@typescript-eslint/prefer-includes': [2],
'@typescript-eslint/prefer-literal-enum-member': [0],
'@typescript-eslint/prefer-namespace-keyword': [2],
'@typescript-eslint/prefer-namespace-keyword': [0], // handled by @typescript-eslint/no-namespace
'@typescript-eslint/prefer-nullish-coalescing': [0],
'@typescript-eslint/prefer-optional-chain': [2, {requireNullish: true}],
'@typescript-eslint/prefer-promise-reject-errors': [2],
@@ -531,8 +531,8 @@ export default defineConfig([
'no-nonoctal-decimal-escape': [2],
'no-obj-calls': [2],
'no-object-constructor': [2],
'no-octal-escape': [2],
'no-octal': [2],
'no-octal-escape': [0], // parse error under strict mode
'no-octal': [0], // parse error under strict mode
'no-param-reassign': [0],
'no-plusplus': [0],
'no-promise-executor-return': [0],
@@ -581,7 +581,7 @@ export default defineConfig([
'no-useless-call': [2],
'no-useless-catch': [2],
'no-useless-computed-key': [2],
'no-useless-concat': [2],
'no-useless-concat': [0], // handled by unicorn/no-useless-concat
'no-useless-constructor': [2],
'no-useless-escape': [2],
'no-useless-rename': [2],
@@ -592,7 +592,7 @@ export default defineConfig([
'no-with': [0], // handled by no-restricted-syntax
'object-shorthand': [2, 'always'],
'one-var': [0],
'operator-assignment': [2, 'always'],
'operator-assignment': [0], // handled by unicorn/operator-assignment
'prefer-arrow-callback': [2, {allowNamedFunctions: true, allowUnboundThis: true}],
'prefer-const': [2, {destructuring: 'all', ignoreReadBeforeAssign: true}],
'prefer-destructuring': [0],
@@ -1095,7 +1095,8 @@ export default defineConfig([
},
},
{
files: ['**/*.test.ts', 'web_src/js/test/setup.ts'],
files: ['**/*.test.ts', 'web_src/js/vitest.setup.ts'],
ignores: ['tests/e2e/**'],
plugins: {vitest},
languageOptions: {globals: globals.vitest},
rules: {
+32 -31
View File
@@ -1,12 +1,12 @@
module gitea.dev
go 1.26.0
go 1.27
toolchain go1.26.5
toolchain go1.27.0
require (
connectrpc.com/connect v1.20.0
gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a
gitea.com/go-chi/binding v0.0.0-20260819122636-082915a69981
gitea.com/go-chi/cache v0.2.1
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
gitea.com/go-chi/session v0.0.0-20260708011333-ebced8a7a2d6
@@ -24,17 +24,17 @@ require (
github.com/PuerkitoBio/goquery v1.12.0
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0
github.com/alecthomas/chroma/v2 v2.27.0
github.com/aws/aws-sdk-go-v2/credentials v1.19.33
github.com/aws/aws-sdk-go-v2/service/codecommit v1.36.3
github.com/aws/aws-sdk-go-v2/credentials v1.19.36
github.com/aws/aws-sdk-go-v2/service/codecommit v1.38.1
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
github.com/blevesearch/bleve/v2 v2.6.0
github.com/bohde/codel v0.2.0
github.com/buildkite/terminal-to-html/v3 v3.17.1
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.20260812203852-971c0284dc33
github.com/chi-middleware/proxy v1.1.1
github.com/coder/websocket v1.8.15
github.com/dlclark/regexp2/v2 v2.5.2
github.com/dlclark/regexp2/v2 v2.7.1
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
github.com/dustin/go-humanize v1.0.1
github.com/editorconfig/editorconfig-core-go/v2 v2.6.4
@@ -43,7 +43,7 @@ require (
github.com/ethantkoenig/rupture v1.0.1
github.com/felixge/fgprof v0.9.5
github.com/fsnotify/fsnotify v1.10.1
github.com/getkin/kin-openapi v0.146.0
github.com/getkin/kin-openapi v0.147.0
github.com/go-chi/chi/v5 v5.3.1
github.com/go-chi/cors v1.2.2
github.com/go-co-op/gocron/v2 v2.22.0
@@ -60,7 +60,6 @@ require (
github.com/google/go-github/v89 v89.0.0
github.com/google/licenseclassifier/v2 v2.0.0
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3
github.com/google/uuid v1.6.0
github.com/gorilla/feeds v1.2.0
github.com/gorilla/sessions v1.4.0
github.com/hashicorp/go-version v1.9.0
@@ -68,16 +67,16 @@ require (
github.com/huandu/xstrings v1.5.0
github.com/jhillyerd/enmime/v2 v2.4.1
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/klauspost/compress v1.19.1
github.com/klauspost/compress v1.19.2
github.com/lib/pq v1.12.3
github.com/markbates/goth v1.82.0
github.com/mattn/go-isatty v0.0.24
github.com/mattn/go-sqlite3 v1.14.49
github.com/mattn/go-sqlite3 v1.14.50
github.com/meilisearch/meilisearch-go v0.36.3
github.com/mholt/archives v0.1.5
github.com/microcosm-cc/bluemonday v1.0.27
github.com/microsoft/go-mssqldb v1.10.0
github.com/minio/minio-go/v7 v7.2.1
github.com/minio/minio-go/v7 v7.3.0
github.com/msteinert/pam/v2 v2.1.0
github.com/niklasfasching/go-org v1.9.1
github.com/opencontainers/go-digest v1.0.0
@@ -91,30 +90,30 @@ require (
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3
github.com/sassoftware/go-rpmutils v0.4.0
github.com/sergi/go-diff v1.4.0
github.com/stretchr/testify v1.11.1
github.com/stretchr/testify v1.12.1
github.com/syndtr/goleveldb v1.0.0
github.com/tstranex/u2f v1.0.0
github.com/ulikunitz/xz v0.5.16
github.com/urfave/cli-docs/v3 v3.1.0
github.com/urfave/cli/v3 v3.10.1
github.com/urfave/cli/v3 v3.11.0
github.com/wneessen/go-mail v0.8.1
github.com/yohcop/openid-go v1.0.1
github.com/yuin/goldmark v1.8.5
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
gitlab.com/gitlab-org/api/client-go/v2 v2.55.1
gitlab.com/gitlab-org/api/client-go/v2 v2.58.1
go.yaml.in/yaml/v4 v4.0.0-rc.5
golang.org/x/crypto v0.54.0
golang.org/x/image v0.44.0
golang.org/x/mod v0.38.0
golang.org/x/net v0.57.0
golang.org/x/crypto v0.55.0
golang.org/x/image v0.45.0
golang.org/x/mod v0.40.0
golang.org/x/net v0.58.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.22.0
golang.org/x/sys v0.47.0
golang.org/x/text v0.40.0
google.golang.org/grpc v1.83.0
google.golang.org/protobuf v1.36.11
golang.org/x/text v0.41.0
google.golang.org/grpc v1.83.1
google.golang.org/protobuf v1.36.12
gopkg.in/ini.v1 v1.67.3
modernc.org/sqlite v1.56.0
modernc.org/sqlite v1.57.0
mvdan.cc/xurls/v2 v2.6.0
xorm.io/builder v0.3.13
xorm.io/xorm v1.4.1
@@ -131,10 +130,10 @@ require (
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/andybalholm/cascadia v1.3.4 // indirect
github.com/aws/aws-sdk-go-v2 v1.43.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.34 // indirect
github.com/aws/smithy-go v1.27.6 // indirect
github.com/aws/aws-sdk-go-v2 v1.43.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.37 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.37 // indirect
github.com/aws/smithy-go v1.27.8 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.5 // indirect
@@ -194,6 +193,7 @@ require (
github.com/google/flatbuffers v25.12.19+incompatible // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/google/go-tpm v0.9.8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
@@ -250,7 +250,6 @@ require (
github.com/spf13/afero v1.15.0 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/stangelandcl/ppmd v0.1.1 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/tinylib/msgp v1.6.4 // indirect
github.com/unknwon/com v1.0.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
@@ -263,13 +262,12 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.28.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.47.0 // indirect
golang.org/x/tools v0.49.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.74.4 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
@@ -278,6 +276,9 @@ require (
ignore (
./.venv
./node_modules
./public
./vendor
./web_src
)
// When doing "go get -u ./...", Golang will try to update all dependencies
+52 -51
View File
@@ -8,8 +8,8 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a h1:JHoBrfuTSF9Ke9aNfSYj1XRPBHjKPgCApVprnt2Am0M=
gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a/go.mod h1:FOsLJIMdpiHzBp3Vby6Wfkdw2ppGscrjgU1IC7E4/zQ=
gitea.com/go-chi/binding v0.0.0-20260819122636-082915a69981 h1:LmdlwGbzgZFZA3bK3R1q8QrNabaSz3KpnOJBLmzhN6E=
gitea.com/go-chi/binding v0.0.0-20260819122636-082915a69981/go.mod h1:q1SSPpkC9A0gfNnoqqZ3My6kEHIpKN6QsTI+Zx73B/o=
gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g=
gitea.com/go-chi/cache v0.2.1/go.mod h1:Qic0HZ8hOHW62ETGbonpwz8WYypj9NieU9659wFUJ8Q=
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo=
@@ -88,18 +88,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/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/aws/aws-sdk-go-v2 v1.43.3 h1:XJIcfv8uDs2ukdQsoAC8/Ebu1ejxwzlayl2ZsiFns2A=
github.com/aws/aws-sdk-go-v2 v1.43.3/go.mod h1:70vwSy16txshwG+g55WkpgPKDIByzHI8ccBsOteo3bQ=
github.com/aws/aws-sdk-go-v2/credentials v1.19.33 h1:/e5V3EWfeDiW6cuRxHsC8gbwko4/vvVYPJR2afBKFFY=
github.com/aws/aws-sdk-go-v2/credentials v1.19.33/go.mod h1:ZxAmkcyOM9beY/WO9oxp2oVPXiP3rq5N1/p4NbenJdE=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.34 h1:vuIfjzoeqhQMGJyOBU3t0ZEjn2jrN8Bbg1N4CgjzM5Q=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.34/go.mod h1:hP28cN4CPJLZHirdQPrZR50JcLN4ApRJP2tzG8cRlhY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.34 h1:9faHsnqxJ1vDvB4wMZy/ajIDyz5QhllQjjc72RJpXAw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.34/go.mod h1:Yp6nIyejpa23nzlB/LhT63KTla9Jdi06nv/HH/OkAH8=
github.com/aws/aws-sdk-go-v2/service/codecommit v1.36.3 h1:0y4NzpyfbufOOZkJ54zJHJl/IC/tj/ACd6E8ViCIZZM=
github.com/aws/aws-sdk-go-v2/service/codecommit v1.36.3/go.mod h1:38Fc43+yPVnGM70Bqh8N7tbUVssQDVwnnFU+49sanPQ=
github.com/aws/smithy-go v1.27.6 h1:0zjT8jgK3jbrTT7JJ3EE6JsMhX8JTrZ+f1sEndYDXrA=
github.com/aws/smithy-go v1.27.6/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aws/aws-sdk-go-v2 v1.43.6 h1:RrmFcqCBxkJuf7g1axVo5krB4jM/AO8r5e5oujrgdoQ=
github.com/aws/aws-sdk-go-v2 v1.43.6/go.mod h1:tXpPM+v0D1lndmga+HqqLDIzUFJlEeR21aspVklHF00=
github.com/aws/aws-sdk-go-v2/credentials v1.19.36 h1:84s5xMme6ENYEdKG8rsbSFFg/8+lbHBeM9QYSO0gnDk=
github.com/aws/aws-sdk-go-v2/credentials v1.19.36/go.mod h1:c46BLdagDLIswjgt+GeQOslXgeS0E6wCacs5yZbxPGk=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.37 h1:lznzIOvvbqjfe8UAaciCRJgBgJsxuTROKlhZuXQWfv8=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.37/go.mod h1:otfkzyfQeMMLZAqX59GSXTL3o22BR/l6HFaRzzbWSqA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.37 h1:zCEORWo0eU0gDjG+IyApE/2B+ZGG1m+GU7B263XV8ds=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.37/go.mod h1:i6c0PEl3TNOWxRbQ++KQcVenPWS/GoQeiklKhNuqzJ8=
github.com/aws/aws-sdk-go-v2/service/codecommit v1.38.1 h1:9/0/sqeplR52m0qWjE00wY8r+jkImLOnu6oat5mjG2o=
github.com/aws/aws-sdk-go-v2/service/codecommit v1.38.1/go.mod h1:ivXqR70vBBk3Snr4IaOABAvuF7ipZpViobAIiAS8AEQ=
github.com/aws/smithy-go v1.27.8 h1:FR0dxZfIlV7Z8eh2iHfIofdunw382XsDV3Mxt9nUvRY=
github.com/aws/smithy-go v1.27.8/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
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/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -191,8 +191,8 @@ github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20260309112543-12416315a635 h1:RwCfD5XyO8jAermEy6pauh5Q5o6mCvbeNcCPOZlIA5o=
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20260309112543-12416315a635/go.mod h1:R+SetERD4+IL7QH0WHp9MLifvITvh9gL3g8vX1j2Fcs=
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20260812203852-971c0284dc33 h1:YG5mXlv9SxS4rdtSxK3RuH146x1HIQzsbVJqSp/Rvfc=
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20260812203852-971c0284dc33/go.mod h1:R+SetERD4+IL7QH0WHp9MLifvITvh9gL3g8vX1j2Fcs=
github.com/chi-middleware/proxy v1.1.1 h1:4HaXUp8o2+bhHr1OhVy+VjN0+L7/07JDcn6v7YrTjrQ=
github.com/chi-middleware/proxy v1.1.1/go.mod h1:jQwMEJct2tz9VmtCELxvnXoMfa+SOdikvbVJVHv/M+0=
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
@@ -238,8 +238,8 @@ github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2/v2 v2.5.2 h1:HAsucWRhsqcDzl6Ua9aR8JwYOTzrZyPrF0/FNxJVAI0=
github.com/dlclark/regexp2/v2 v2.5.2/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dlclark/regexp2/v2 v2.7.1 h1:yqDtwI1ptXXvEUNpYTk2lad4jLtAcKqkzepn4savSk4=
github.com/dlclark/regexp2/v2 v2.7.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
@@ -272,8 +272,8 @@ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx5
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/getkin/kin-openapi v0.146.0 h1:RA/1RdxrSJW4oc1+6IfnYB6AO9CaGy8GTKPh0k4Ordo=
github.com/getkin/kin-openapi v0.146.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY=
github.com/getkin/kin-openapi v0.147.0 h1:s+Xsm9gUMPJbgCnABZ2to3zSQQ5A9dyj/zo62VVsldY=
github.com/getkin/kin-openapi v0.147.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY=
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 h1:mtDjlmloH7ytdblogrMz1/8Hqua1y8B4ID+bh3rvod0=
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1/go.mod h1:fenKRzpXDjNpsIBhuhUzvjCKlDjKam0boRAenTE0Q6A=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
@@ -462,8 +462,8 @@ 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/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
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.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
@@ -505,8 +505,8 @@ github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/a
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-shellwords v1.0.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXNtDcf4=
github.com/mattn/go-shellwords v1.0.13/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.14.49 h1:B8jBHC3xhxZgxztrgruTuLucebnULQnx4W7cF7SAE9w=
github.com/mattn/go-sqlite3 v1.14.49/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/mattn/go-sqlite3 v1.14.50 h1:dmdFvo1XG4MPzA4IkAmE9upVz/Nj31uRoM5+jC8hYbY=
github.com/mattn/go-sqlite3 v1.14.50/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/meilisearch/meilisearch-go v0.36.3 h1:Yx1aTY5jDgtbStPVkhJTDoLnZTy5sejQSPyjfNMy6e4=
github.com/meilisearch/meilisearch-go v0.36.3/go.mod h1:hWcR0MuWLSzHfbz9GGzIr3s9rnXLm1jqkmHkJPbUSvM=
github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk=
@@ -525,8 +525,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/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/minio-go/v7 v7.2.1 h1:PfBfwvKB/MmqyN8Vb1G9voWisaM9OrLv+WwOvMwS9Dw=
github.com/minio/minio-go/v7 v7.2.1/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0=
github.com/minio/minio-go/v7 v7.3.0 h1:HM4pFCSQq/TK+j0/zmorSh5ddh81iDgRgU0BG0Vz/YU=
github.com/minio/minio-go/v7 v7.3.0/go.mod h1:KUPWdecEO1LWyUz+sTGXAuf2jZHrPh5fCsRH86QbPfk=
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/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -681,8 +681,9 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM=
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
@@ -700,8 +701,8 @@ 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/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/v3 v3.10.1 h1:7Kx9H50hrHbRbyxgO1KP6/BcbiGRz0uYh5YyQ30JEEY=
github.com/urfave/cli/v3 v3.10.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/urfave/cli/v3 v3.11.0 h1:P/euJp99kb9p0tlVY+iYTLYYTAQlfl0hR2gUO1Img1Q=
github.com/urfave/cli/v3 v3.11.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/wneessen/go-mail v0.8.1 h1:tVcncj02/QySVFw3zr/kXOzZcuFQqBNT6K+Rbgm/pcM=
github.com/wneessen/go-mail v0.8.1/go.mod h1:dWZ61zadzCIyvB4y1/YzC5O7MrbbzBfPkARmbosdf8w=
@@ -731,8 +732,8 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
gitlab.com/gitlab-org/api/client-go/v2 v2.55.1 h1:eX0iBhJ9GWP2XqQOH/pkmSvPosFS3rcyFcup/3kHL2k=
gitlab.com/gitlab-org/api/client-go/v2 v2.55.1/go.mod h1:6GcBiCVrZBtOvl6HiahZ3nq5eBs7JMUARJeLN4/RgZU=
gitlab.com/gitlab-org/api/client-go/v2 v2.58.1 h1:XMuEYGaruQ3Yu7RFGE4b1fmi//QkAPUisq9LV9jahbA=
gitlab.com/gitlab-org/api/client-go/v2 v2.58.1/go.mod h1:tuYYHZSRj9eKea28W3uySf9bSqfkE2RknDpBdzxdnhk=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
@@ -750,8 +751,8 @@ go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
@@ -767,12 +768,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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
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.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -781,8 +782,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -798,8 +799,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.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -868,8 +869,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -881,24 +882,24 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ=
google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y=
google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@@ -941,8 +942,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.56.0 h1:/D8e2RfFqoy/Zc6PuC76U28zFwmI/sYx1Kjm4yEn9e0=
modernc.org/sqlite v1.56.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ=
modernc.org/sqlite v1.57.0 h1:qNQP6xnx5M0ISNtlnxoOX0+cD5bJ0/gr9aMmndFczzg=
modernc.org/sqlite v1.57.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+3
View File
@@ -422,6 +422,9 @@ func prepareMigrationTasks() []*migration {
newMigration(346, "Add license_path column to repo_license and backfill", v28.AddLicensePathToRepoLicense),
newMigration(347, "Add watch options", v28.AddWatchOptions),
newMigration(348, "Recreate email_hash table for SHA256 avatar hashes", v28.RecreateEmailHashTable),
newMigration(349, "Expand action_schedule content column", v28.ExpandActionScheduleContent),
newMigration(350, "Add published_unix column to release", v28.AddPublishedUnixToRelease),
newMigration(351, "Track transfer recipient access grants", v28.AddRecipientAccessGrantedToRepoTransfer),
}
return preparedMigrations
}
+3 -3
View File
@@ -13,9 +13,9 @@ import (
func AddWatchOptions(_ context.Context, x base.EngineMigration) error {
type Watch struct {
PullRequests bool `xorm:"NOT NULL DEFAULT true"`
Issues bool `xorm:"NOT NULL DEFAULT true"`
Releases bool `xorm:"NOT NULL DEFAULT true"`
IncludePullRequests bool `xorm:"NOT NULL DEFAULT true"`
IncludeIssues bool `xorm:"NOT NULL DEFAULT true"`
IncludeReleases bool `xorm:"NOT NULL DEFAULT true"`
}
_, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreConstrains: true,
+29
View File
@@ -0,0 +1,29 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v28
import (
"context"
"gitea.dev/modelmigration/base"
"gitea.dev/modules/setting"
"xorm.io/xorm/schemas"
)
func ExpandActionScheduleContent(ctx context.Context, x base.EngineMigration) error {
if !setting.Database.Type.IsMySQL() {
return nil
}
return base.ModifyColumn(ctx, x, "action_schedule", &schemas.Column{
Name: "content",
SQLType: schemas.SQLType{
Name: "LONGBLOB",
},
Length: 0,
Nullable: true,
DefaultIsEmpty: true,
})
}
+47
View File
@@ -0,0 +1,47 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v28
import (
"bytes"
"testing"
"gitea.dev/modelmigration/migrationtest"
"gitea.dev/modules/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestExpandActionScheduleContent(t *testing.T) {
if !setting.Database.Type.IsMySQL() {
t.Skip("Only MySQL limits BLOB columns to 65,535 bytes")
}
type ActionSchedule struct {
ID int64 `xorm:"pk autoincr"`
Content []byte `xorm:"BLOB"`
}
x, deferable := migrationtest.PrepareTestEnv(t, 0, new(ActionSchedule))
defer deferable()
if x == nil || t.Failed() {
return
}
require.NoError(t, ExpandActionScheduleContent(t.Context(), x))
tables := migrationtest.LoadTableSchemasMap(t, x)
assert.Equal(t, "LONGBLOB", tables["action_schedule"].GetColumn("content").SQLType.Name)
content := bytes.Repeat([]byte("x"), 65_536)
_, err := x.Insert(&ActionSchedule{Content: content})
require.NoError(t, err)
var stored ActionSchedule
has, err := x.Get(&stored)
require.NoError(t, err)
require.True(t, has)
assert.Equal(t, content, stored.Content)
}
+28
View File
@@ -0,0 +1,28 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v28
import (
"context"
"gitea.dev/modelmigration/base"
"xorm.io/xorm"
)
func AddPublishedUnixToRelease(_ context.Context, x base.EngineMigration) error {
type Release struct {
PublishedUnix int64 `xorm:"NOT NULL DEFAULT 0"`
}
if _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreConstrains: true,
IgnoreDropIndices: true,
}, new(Release)); err != nil {
return err
}
// existing rows have no recorded publication time, so fall back to their creation time
_, err := x.Exec("UPDATE `release` SET published_unix = created_unix WHERE published_unix = 0 AND is_draft = ?", false)
return err
}
+41
View File
@@ -0,0 +1,41 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v28
import (
"testing"
"gitea.dev/modelmigration/migrationtest"
"github.com/stretchr/testify/require"
)
func TestAddPublishedUnixToRelease(t *testing.T) {
type Release struct {
ID int64 `xorm:"pk autoincr"`
IsDraft bool `xorm:"NOT NULL DEFAULT false"`
IsTag bool `xorm:"NOT NULL DEFAULT false"`
CreatedUnix int64 `xorm:"INDEX"`
}
x, deferable := migrationtest.PrepareTestEnv(t, 0, new(Release))
defer deferable()
if x == nil || t.Failed() {
return
}
_, err := x.Insert(
&Release{CreatedUnix: 1000000},
&Release{IsDraft: true, CreatedUnix: 2000000},
&Release{IsTag: true, CreatedUnix: 3000000},
)
require.NoError(t, err)
require.NoError(t, AddPublishedUnixToRelease(t.Context(), x))
var got []struct{ PublishedUnix int64 }
require.NoError(t, x.Table("release").OrderBy("id").Find(&got))
require.Equal(t, []int64{1000000, 0, 3000000}, []int64{got[0].PublishedUnix, got[1].PublishedUnix, got[2].PublishedUnix},
"everything but drafts is backfilled")
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v28
import (
"context"
"gitea.dev/modelmigration/base"
"xorm.io/xorm"
)
func AddRecipientAccessGrantedToRepoTransfer(_ context.Context, x base.EngineMigration) error {
type RepoTransfer struct {
RecipientAccessGranted bool `xorm:"NOT NULL DEFAULT false"`
}
_, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreConstrains: true,
IgnoreDropIndices: true,
}, new(RepoTransfer))
return err
}
+76 -52
View File
@@ -8,13 +8,12 @@ package actions
import (
"context"
"errors"
"time"
"slices"
"gitea.dev/models/db"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/builder"
)
@@ -23,12 +22,12 @@ import (
type ArtifactStatus int64
const (
ArtifactStatusUploadPending ArtifactStatus = iota + 1 // 1 ArtifactStatusUploadPending is the status of an artifact upload that is pending
ArtifactStatusUploadConfirmed // 2 ArtifactStatusUploadConfirmed is the status of an artifact upload that is confirmed
ArtifactStatusUploadError // 3 ArtifactStatusUploadError is the status of an artifact upload that is errored
ArtifactStatusExpired // 4, ArtifactStatusExpired is the status of an artifact that is expired
ArtifactStatusPendingDeletion // 5, ArtifactStatusPendingDeletion is the status of an artifact that is pending deletion
ArtifactStatusDeleted // 6, ArtifactStatusDeleted is the status of an artifact that is deleted
ArtifactStatusUploadPending ArtifactStatus = iota + 1
ArtifactStatusUploadConfirmed
ArtifactStatusUploadError // unused, kept so the numbering below stays stable
ArtifactStatusExpired
ArtifactStatusPendingDeletion
ArtifactStatusDeleted
)
func (status ArtifactStatus) ToString() string {
@@ -87,15 +86,36 @@ type ActionArtifact struct {
Status ArtifactStatus `xorm:"index"` // The status of the artifact, uploading, expired or need-delete
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated index"`
ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired
ExpiredUnix timeutil.TimeStamp `xorm:"index"` // 0 means the artifact is kept forever
}
func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPath string, expiredDays int64) (*ActionArtifact, error) {
const artifactKeepForever timeutil.TimeStamp = 0
func artifactExpiry(requested optional.Option[timeutil.TimeStamp]) timeutil.TimeStamp {
if requested.Has() {
return max(requested.Value(), artifactKeepForever+1)
}
if setting.Actions.ArtifactRetentionDays <= 0 {
return artifactKeepForever
}
return timeutil.TimeStampNow().Add(timeutil.Day * setting.Actions.ArtifactRetentionDays)
}
// CreateArtifact returns the artifact for the name and path, creating it on first upload and refreshing its expiry either way.
func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPath string, expiry optional.Option[timeutil.TimeStamp]) (*ActionArtifact, error) {
if err := t.LoadJob(ctx); err != nil {
return nil, err
}
artifact, err := getArtifactByNameAndPath(ctx, t.Job.RunID, t.Job.RunAttemptID, artifactName, artifactPath)
if errors.Is(err, util.ErrNotExist) {
expiredUnix := artifactExpiry(expiry)
artifact, exist, err := db.Get[ActionArtifact](ctx, builder.Eq{
"run_id": t.Job.RunID, "run_attempt_id": t.Job.RunAttemptID,
"artifact_name": artifactName, "artifact_path": artifactPath,
})
if err != nil {
return nil, err
}
if !exist {
artifact := &ActionArtifact{
ArtifactName: artifactName,
ArtifactPath: artifactPath,
@@ -106,40 +126,24 @@ func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPa
OwnerID: t.OwnerID,
CommitSHA: t.CommitSHA,
Status: ArtifactStatusUploadPending,
ExpiredUnix: timeutil.TimeStamp(time.Now().Unix() + timeutil.Day*expiredDays),
ExpiredUnix: expiredUnix,
}
if _, err := db.GetEngine(ctx).Insert(artifact); err != nil {
return nil, err
}
return artifact, nil
} else if err != nil {
return nil, err
}
if _, err := db.GetEngine(ctx).ID(artifact.ID).Cols("expired_unix").Update(&ActionArtifact{
ExpiredUnix: timeutil.TimeStamp(time.Now().Unix() + timeutil.Day*expiredDays),
}); err != nil {
artifact.ExpiredUnix = expiredUnix
if err := UpdateArtifact(ctx, artifact, "expired_unix"); err != nil {
return nil, err
}
return artifact, nil
}
func getArtifactByNameAndPath(ctx context.Context, runID, runAttemptID int64, name, fpath string) (*ActionArtifact, error) {
var art ActionArtifact
has, err := db.GetEngine(ctx).Where("run_id = ? AND run_attempt_id = ? AND artifact_name = ? AND artifact_path = ?", runID, runAttemptID, name, fpath).Get(&art)
if err != nil {
return nil, err
} else if !has {
return nil, util.ErrNotExist
}
return &art, nil
}
// UpdateArtifactByID updates an artifact by id
func UpdateArtifactByID(ctx context.Context, id int64, art *ActionArtifact) error {
art.ID = id
_, err := db.GetEngine(ctx).ID(id).AllCols().Update(art)
func UpdateArtifact(ctx context.Context, art *ActionArtifact, cols ...string) error {
_, err := db.GetEngine(ctx).ID(art.ID).Cols(cols...).Update(art)
return err
}
@@ -147,9 +151,9 @@ type FindArtifactsOptions struct {
db.ListOptions
RepoID int64
RunID int64
RunAttemptID optional.Option[int64] // use optional to allow filtering by zero (legacy artifacts have run_attempt_id=0)
RunAttemptIDs []int64 // empty means every attempt; pass 0 to target legacy artifacts, which have run_attempt_id=0
ArtifactName string
Status int
Status ArtifactStatus
FinalizedArtifactsV4 bool
}
@@ -157,7 +161,7 @@ func (opts FindArtifactsOptions) ToOrders() string {
return "id"
}
var _ db.FindOptionsOrder = (*FindArtifactsOptions)(nil)
var _ db.FindOptions = (*FindArtifactsOptions)(nil)
func (opts FindArtifactsOptions) ToConds() builder.Cond {
cond := builder.NewCond()
@@ -167,8 +171,8 @@ func (opts FindArtifactsOptions) ToConds() builder.Cond {
if opts.RunID > 0 {
cond = cond.And(builder.Eq{"run_id": opts.RunID})
}
if opts.RunAttemptID.Has() {
cond = cond.And(builder.Eq{"run_attempt_id": opts.RunAttemptID.Value()})
if len(opts.RunAttemptIDs) > 0 {
cond = cond.And(builder.In("run_attempt_id", opts.RunAttemptIDs))
}
if opts.ArtifactName != "" {
cond = cond.And(builder.Eq{"artifact_name": opts.ArtifactName})
@@ -185,6 +189,27 @@ func (opts FindArtifactsOptions) ToConds() builder.Cond {
return cond
}
// FindReadableArtifacts returns the artifacts of opts.RunAttemptIDs, only keeps the ones from a newer attempt.
func FindReadableArtifacts(ctx context.Context, opts FindArtifactsOptions) ([]*ActionArtifact, error) {
arts, err := db.Find[ActionArtifact](ctx, opts)
if err != nil || len(opts.RunAttemptIDs) <= 1 {
return arts, err
}
return keepLatestAttemptArtifacts(arts), nil
}
// keepLatestAttemptArtifacts keeps, per name, only the artifacts of the newest attempt that has it.
// A v3 artifact is one row per uploaded file, so the whole group of the winning attempt is kept.
func keepLatestAttemptArtifacts(arts []*ActionArtifact) []*ActionArtifact {
latest := make(map[string]int64)
for _, art := range arts {
latest[art.ArtifactName] = max(latest[art.ArtifactName], art.RunAttemptID)
}
return slices.DeleteFunc(arts, func(art *ActionArtifact) bool {
return art.RunAttemptID != latest[art.ArtifactName]
})
}
// ActionArtifactMeta is the meta-data of an artifact
type ActionArtifactMeta struct {
ArtifactName string
@@ -208,7 +233,7 @@ func ListUploadedArtifactsMetaByRunAttempt(ctx context.Context, repoID, runID, r
func ListNeedExpiredArtifacts(ctx context.Context) ([]*ActionArtifact, error) {
arts := make([]*ActionArtifact, 0, 10)
return arts, db.GetEngine(ctx).
Where("expired_unix < ? AND status = ?", timeutil.TimeStamp(time.Now().Unix()), ArtifactStatusUploadConfirmed).Find(&arts)
Where("expired_unix > ? AND expired_unix < ? AND status = ?", artifactKeepForever, timeutil.TimeStampNow(), ArtifactStatusUploadConfirmed).Find(&arts)
}
// ListPendingDeleteArtifacts returns all artifacts in pending-delete status.
@@ -219,23 +244,24 @@ func ListPendingDeleteArtifacts(ctx context.Context, limit int) ([]*ActionArtifa
Where("status = ?", ArtifactStatusPendingDeletion).Limit(limit).Find(&arts)
}
// SetArtifactExpired sets an artifact to expired
func setConfirmedArtifactsStatus(ctx context.Context, status ArtifactStatus, cond builder.Cond) error {
_, err := db.GetEngine(ctx).Where(cond).And(builder.Eq{"status": ArtifactStatusUploadConfirmed}).
Cols("status").Update(&ActionArtifact{Status: status})
return err
}
func SetArtifactExpired(ctx context.Context, artifactID int64) error {
_, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusExpired})
return err
return setConfirmedArtifactsStatus(ctx, ArtifactStatusExpired, builder.Eq{"id": artifactID})
}
// SetArtifactNeedDeleteByID sets an artifact to need-delete by ID, cron job will delete it.
func SetArtifactNeedDeleteByID(ctx context.Context, artifactID int64) error {
_, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusPendingDeletion})
return err
return setConfirmedArtifactsStatus(ctx, ArtifactStatusPendingDeletion, builder.Eq{"id": artifactID})
}
// SetArtifactNeedDeleteByRunAttempt sets an artifact to need-delete in a run attempt, cron job will delete it.
// runAttemptID may be 0 for legacy artifacts created before ActionRunAttempt existed.
func SetArtifactNeedDeleteByRunAttempt(ctx context.Context, runID, runAttemptID int64, name string) error {
_, err := db.GetEngine(ctx).Where("run_id=? AND run_attempt_id=? AND artifact_name=? AND status = ?", runID, runAttemptID, name, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusPendingDeletion})
return err
return setConfirmedArtifactsStatus(ctx, ArtifactStatusPendingDeletion,
builder.Eq{"run_id": runID, "run_attempt_id": runAttemptID, "artifact_name": name})
}
// GetArtifactsByRunAttemptAndName returns all artifacts with the given name in the specified run attempt.
@@ -248,8 +274,6 @@ func GetArtifactsByRunAttemptAndName(ctx context.Context, runID, runAttemptID in
Find(&arts)
}
// SetArtifactDeleted sets an artifact to deleted
func SetArtifactDeleted(ctx context.Context, artifactID int64) error {
_, err := db.GetEngine(ctx).ID(artifactID).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusDeleted})
return err
return UpdateArtifact(ctx, &ActionArtifact{ID: artifactID, Status: ArtifactStatusDeleted}, "status")
}
+27
View File
@@ -0,0 +1,27 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestKeepLatestAttemptArtifacts(t *testing.T) {
arts := []*ActionArtifact{
{ID: 1, RunAttemptID: 1, ArtifactName: "inherited"},
{ID: 2, RunAttemptID: 1, ArtifactName: "shadowed", ArtifactPath: "a.txt"},
{ID: 3, RunAttemptID: 1, ArtifactName: "shadowed", ArtifactPath: "b.txt"},
{ID: 4, RunAttemptID: 2, ArtifactName: "shadowed", ArtifactPath: "c.txt"},
{ID: 5, RunAttemptID: 2, ArtifactName: "own"},
}
// the whole "shadowed" group of attempt 1 is dropped, its multi-file rows must not mix with attempt 2
var ids []int64
for _, art := range keepLatestAttemptArtifacts(arts) {
ids = append(ids, art.ID)
}
assert.Equal(t, []int64{1, 4, 5}, ids)
}
+51
View File
@@ -11,6 +11,7 @@ import (
"gitea.dev/models/db"
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"gitea.dev/modules/log"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
@@ -96,6 +97,56 @@ func GetRunAttemptByRunIDAndAttemptNum(ctx context.Context, runID, attemptNum in
return &attempt, nil
}
// GetArtifactAttemptIDs returns the IDs of the attempts whose artifacts the job may read, newest first,
// always including the job's own attempt.
// An attempt that re-ran only some of the run's jobs keeps the artifacts of the attempt it re-ran from,
// because the jobs it passed through never upload them again; a rerun of the whole run starts over.
func GetArtifactAttemptIDs(ctx context.Context, job *ActionRunJob) ([]int64, error) {
if job.Attempt <= 1 || job.RunAttemptID == 0 {
return []int64{job.RunAttemptID}, nil
}
attempts, err := ListRunAttemptsByRunID(ctx, job.RunID)
if err != nil {
return nil, err
}
// a newer attempt is never readable, and attempt 1 has nothing older to continue into
candidateIDs := container.FilterSlice(attempts, func(a *ActionRunAttempt) (int64, bool) {
return a.ID, a.Attempt > 1 && a.Attempt <= job.Attempt
})
passThroughAttemptIDs, err := findPassThroughAttemptIDs(ctx, candidateIDs)
if err != nil {
return nil, err
}
ids := make([]int64, 0, len(attempts))
for _, attempt := range attempts {
if attempt.Attempt > job.Attempt {
continue
}
ids = append(ids, attempt.ID)
if !slices.Contains(passThroughAttemptIDs, attempt.ID) {
// stops at the first attempt that passed no job through
break
}
}
return ids, nil
}
// findPassThroughAttemptIDs narrows the given attempts to those that were a rerun of selected jobs:
// only such a rerun clones jobs carrying a source task.
// TODO: best-effort. Needs a better way to distinguish between "partial re-run" and "full re-run".
func findPassThroughAttemptIDs(ctx context.Context, attemptIDs []int64) ([]int64, error) {
passThroughAttemptIDs := make([]int64, 0, len(attemptIDs))
return passThroughAttemptIDs, db.GetEngine(ctx).
Table("action_run_job").
Cols("run_attempt_id").
In("run_attempt_id", attemptIDs).
Where("source_task_id <> 0").
Distinct("run_attempt_id").
Find(&passThroughAttemptIDs)
}
// FindConcurrentRunAttempts returns attempts in the given concurrency group and status set.
// Results are unordered; callers must not depend on any particular row order.
func FindConcurrentRunAttempts(ctx context.Context, repoID int64, concurrencyGroup string, statuses []Status) ([]*ActionRunAttempt, error) {
+3 -2
View File
@@ -13,6 +13,7 @@ import (
"gitea.dev/modules/container"
"gitea.dev/modules/optional"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/builder"
)
@@ -155,10 +156,10 @@ func (opts FindRunJobOptions) ToJoins() []db.JoinFunc {
}
func (opts FindRunJobOptions) ToOrders() string {
return string(opts.OrderBy)
return util.IfZero(string(opts.OrderBy), "action_run_job.id")
}
var _ db.FindOptionsOrder = FindRunJobOptions{}
var _ db.FindOptions = (*FindRunJobOptions)(nil)
// CountRunJobsByRunAndAttemptID counts the jobs belonging to the given run attempt.
// It is used to enforce MaxJobNumPerRun when reusable-workflow expansion inserts new jobs.
+12
View File
@@ -11,6 +11,7 @@ import (
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"gitea.dev/modules/optional"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/translation"
webhook_module "gitea.dev/modules/webhook"
@@ -203,3 +204,14 @@ func GetActors(ctx context.Context, repoID int64) ([]*user_model.User, error) {
OrderBy(user_model.GetOrderByName()).
Find(&actors)
}
// FindOldestRuns returns up to limit runs in the given statuses created before olderThan, lowest id first.
func FindOldestRuns(ctx context.Context, statuses []Status, olderThan timeutil.TimeStamp, limit int) ([]*ActionRun, error) {
runs := make([]*ActionRun, 0, limit)
return runs, db.GetEngine(ctx).
Where(builder.In("`action_run`.status", statuses)).
And(builder.Lt{"`action_run`.created": olderThan}).
OrderBy("`action_run`.`id` ASC").
Limit(limit).
Find(&runs)
}
+2 -2
View File
@@ -30,8 +30,8 @@ type ActionSchedule struct {
Ref string
CommitSHA string
Event webhook_module.HookEventType
EventPayload string `xorm:"LONGTEXT"`
Content []byte
EventPayload string `xorm:"LONGTEXT"`
Content []byte `xorm:"LONGBLOB"`
Created timeutil.TimeStamp `xorm:"created"`
Updated timeutil.TimeStamp `xorm:"updated"`
}
+4
View File
@@ -53,6 +53,10 @@ type FindScopedWorkflowSourceOpts struct {
SourceRepoID int64
}
func (opts FindScopedWorkflowSourceOpts) ToOrders() string {
return "id"
}
func (opts FindScopedWorkflowSourceOpts) ToConds() builder.Cond {
cond := builder.NewCond()
if len(opts.OwnerIDs) > 0 {
+8 -40
View File
@@ -5,7 +5,6 @@ package actions
import (
"context"
"crypto/subtle"
"errors"
"fmt"
"strings"
@@ -22,7 +21,6 @@ import (
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
lru "github.com/hashicorp/golang-lru/v2"
"google.golang.org/protobuf/types/known/timestamppb"
"xorm.io/builder"
)
@@ -66,21 +64,8 @@ type ActionTask struct {
// it only decides whether the runner is reachable, not whether the task should be killed.
const taskReportTimeout = time.Minute
var successfulTokenTaskCache *lru.Cache[string, any]
func init() {
db.RegisterModel(new(ActionTask), func() error {
if setting.SuccessfulTokensCacheSize > 0 {
var err error
successfulTokenTaskCache, err = lru.New[string, any](setting.SuccessfulTokensCacheSize)
if err != nil {
return fmt.Errorf("unable to allocate Task cache: %v", err)
}
} else {
successfulTokenTaskCache = nil
}
return nil
})
db.RegisterModel(new(ActionTask))
}
func (task *ActionTask) Duration() time.Duration {
@@ -195,21 +180,21 @@ func GetRunningTaskByToken(ctx context.Context, token string) (*ActionTask, erro
}
}
cacheKey := "actions:" + token
lastEight := token[len(token)-8:]
if id := getTaskIDFromCache(token); id > 0 {
if cached, _ := auth_model.TokenCache().Get(cacheKey); cached != nil {
task := &ActionTask{
TokenLastEight: lastEight,
}
// Re-get the task from the db in case it has been deleted in the intervening period
has, err := db.GetEngine(ctx).ID(id).Get(task)
has, err := db.GetEngine(ctx).ID(cached.TokenID).Get(task)
if err != nil {
return nil, err
}
if has {
if has && util.CryptoConstTimeEqual(task.TokenHash, cached.TokenHash) {
return task, nil
}
successfulTokenTaskCache.Remove(token)
auth_model.TokenCache().Remove(cacheKey)
}
var tasks []*ActionTask
@@ -223,10 +208,8 @@ func GetRunningTaskByToken(ctx context.Context, token string) (*ActionTask, erro
for _, t := range tasks {
tempHash := auth_model.HashToken(token, t.TokenSalt)
if subtle.ConstantTimeCompare([]byte(t.TokenHash), []byte(tempHash)) == 1 {
if successfulTokenTaskCache != nil {
successfulTokenTaskCache.Add(token, t.ID)
}
if util.CryptoConstTimeEqual(t.TokenHash, tempHash) {
auth_model.TokenCache().Add(cacheKey, &auth_model.TokenCacheItem{TokenID: t.ID, TokenHash: t.TokenHash})
return t, nil
}
}
@@ -671,18 +654,3 @@ func logFileName(repoFullName string, taskID int64) string {
return ret
}
func getTaskIDFromCache(token string) int64 {
if successfulTokenTaskCache == nil {
return 0
}
tInterface, ok := successfulTokenTaskCache.Get(token)
if !ok {
return 0
}
t, ok := tInterface.(int64)
if !ok {
return 0
}
return t
}
+4
View File
@@ -79,6 +79,10 @@ type FindVariablesOpts struct {
Name string
}
func (opts FindVariablesOpts) ToOrders() string {
return "name"
}
func (opts FindVariablesOpts) ToConds() builder.Cond {
cond := builder.NewCond()
+1 -1
View File
@@ -54,7 +54,7 @@ func TestCreateOrUpdateIssueNotificationsIgnored(t *testing.T) {
// user 4 watches repo 1 and would be notified about issue 1
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
assert.NoError(t, repo_model.WatchIgnoreRepo(t.Context(), user, repo))
assert.NoError(t, repo_model.WatchRepoWithOptions(t.Context(), user, repo, repo_model.WatchOptions{Mode: repo_model.WatchModeDont}))
notified, err := activities_model.CreateOrUpdateIssueNotifications(t.Context(), 1, 0, 2, 0)
assert.NoError(t, err)
+2 -2
View File
@@ -367,7 +367,7 @@ func (stats *ActivityStats) FillReleases(ctx context.Context, repoID int64, from
// Published releases list
sess := releasesForActivityStatement(ctx, repoID, fromTime)
sess.OrderBy("`release`.created_unix DESC")
sess.OrderBy("`release`.published_unix DESC")
stats.PublishedReleases = make([]*repo_model.Release, 0)
if err = sess.Find(&stats.PublishedReleases); err != nil {
return err
@@ -386,5 +386,5 @@ func (stats *ActivityStats) FillReleases(ctx context.Context, repoID int64, from
func releasesForActivityStatement(ctx context.Context, repoID int64, fromTime time.Time) db.Session {
return db.GetEngine(ctx).Where("`release`.repo_id = ?", repoID).
And("`release`.is_draft = ?", false).
And("`release`.created_unix >= ?", fromTime.Unix())
And("`release`.published_unix >= ?", fromTime.Unix())
}
+4
View File
@@ -75,6 +75,10 @@ type FindGPGKeyOptions struct {
IncludeSubKeys bool
}
func (opts FindGPGKeyOptions) ToOrders() string {
return "id"
}
func (opts FindGPGKeyOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if !opts.IncludeSubKeys {
+7 -3
View File
@@ -38,7 +38,7 @@ const (
// PublicKey represents a user or deploy SSH public key.
type PublicKey struct {
ID int64 `xorm:"pk autoincr"`
OwnerID int64 `xorm:"INDEX NOT NULL"`
OwnerID int64 `xorm:"INDEX NOT NULL"` // deploy-key doesn't have owner
Name string `xorm:"NOT NULL"`
Fingerprint string `xorm:"INDEX NOT NULL"`
Content string `xorm:"MEDIUMTEXT NOT NULL"`
@@ -73,7 +73,7 @@ func (key *PublicKey) OmitEmail() string {
return strings.Join(fields[:2], " ")
}
func addKey(ctx context.Context, key *PublicKey) (err error) {
func addPublicKey(ctx context.Context, key *PublicKey) (err error) {
if len(key.Fingerprint) == 0 {
key.Fingerprint, err = CalcFingerprint(key.Content)
if err != nil {
@@ -123,7 +123,7 @@ func AddPublicKey(ctx context.Context, ownerID int64, name, content string, auth
LoginSourceID: authSourceID,
Verified: verified,
}
if err = addKey(ctx, key); err != nil {
if err = addPublicKey(ctx, key); err != nil {
return nil, fmt.Errorf("addKey: %w", err)
}
@@ -184,6 +184,10 @@ type FindPublicKeyOptions struct {
LoginSourceID int64
}
func (opts FindPublicKeyOptions) ToOrders() string {
return "id"
}
func (opts FindPublicKeyOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.OwnerID > 0 {
+46 -77
View File
@@ -11,19 +11,11 @@ import (
"gitea.dev/models/db"
"gitea.dev/models/perm"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// ________ .__ ____ __.
// \______ \ ____ ______ | | ____ ___.__.| |/ _|____ ___.__.
// | | \_/ __ \\____ \| | / _ < | || <_/ __ < | |
// | ` \ ___/| |_> > |_( <_> )___ || | \ ___/\___ |
// /_______ /\___ > __/|____/\____// ____||____|__ \___ > ____|
// \/ \/|__| \/ \/ \/\/
//
// This file contains functions specific to DeployKeys
// DeployKey represents deploy key information and its relation with repository.
type DeployKey struct {
ID int64 `xorm:"pk autoincr"`
@@ -31,30 +23,29 @@ type DeployKey struct {
RepoID int64 `xorm:"UNIQUE(s) INDEX"`
Name string
Fingerprint string
Content string `xorm:"-"`
Mode perm.AccessMode `xorm:"NOT NULL DEFAULT 1"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
HasRecentActivity bool `xorm:"-"`
HasUsed bool `xorm:"-"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
PublicKey *PublicKey `xorm:"-"`
}
// AfterLoad is invoked from XORM after setting the values of all fields of this object.
func (key *DeployKey) AfterLoad() {
key.HasUsed = key.UpdatedUnix > key.CreatedUnix
key.HasRecentActivity = key.UpdatedUnix.AddDuration(7*24*time.Hour) > timeutil.TimeStampNow()
func (key *DeployKey) HasUsed() bool {
return key.UpdatedUnix > key.CreatedUnix
}
// GetContent gets associated public key content.
func (key *DeployKey) GetContent(ctx context.Context) error {
pkey, err := GetPublicKeyByID(ctx, key.KeyID)
if err != nil {
return err
func (key *DeployKey) HasRecentActivity() bool {
return key.UpdatedUnix.AddDuration(7*24*time.Hour) > timeutil.TimeStampNow()
}
func (key *DeployKey) LoadPublicKey(ctx context.Context) (err error) {
if key.PublicKey != nil {
return nil
}
key.Content = pkey.Content
return nil
key.PublicKey, err = GetPublicKeyByID(ctx, key.KeyID)
return err
}
// IsReadOnly checks if the key can only be used for read operations, used by template
@@ -66,57 +57,39 @@ func init() {
db.RegisterModel(new(DeployKey))
}
func checkDeployKey(ctx context.Context, keyID, repoID int64, name string) error {
func checkDeployKey(ctx context.Context, repoID, publicKeyID int64, name string) error {
// Note: We want error detail, not just true or false here.
has, err := db.GetEngine(ctx).
Where("key_id = ? AND repo_id = ?", keyID, repoID).
Where("repo_id=? AND (key_id=? OR name=?)", repoID, publicKeyID, name).
Get(new(DeployKey))
if err != nil {
return err
} else if has {
return ErrDeployKeyAlreadyExist{keyID, repoID}
return ErrDeployKeyAlreadyExist{publicKeyID, repoID}
}
has, err = db.GetEngine(ctx).
Where("repo_id = ? AND name = ?", repoID, name).
Get(new(DeployKey))
if err != nil {
return err
} else if has {
return ErrDeployKeyNameAlreadyUsed{repoID, name}
}
return nil
}
// addDeployKey adds new key-repo relation.
func addDeployKey(ctx context.Context, keyID, repoID int64, name, fingerprint string, mode perm.AccessMode) (*DeployKey, error) {
if err := checkDeployKey(ctx, keyID, repoID, name); err != nil {
func addDeployKey(ctx context.Context, repoID, publicKeyID int64, name, fingerprint string, mode perm.AccessMode) (*DeployKey, error) {
if err := checkDeployKey(ctx, repoID, publicKeyID, name); err != nil {
return nil, err
}
key := &DeployKey{
KeyID: keyID,
RepoID: repoID,
Name: name,
Fingerprint: fingerprint,
Mode: mode,
}
key := &DeployKey{KeyID: publicKeyID, RepoID: repoID, Name: name, Fingerprint: fingerprint, Mode: mode}
return key, db.Insert(ctx, key)
}
// AddDeployKey add new deploy key to database and authorized_keys file.
func AddDeployKey(ctx context.Context, repoID int64, name, content string, readOnly bool) (*DeployKey, error) {
func AddDeployKey(ctx context.Context, repoID int64, name, content string, accessMode perm.AccessMode) (*DeployKey, error) {
fingerprint, err := CalcFingerprint(content)
if err != nil {
return nil, err
}
accessMode := perm.AccessModeRead
if !readOnly {
accessMode = perm.AccessModeWrite
if accessMode != perm.AccessModeRead && accessMode != perm.AccessModeWrite {
return nil, util.NewInvalidArgumentErrorf("invalid access mode")
}
return db.WithTx2(ctx, func(ctx context.Context) (*DeployKey, error) {
pkey, exist, err := db.Get[PublicKey](ctx, builder.Eq{"fingerprint": fingerprint})
if err != nil {
@@ -126,52 +99,46 @@ func AddDeployKey(ctx context.Context, repoID int64, name, content string, readO
return nil, ErrKeyAlreadyExist{0, fingerprint, ""}
}
} else {
// First time use this deploy key.
// First time use this deploy key, add a shared public key
pkey = &PublicKey{
Fingerprint: fingerprint,
Mode: accessMode,
Mode: perm.AccessModeNone,
Type: KeyTypeDeploy,
Name: "(DeployKey)",
Content: content,
Name: name,
Fingerprint: fingerprint,
}
if err = addKey(ctx, pkey); err != nil {
return nil, fmt.Errorf("addKey: %w", err)
if err = addPublicKey(ctx, pkey); err != nil {
return nil, fmt.Errorf("addPublicKey: %w", err)
}
}
key, err := addDeployKey(ctx, pkey.ID, repoID, name, pkey.Fingerprint, accessMode)
if err != nil {
return nil, err
}
return key, nil
return addDeployKey(ctx, repoID, pkey.ID, name, fingerprint, accessMode)
})
}
// GetDeployKeyByID returns deploy key by given ID.
func GetDeployKeyByID(ctx context.Context, id int64) (*DeployKey, error) {
key, exist, err := db.GetByID[DeployKey](ctx, id)
func GetDeployKeyByID(ctx context.Context, repoID, deployKeyID int64) (*DeployKey, error) {
key, exist, err := db.Get[DeployKey](ctx, builder.Eq{"id": deployKeyID, "repo_id": repoID})
if err != nil {
return nil, err
} else if !exist {
return nil, ErrDeployKeyNotExist{id, 0, 0}
return nil, ErrDeployKeyNotExist{deployKeyID, 0, repoID}
}
return key, nil
}
// GetDeployKeyByRepo returns deploy key by given public key ID and repository ID.
func GetDeployKeyByRepo(ctx context.Context, keyID, repoID int64) (*DeployKey, error) {
key, exist, err := db.Get[DeployKey](ctx, builder.Eq{"key_id": keyID, "repo_id": repoID})
// GetDeployKeyByRepoPublicKey returns deploy key by given public key ID and repository ID.
func GetDeployKeyByRepoPublicKey(ctx context.Context, repoID, publicKeyID int64) (*DeployKey, error) {
key, exist, err := db.Get[DeployKey](ctx, builder.Eq{"key_id": publicKeyID, "repo_id": repoID})
if err != nil {
return nil, err
} else if !exist {
return nil, ErrDeployKeyNotExist{0, keyID, repoID}
return nil, ErrDeployKeyNotExist{0, publicKeyID, repoID}
}
return key, nil
}
// IsDeployKeyExistByKeyID return true if there is at least one deploykey with the key id
func IsDeployKeyExistByKeyID(ctx context.Context, keyID int64) (bool, error) {
// IsDeployKeyExistByPublicKeyID return true if there is at least one deploy-key with the key id
func IsDeployKeyExistByPublicKeyID(ctx context.Context, keyID int64) (bool, error) {
return db.GetEngine(ctx).
Where("key_id = ?", keyID).
Get(new(DeployKey))
@@ -191,11 +158,13 @@ type ListDeployKeysOptions struct {
Fingerprint string
}
func (opt ListDeployKeysOptions) ToOrders() string {
return "name"
}
func (opt ListDeployKeysOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opt.RepoID != 0 {
cond = cond.And(builder.Eq{"repo_id": opt.RepoID})
}
cond = cond.And(builder.Eq{"repo_id": opt.RepoID}) // repo ID must be used
if opt.KeyID != 0 {
cond = cond.And(builder.Eq{"key_id": opt.KeyID})
}
+35 -47
View File
@@ -6,22 +6,16 @@ package auth
import (
"context"
"crypto/subtle"
"encoding/hex"
"fmt"
"time"
"gitea.dev/models/db"
"gitea.dev/modules/setting"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
lru "github.com/hashicorp/golang-lru/v2"
"xorm.io/builder"
)
var successfulAccessTokenCache *lru.Cache[string, any]
// AccessToken represents a personal access token.
type AccessToken struct {
ID int64 `xorm:"pk autoincr"`
@@ -46,32 +40,43 @@ func (t *AccessToken) AfterLoad() {
}
func init() {
db.RegisterModel(new(AccessToken), func() error {
if setting.SuccessfulTokensCacheSize > 0 {
var err error
successfulAccessTokenCache, err = lru.New[string, any](setting.SuccessfulTokensCacheSize)
if err != nil {
return fmt.Errorf("unable to allocate AccessToken cache: %w", err)
}
} else {
successfulAccessTokenCache = nil
}
return nil
})
db.RegisterModel(new(AccessToken))
}
// NewAccessToken creates new access token.
func NewAccessToken(ctx context.Context, t *AccessToken) error {
// setNewTokenValue generates a fresh random token value and fills in its salt, hash, and last-eight.
func (t *AccessToken) setNewTokenValue() {
salt := util.CryptoRandomString(10)
token := util.CryptoRandomBytes(20)
t.TokenSalt = salt
t.Token = hex.EncodeToString(token)
t.TokenHash = HashToken(t.Token, t.TokenSalt)
t.TokenLastEight = t.Token[len(t.Token)-8:]
}
// NewAccessToken creates new access token.
func NewAccessToken(ctx context.Context, t *AccessToken) error {
t.setNewTokenValue()
_, err := db.GetEngine(ctx).Insert(t)
return err
}
// RegenerateAccessToken regenerates the token value of an existing access token owned by userID, keeping its name and scope.
func RegenerateAccessToken(ctx context.Context, id, userID int64) (*AccessToken, error) {
t := &AccessToken{}
has, err := db.GetEngine(ctx).Where("id=? AND uid=?", id, userID).Get(t)
if err != nil {
return nil, err
} else if !has {
return nil, util.NewNotExistErrorf("access token not found")
}
t.setNewTokenValue()
if _, err := db.GetEngine(ctx).ID(t.ID).Cols("token_hash", "token_salt", "token_last_eight").NoAutoTime().Update(t); err != nil {
return nil, err
}
return t, nil
}
// DisplayPublicOnly whether to display this as a public-only token.
func (t *AccessToken) DisplayPublicOnly() bool {
publicOnly, err := t.Scope.PublicOnly()
@@ -81,41 +86,26 @@ func (t *AccessToken) DisplayPublicOnly() bool {
return publicOnly
}
func getAccessTokenIDFromCache(token string) int64 {
if successfulAccessTokenCache == nil {
return 0
}
tInterface, ok := successfulAccessTokenCache.Get(token)
if !ok {
return 0
}
t, ok := tInterface.(int64)
if !ok {
return 0
}
return t
}
// GetAccessTokenBySHA returns access token by given token value
func GetAccessTokenBySHA(ctx context.Context, token string) (*AccessToken, error) {
if len(token) < 8 {
return nil, util.NewNotExistErrorf("access token not found")
}
cacheKey := "access:" + token
lastEight := token[len(token)-8:]
if id := getAccessTokenIDFromCache(token); id > 0 {
accessToken := &AccessToken{
TokenLastEight: lastEight,
}
// Re-get the token from the db in case it has been deleted in the intervening period
has, err := db.GetEngine(ctx).ID(id).Get(accessToken)
if cached, _ := TokenCache().Get(cacheKey); cached != nil {
// Re-get the token from the db in case it has been deleted or regenerated in the intervening period
accessToken := &AccessToken{}
has, err := db.GetEngine(ctx).ID(cached.TokenID).Get(accessToken)
if err != nil {
return nil, err
}
if has {
if has && util.CryptoConstTimeEqual(accessToken.TokenHash, cached.TokenHash) {
return accessToken, nil
}
successfulAccessTokenCache.Remove(token)
// either the token has been deleted or changed, invalidate the cache
TokenCache().Remove(cacheKey)
}
var tokens []AccessToken
@@ -128,10 +118,8 @@ func GetAccessTokenBySHA(ctx context.Context, token string) (*AccessToken, error
for _, t := range tokens {
tempHash := HashToken(token, t.TokenSalt)
if subtle.ConstantTimeCompare([]byte(t.TokenHash), []byte(tempHash)) == 1 {
if successfulAccessTokenCache != nil {
successfulAccessTokenCache.Add(token, t.ID)
}
if util.CryptoConstTimeEqual(t.TokenHash, tempHash) {
TokenCache().Add(cacheKey, &TokenCacheItem{TokenID: t.ID, TokenHash: t.TokenHash})
return &t, nil
}
}
+40
View File
@@ -117,6 +117,46 @@ func TestUpdateAccessToken(t *testing.T) {
unittest.AssertExistsAndLoadBean(t, token)
}
func TestRegenerateAccessToken(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
const oldToken = "d2c6c1ba3890b309189a8e618c72a162e4efbf36"
// prime the successful-lookup cache with the old token value, as a real request would
before, err := auth_model.GetAccessTokenBySHA(t.Context(), oldToken)
assert.NoError(t, err)
assert.Equal(t, "Token A", before.Name)
regenerated, err := auth_model.RegenerateAccessToken(t.Context(), before.ID, before.UID)
assert.NoError(t, err)
assert.Equal(t, before.ID, regenerated.ID)
assert.Equal(t, before.Name, regenerated.Name)
assert.Equal(t, before.Scope, regenerated.Scope)
assert.NotEqual(t, before.TokenHash, regenerated.TokenHash)
assert.NotEmpty(t, regenerated.Token)
// the old token value must stop authenticating, even though it was cached as successful above
_, err = auth_model.GetAccessTokenBySHA(t.Context(), oldToken)
assert.Error(t, err)
assert.ErrorIs(t, err, util.ErrNotExist)
// the new token value must authenticate
found, err := auth_model.GetAccessTokenBySHA(t.Context(), regenerated.Token)
assert.NoError(t, err)
assert.Equal(t, before.ID, found.ID)
assert.Equal(t, before.UpdatedUnix, found.UpdatedUnix)
// wrong owner
_, err = auth_model.RegenerateAccessToken(t.Context(), before.ID, before.UID+1)
assert.Error(t, err)
assert.ErrorIs(t, err, util.ErrNotExist)
// nonexistent token
_, err = auth_model.RegenerateAccessToken(t.Context(), 100, 100)
assert.Error(t, err)
assert.ErrorIs(t, err, util.ErrNotExist)
}
func TestDeleteAccessTokenByID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
+6 -1
View File
@@ -14,6 +14,7 @@ import (
"slices"
"strings"
"time"
"uuid"
"gitea.dev/models/db"
"gitea.dev/modules/container"
@@ -21,7 +22,6 @@ import (
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
uuid "github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"golang.org/x/oauth2"
"xorm.io/builder"
@@ -83,6 +83,11 @@ func BuiltinApplications() map[string]*BuiltinOAuth2Application {
DisplayName: "tea",
RedirectURIs: []string{"http://127.0.0.1", "https://127.0.0.1"},
}
m["b757811a-05c8-4c76-8d74-a5ee3d2073f2"] = &BuiltinOAuth2Application{
ConfigName: "gitea-app",
DisplayName: "Gitea App",
RedirectURIs: []string{"com.gitea.app://oauth/callback"},
}
return m
}
+4
View File
@@ -259,6 +259,10 @@ type FindSourcesOptions struct {
LoginType Type
}
func (opts FindSourcesOptions) ToOrders() string {
return "name"
}
func (opts FindSourcesOptions) ToConds() builder.Cond {
conds := builder.NewCond()
if opts.IsActive.Has() {
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth
import (
"sync"
"gitea.dev/modules/setting"
lru "github.com/hashicorp/golang-lru/v2"
)
type TokenCacheItem struct {
TokenID int64
TokenHash string
}
var TokenCache = sync.OnceValue(func() *lru.Cache[string, *TokenCacheItem] {
cacheSize := max(setting.SuccessfulTokensCacheSize, 20)
c, _ := lru.New[string, *TokenCacheItem](cacheSize) // it only fails when size <= 0
return c
})
+58 -25
View File
@@ -5,39 +5,72 @@ package db
import (
"context"
"fmt"
"gitea.dev/modules/setting"
"xorm.io/builder"
"xorm.io/xorm/schemas"
)
// Iterate iterates all the Bean object
func Iterate[Bean any](ctx context.Context, cond builder.Cond, f func(ctx context.Context, bean *Bean) error) error {
var start int
batchSize := setting.Database.IterateBufferSize
sess := GetEngine(ctx)
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
beans := make([]*Bean, 0, batchSize)
if cond != nil {
sess = sess.Where(cond)
}
if err := sess.Limit(batchSize, start).Find(&beans); err != nil {
return err
}
if len(beans) == 0 {
return nil
}
start += len(beans)
func iterateTableByColumn[Bean any](ctx context.Context, colName string, cond builder.Cond, f func(ctx context.Context, bean *Bean) error) error {
table, err := xormEngine.TableInfo(new(Bean))
if err != nil {
return err
}
for _, bean := range beans {
if err := f(ctx, bean); err != nil {
return err
}
var col *schemas.Column
if colName == "" {
if len(table.PrimaryKeys) != 1 {
return fmt.Errorf("table %s has %d primary keys, only the table with exactly one primary key can be iterated", table.Name, len(table.PrimaryKeys))
}
colName = table.PrimaryKeys[0]
}
col = table.GetColumn(colName)
batchSize := setting.Database.IterateBufferSize
var lastColValue any
for {
if ctx.Err() != nil {
return ctx.Err()
}
beans := make([]*Bean, 0, batchSize)
query := GetEngine(ctx).Table(table.Name).Asc(colName)
batchCond := cond
if lastColValue != nil {
batchCond = builder.And(cond, builder.Gt{col.Name: lastColValue})
}
if batchCond != nil {
query = query.Where(batchCond)
}
if err := query.Limit(batchSize).Find(&beans); err != nil {
return err
}
if len(beans) == 0 {
return nil
}
reflectVal, err := col.ValueOf(beans[len(beans)-1])
if err != nil {
return err
}
lastColValue = reflectVal.Interface()
for _, bean := range beans {
if err := f(ctx, bean); err != nil {
return err
}
}
}
}
func IterateByColumn[Bean any](ctx context.Context, colName string, cond builder.Cond, f func(ctx context.Context, bean *Bean) error) error {
return iterateTableByColumn(ctx, colName, cond, f)
}
func Iterate[Bean any](ctx context.Context, cond builder.Cond, f func(ctx context.Context, bean *Bean) error) error {
return iterateTableByColumn(ctx, "", cond, f)
}
+12 -25
View File
@@ -38,10 +38,7 @@ type ListOptions struct {
var ListOptionsAll = ListOptions{ListAll: true}
var (
_ Paginator = &ListOptions{}
_ FindOptions = ListOptions{}
)
var _ Paginator = &ListOptions{}
// GetSkipTake returns the skip and take values
func (opts *ListOptions) GetSkipTake() (skip, take int) {
@@ -117,6 +114,7 @@ type FindOptions interface {
GetPageSize() int
IsListAll() bool
ToConds() builder.Cond
ToOrders() string
}
type JoinFunc func(sess Engine) error
@@ -125,10 +123,6 @@ type FindOptionsJoin interface {
ToJoins() []JoinFunc
}
type FindOptionsOrder interface {
ToOrders() string
}
// Find represents a common find function which accept an options interface
func Find[T any](ctx context.Context, opts FindOptions) ([]*T, error) {
sess := GetEngine(ctx).Where(opts.ToConds())
@@ -140,12 +134,7 @@ func Find[T any](ctx context.Context, opts FindOptions) ([]*T, error) {
}
}
}
if orderOpt, ok := opts.(FindOptionsOrder); ok {
if order := orderOpt.ToOrders(); order != "" {
sess.OrderBy(order)
}
}
sess.OrderBy(opts.ToOrders())
page, pageSize := opts.GetPage(), opts.GetPageSize()
if !opts.IsListAll() && pageSize > 0 {
if page == 0 {
@@ -167,15 +156,17 @@ func Find[T any](ctx context.Context, opts FindOptions) ([]*T, error) {
// Count represents a common count function which accept an options interface
func Count[T any](ctx context.Context, opts FindOptions) (int64, error) {
sess := GetEngine(ctx).Where(opts.ToConds())
if joinOpt, ok := opts.(FindOptionsJoin); ok {
for _, joinFunc := range joinOpt.ToJoins() {
if err := joinFunc(sess); err != nil {
return 0, err
sess := GetEngine(ctx)
if opts != nil {
sess.Where(opts.ToConds())
if joinOpt, ok := opts.(FindOptionsJoin); ok {
for _, joinFunc := range joinOpt.ToJoins() {
if err := joinFunc(sess); err != nil {
return 0, err
}
}
}
}
var object T
return sess.Count(&object)
}
@@ -194,11 +185,7 @@ func FindAndCount[T any](ctx context.Context, opts FindOptions) ([]*T, int64, er
}
}
}
if orderOpt, ok := opts.(FindOptionsOrder); ok {
if order := orderOpt.ToOrders(); order != "" {
sess.OrderBy(order)
}
}
sess.OrderBy(opts.ToOrders())
findPageSize := defaultFindSliceSize
if pageSize > 0 {
+4
View File
@@ -18,6 +18,10 @@ type mockListOptions struct {
db.ListOptions
}
func (opts mockListOptions) ToOrders() string {
return "id"
}
func (opts mockListOptions) IsListAll() bool {
return true
}
+10
View File
@@ -11,6 +11,7 @@
is_prerelease: false
is_tag: false
created_unix: 946684800
published_unix: 946684800
- id: 2
repo_id: 40
@@ -25,6 +26,7 @@
is_prerelease: false
is_tag: false
created_unix: 946684800
published_unix: 946684800
- id: 3
repo_id: 1
@@ -39,6 +41,7 @@
is_prerelease: false
is_tag: true
created_unix: 946684800
published_unix: 946684800
- id: 4
repo_id: 1
@@ -66,6 +69,7 @@
is_prerelease: true
is_tag: false
created_unix: 946684800
published_unix: 946684800
- id: 6
repo_id: 57
@@ -80,6 +84,7 @@
is_prerelease: false
is_tag: false
created_unix: 946684801
published_unix: 946684801
- id: 7
repo_id: 57
@@ -94,6 +99,7 @@
is_prerelease: false
is_tag: false
created_unix: 946684802
published_unix: 946684802
- id: 8
repo_id: 57
@@ -108,6 +114,7 @@
is_prerelease: false
is_tag: false
created_unix: 946684803
published_unix: 946684803
- id: 9
repo_id: 57
@@ -122,6 +129,7 @@
is_prerelease: false
is_tag: false
created_unix: 946684803
published_unix: 946684803
- id: 10
repo_id: 57
@@ -136,6 +144,7 @@
is_prerelease: false
is_tag: false
created_unix: 946684803
published_unix: 946684803
- id: 11
repo_id: 2
@@ -150,5 +159,6 @@
is_prerelease: false
is_tag: false
created_unix: 946684803
published_unix: 946684803
# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly
+37 -13
View File
@@ -15,8 +15,12 @@ import (
asymkey_model "gitea.dev/models/asymkey"
"gitea.dev/models/db"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/cache"
"gitea.dev/modules/cachegroup"
"gitea.dev/modules/commitstatus"
"gitea.dev/modules/git"
"gitea.dev/modules/log"
@@ -213,8 +217,8 @@ func (status *CommitStatus) LocaleString(lang translation.Locale) string {
return lang.TrString("repo.commitstatus." + status.State.String())
}
// HideActionsURL set `TargetURL` to an empty string if the status comes from Gitea Actions
func (status *CommitStatus) HideActionsURL(ctx context.Context) {
// hideActionsURL set `TargetURL` to an empty string if the status comes from Gitea Actions
func (status *CommitStatus) hideActionsURL(ctx context.Context) {
if _, ok := status.cutTargetURLGiteaActionsPrefix(ctx); ok {
status.TargetURL = ""
}
@@ -544,18 +548,38 @@ func HashCommitStatusContext(context string) string {
return fmt.Sprintf("%x", sha1.Sum([]byte(context)))
}
// CommitStatusesHideActionsURL hide Gitea Actions urls
func CommitStatusesHideActionsURL(ctx context.Context, statuses []*CommitStatus) {
idToRepos := make(map[int64]*repo_model.Repository)
// CommitStatusesApplyDoerPermission hides the Gitea Actions url of every status whose repository
// the doer cannot read the Actions unit of, so the "Details" link does not lead to a 404.
func CommitStatusesApplyDoerPermission(ctx context.Context, doer *user_model.User, statuses []*CommitStatus) {
for _, status := range statuses {
if status == nil {
continue
if status != nil && !statusRepoCanReadActions(ctx, doer, status) {
status.hideActionsURL(ctx)
}
if status.Repo == nil {
status.Repo = idToRepos[status.RepoID]
}
status.HideActionsURL(ctx)
idToRepos[status.RepoID] = status.Repo
}
}
// SignCommitsApplyDoerPermission is CommitStatusesApplyDoerPermission for a list of commits.
func SignCommitsApplyDoerPermission(ctx context.Context, doer *user_model.User, commits []*SignCommitWithStatuses) {
var statuses []*CommitStatus
for _, commit := range commits {
statuses = append(statuses, commit.Status)
statuses = append(statuses, commit.Statuses...)
}
CommitStatusesApplyDoerPermission(ctx, doer, statuses)
}
func statusRepoCanReadActions(ctx context.Context, doer *user_model.User, status *CommitStatus) bool {
perm, err := cache.GetWithContextCache(ctx, cachegroup.RepoUserPermission, access_model.RepoUserPermissionCacheKey(status.RepoID, doer),
func(ctx context.Context, _ string) (access_model.Permission, error) { // only runs on a cache miss
if err := status.loadRepository(ctx); err != nil {
return access_model.Permission{}, err
}
return access_model.GetDoerRepoPermission(ctx, status.Repo, doer)
},
)
if err != nil {
log.Error("GetDoerRepoPermission[%d]: %v", status.RepoID, err)
return false
}
return perm.CanRead(unit.TypeActions)
}
+14 -9
View File
@@ -4,11 +4,9 @@
package git_test
import (
"fmt"
"testing"
"time"
actions_model "gitea.dev/models/actions"
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
repo_model "gitea.dev/models/repo"
@@ -233,17 +231,23 @@ func TestFindRepoRecentCommitStatusContexts(t *testing.T) {
}
}
func TestCommitStatusesHideActionsURL(t *testing.T) {
func TestCommitStatusesApplyDoerPermission(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
// repo4 is public and has the actions unit, repo2 is private and owned by someone else
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 791, RepoID: repo.ID})
assert.NoError(t, run.LoadAttributes(t.Context()))
otherRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
visibleURL := repo.Link() + "/actions/runs/1/jobs/1"
statuses := []*git_model.CommitStatus{
{
RepoID: repo.ID,
TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), run.ID),
TargetURL: visibleURL,
},
{
RepoID: otherRepo.ID,
TargetURL: otherRepo.Link() + "/actions/runs/1/jobs/1",
},
{
RepoID: repo.ID,
@@ -251,9 +255,10 @@ func TestCommitStatusesHideActionsURL(t *testing.T) {
},
}
git_model.CommitStatusesHideActionsURL(t.Context(), statuses)
assert.Empty(t, statuses[0].TargetURL)
assert.Equal(t, "https://mycicd.org/1", statuses[1].TargetURL)
git_model.CommitStatusesApplyDoerPermission(t.Context(), doer, statuses)
assert.Equal(t, visibleURL, statuses[0].TargetURL)
assert.Empty(t, statuses[1].TargetURL)
assert.Equal(t, "https://mycicd.org/1", statuses[2].TargetURL)
}
func TestGetCountLatestCommitStatus(t *testing.T) {
+1 -1
View File
@@ -124,7 +124,7 @@ func GetLFSLockByRepoID(ctx context.Context, repoID int64, page, pageSize int) (
e.Limit(pageSize, start)
}
lfsLocks := make(LFSLockList, 0, pageSize)
return lfsLocks, e.Find(&lfsLocks, &LFSLock{RepoID: repoID})
return lfsLocks, e.OrderBy("id").Find(&lfsLocks, &LFSLock{RepoID: repoID})
}
// GetTreePathLock returns LSF lock for the treePath
+4
View File
@@ -74,6 +74,10 @@ type AssignedIssuesOptions struct {
RepoOwnerID int64
}
func (opts *AssignedIssuesOptions) ToOrders() string {
return "id"
}
func (opts *AssignedIssuesOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.AssigneeID != 0 {
+4
View File
@@ -1075,6 +1075,10 @@ type FindCommentsOptions struct {
IsPull optional.Option[bool]
}
func (opts FindCommentsOptions) ToOrders() string {
return "id"
}
// ToConds implements FindOptions interface
func (opts FindCommentsOptions) ToConds() builder.Cond {
cond := builder.NewCond()
+18 -10
View File
@@ -11,6 +11,7 @@ import (
"gitea.dev/models/db"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
@@ -220,7 +221,7 @@ func applyRepoConditions(sess db.Session, opts *IssuesOptions) {
if opts.RepoCond == nil {
opts.RepoCond = builder.NewCond()
}
opts.RepoCond = opts.RepoCond.Or(builder.In("issue.repo_id", builder.Select("id").From("repository").Where(builder.Eq{"is_private": false})))
opts.RepoCond = opts.RepoCond.Or(builder.In("issue.repo_id", builder.Select("id").From("repository").Where(repo_model.PublicRepoUnderPublicOwnerCond())))
}
if opts.RepoCond != nil {
sess.And(opts.RepoCond)
@@ -287,8 +288,8 @@ func applyConditions(sess db.Session, opts *IssuesOptions) {
}
}
// teamUnitsRepoCond returns query condition for those repo id in the special org team with special units access
func teamUnitsRepoCond(id string, userID, orgID, teamID int64, units ...unit.Type) builder.Cond {
// teamUnitsRepoReaderCond returns query condition for those repo id in the special org team with special units access
func teamUnitsRepoReaderCond(id string, userID, orgID, teamID int64, units ...unit.Type) builder.Cond {
return builder.In(id,
builder.Select("repo_id").From("team_repo").Where(
builder.Eq{
@@ -316,12 +317,19 @@ func teamUnitsRepoCond(id string, userID, orgID, teamID int64, units ...unit.Typ
}),
),
)).And(
builder.In(
"team_id", builder.Select("team_id").From("team_unit").Where(
builder.Eq{
"`team_unit`.org_id": orgID,
}.And(
builder.In("`team_unit`.type", units),
builder.Or(
builder.In(
"team_id", builder.Select("id").From("team").Where(
builder.Eq{"id": teamID}.And(builder.Gt{"authorize": perm.AccessModeNone}),
),
),
builder.In(
"team_id", builder.Select("team_id").From("team_unit").Where(
builder.Eq{
"`team_unit`.org_id": orgID,
}.And(
builder.In("`team_unit`.type", units),
),
),
),
),
@@ -338,7 +346,7 @@ func issuePullAccessibleRepoCond(repoIDstr string, userID int64, owner *user_mod
}
if owner != nil && owner.IsOrganization() {
if team != nil {
cond = cond.And(teamUnitsRepoCond(repoIDstr, userID, owner.ID, team.ID, unitType)) // special team member repos
cond = cond.And(teamUnitsRepoReaderCond(repoIDstr, userID, owner.ID, team.ID, unitType)) // special team member repos
} else {
cond = cond.And(
builder.Or(
+1 -10
View File
@@ -626,16 +626,7 @@ func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *u
unittype = unit.TypePullRequests
}
for _, team := range teams {
if team.HasAdminAccess() {
checked = append(checked, team.ID)
resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
continue
}
has, err := db.Exist[organization.TeamUnit](ctx, builder.Eq{"org_id": issue.Repo.Owner.ID, "team_id": team.ID, "`type`": unittype})
if err != nil {
return nil, fmt.Errorf("get team units (%d): %w", team.ID, err)
}
if has {
if team.UnitEnabled(ctx, unittype) {
checked = append(checked, team.ID)
resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
}
+1 -1
View File
@@ -82,7 +82,7 @@ func CheckIssueWatch(ctx context.Context, user *user_model.User, issue *Issue) (
if err != nil {
return false, err
}
if repo_model.IsWatchMode(w.Mode) && util.Iif(issue.IsPull, w.PullRequests, w.Issues) {
if repo_model.IsWatchModeWatching(w.Mode) && util.Iif(issue.IsPull, w.IncludePullRequests, w.IncludeIssues) {
return true, nil
}
return IsUserParticipantsOfIssue(ctx, user, issue), nil
+41 -3
View File
@@ -5,9 +5,11 @@
package issues
import (
"cmp"
"context"
"errors"
"fmt"
"math"
"slices"
"strconv"
"strings"
@@ -184,11 +186,47 @@ func (l *Label) ExclusiveScope() string {
if !l.Exclusive {
return ""
}
lastIndex := strings.LastIndex(l.Name, "/")
if lastIndex == -1 || lastIndex == 0 || lastIndex == len(l.Name)-1 {
scope, name, found := strings.CutLast(l.Name, "/")
if !found || scope == "" || name == "" {
return ""
}
return l.Name[:lastIndex]
return scope
}
// CompareLabelForDisplay compares labels for displaying them in dropdowns or lists.
// Labels are grouped by their exclusive scope, and labels within the same scope
// are sorted by their exclusive order, where unordered labels (order 0) come last.
// Labels without a scope are listed first and everything else falls back to name order.
func CompareLabelForDisplay(a, b *Label) int {
scopeA, scopeB := a.ExclusiveScope(), b.ExclusiveScope()
if scopeA != scopeB {
if scopeA == "" {
return -1
}
if scopeB == "" {
return 1
}
return strings.Compare(scopeA, scopeB)
}
if scopeA != "" {
orderA, orderB := a.ExclusiveOrder, b.ExclusiveOrder
if orderA <= 0 {
orderA = math.MaxInt
}
if orderB <= 0 {
orderB = math.MaxInt
}
if orderA != orderB {
return cmp.Compare(orderA, orderB)
}
}
return strings.Compare(a.Name, b.Name)
}
// SortLabelsForDisplay sorts labels in place for displaying them in dropdowns or lists,
// grouping them by their exclusive scope and respecting the exclusive order within each scope.
func SortLabelsForDisplay(labels []*Label) {
slices.SortStableFunc(labels, CompareLabelForDisplay)
}
// NewLabel creates a new label
+41
View File
@@ -54,6 +54,47 @@ func TestLabel_ExclusiveScope(t *testing.T) {
assert.Equal(t, "scope/subscope", label.ExclusiveScope())
}
func TestSortLabelsForDisplay(t *testing.T) {
labels := []*issues_model.Label{
{Name: "priority/low", Exclusive: true, ExclusiveOrder: 4},
{Name: "priority/critical", Exclusive: true, ExclusiveOrder: 1},
{Name: "priority/medium", Exclusive: true, ExclusiveOrder: 3},
{Name: "priority/high", Exclusive: true, ExclusiveOrder: 2},
{Name: "bug"},
{Name: "enhancement"},
{Name: "kind/question", Exclusive: true},
}
issues_model.SortLabelsForDisplay(labels)
names := make([]string, 0, len(labels))
for _, l := range labels {
names = append(names, l.Name)
}
assert.Equal(t, []string{
"bug",
"enhancement",
"kind/question",
"priority/critical",
"priority/high",
"priority/medium",
"priority/low",
}, names)
// labels without an exclusive order in the same scope are listed last, ordered by name
labels = []*issues_model.Label{
{Name: "scope/unordered-b", Exclusive: true},
{Name: "scope/ordered", Exclusive: true, ExclusiveOrder: 1},
{Name: "scope/unordered-a", Exclusive: true},
}
issues_model.SortLabelsForDisplay(labels)
names = names[:0]
for _, l := range labels {
names = append(names, l.Name)
}
assert.Equal(t, []string{"scope/ordered", "scope/unordered-a", "scope/unordered-b"}, names)
}
func TestNewLabels(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
labels := []*issues_model.Label{
+13 -9
View File
@@ -1065,19 +1065,23 @@ func (r *Review) GetCodeCommentsCount(ctx context.Context) int {
return int(count)
}
// HTMLURL formats a URL-string to the related review issue-comment
// HashTag returns unique hash tag for review.
func (r *Review) HashTag() string {
return fmt.Sprintf("pullrequestreview-%d", r.ID)
}
// HTMLURL formats a URL-string to the review on the pull request page
func (r *Review) HTMLURL(ctx context.Context) string {
opts := FindCommentsOptions{
Type: CommentTypeReview,
IssueID: r.IssueID,
ReviewID: r.ID,
if r.Type != ReviewTypeApprove && r.Type != ReviewTypeComment && r.Type != ReviewTypeReject {
return "" // only submitted reviews get a timeline block carrying the anchor
}
comment := new(Comment)
has, err := db.GetEngine(ctx).Where(opts.ToConds()).Get(comment)
if err != nil || !has {
if err := r.LoadIssue(ctx); err != nil {
return ""
}
return comment.HTMLURL(ctx)
if err := r.Issue.LoadRepo(ctx); err != nil {
return ""
}
return r.Issue.HTMLURL(ctx) + "#" + r.HashTag()
}
// RemapExternalUser ExternalUserRemappable interface
+10
View File
@@ -12,6 +12,7 @@ import (
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
"github.com/stretchr/testify/assert"
)
@@ -28,6 +29,15 @@ func TestGetReviewByID(t *testing.T) {
assert.True(t, issues_model.IsErrReviewNotExist(err), "IsErrReviewNotExist")
}
func TestReview_HTMLURL(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
review := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 1})
assert.Equal(t, setting.AppURL+"user2/repo1/pulls/2#pullrequestreview-1", review.HTMLURL(t.Context()))
pendingReview := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 4})
assert.Empty(t, pendingReview.HTMLURL(t.Context()))
}
func TestReview_LoadAttributes(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
review := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 1})
+14 -25
View File
@@ -91,11 +91,6 @@ func (org *Organization) IsOwnedBy(ctx context.Context, uid int64) (bool, error)
return IsOrganizationOwner(ctx, org.ID, uid)
}
// IsOrgAdmin returns true if given user is in the owner team or an admin team.
func (org *Organization) IsOrgAdmin(ctx context.Context, uid int64) (bool, error) {
return IsOrganizationAdmin(ctx, org.ID, uid)
}
// IsOrgMember returns true if given user is member of organization.
func (org *Organization) IsOrgMember(ctx context.Context, uid int64) (bool, error) {
return IsOrganizationMember(ctx, org.ID, uid)
@@ -140,10 +135,6 @@ func (org *Organization) GetMembers(ctx context.Context, doer *user_model.User)
// HasMemberWithUserID returns true if user with userID is part of the u organisation.
func (org *Organization) HasMemberWithUserID(ctx context.Context, userID int64) bool {
return org.hasMemberWithUserID(ctx, userID)
}
func (org *Organization) hasMemberWithUserID(ctx context.Context, userID int64) bool {
isMember, err := IsOrganizationMember(ctx, org.ID, userID)
if err != nil {
log.Error("IsOrganizationMember: %v", err)
@@ -284,8 +275,8 @@ func (org *Organization) CustomAvatarRelativePath() string {
return org.Avatar
}
// UnitPermission returns unit permission
func (org *Organization) UnitPermission(ctx context.Context, doer *user_model.User, unitType unit.Type) perm.AccessMode {
func (org *Organization) AnyRepoUnitPermission(ctx context.Context, doer *user_model.User, unitType unit.Type) perm.AccessMode {
// FIXME: ORG-TEAM-UNIT-MAX-PERMISSION: this function is not right, team can access repo1's code doesn't mean it can access repo2's code
if doer != nil {
teams, err := GetUserOrgTeams(ctx, org.ID, doer.ID)
if err != nil {
@@ -299,11 +290,11 @@ func (org *Organization) UnitPermission(ctx context.Context, doer *user_model.Us
}
if len(teams) > 0 {
return teams.UnitMaxAccess(unitType)
return teams.AnyRepoUnitMaxAccess(ctx, unitType)
}
}
if org.Visibility.IsPublic() {
if ownerVisibilitySatisfiesDoer(org.AsUser(), doer) {
return perm.AccessModeRead
}
@@ -445,8 +436,7 @@ func GetUsersWhoCanCreateOrgRepo(ctx context.Context, orgID int64) (map[int64]*u
And("team_user.org_id = ?", orgID).Find(&users)
}
// HasOrgOrUserVisible tells if the given user can see the given org or user
func HasOrgOrUserVisible(ctx context.Context, orgOrUser, user *user_model.User) bool {
func ownerVisibilitySatisfiesDoer(orgOrUser, user *user_model.User) bool {
// If user is nil, it's an anonymous user/request.
// The Ghost user is handled like an anonymous user.
if user == nil || user.IsGhost() {
@@ -461,18 +451,17 @@ func HasOrgOrUserVisible(ctx context.Context, orgOrUser, user *user_model.User)
return true
}
if (orgOrUser.Visibility == structs.VisibleTypePrivate || user.IsRestricted) && !OrgFromUser(orgOrUser).hasMemberWithUserID(ctx, user.ID) {
return false
}
return true
return orgOrUser.Visibility != structs.VisibleTypePrivate && !user.IsRestricted
}
// HasOrgOrUserVisible tells if the given user can see the given org or user
func HasOrgOrUserVisible(ctx context.Context, owner, doer *user_model.User) bool {
return ownerVisibilitySatisfiesDoer(owner, doer) ||
(doer != nil && OrgFromUser(owner).HasMemberWithUserID(ctx, doer.ID))
}
// HasOrgsVisible tells if the given user can see at least one of the orgs provided
func HasOrgsVisible(ctx context.Context, orgs []*Organization, user *user_model.User) bool {
if len(orgs) == 0 {
return false
}
for _, org := range orgs {
if HasOrgOrUserVisible(ctx, org.AsUser(), user) {
return true
@@ -596,8 +585,8 @@ func RemoveOrgRepo(ctx context.Context, orgID, repoID int64) error {
// GetUserTeams returns all teams that belong to user,
// and that the user has joined.
func (org *Organization) GetUserTeams(ctx context.Context, userID int64, cols ...string) ([]*Team, error) {
teams := make([]*Team, 0, org.NumTeams)
func (org *Organization) GetUserTeams(ctx context.Context, userID int64, cols ...string) (TeamList, error) {
teams := make(TeamList, 0, org.NumTeams)
return teams, db.GetEngine(ctx).
Where("`team_user`.org_id = ?", org.ID).
Join("INNER", "team_user", "`team_user`.team_id = team.id").
+3
View File
@@ -89,6 +89,9 @@ func DoerViewOtherVisibility(doer, other *user_model.User) structs.VisibleType {
if doer.IsAdmin || doer.ID == other.ID {
return structs.VisibleTypePrivate
}
if doer.IsRestricted {
return structs.VisibleTypePublic
}
return structs.VisibleTypeLimited
}
+9 -3
View File
@@ -77,8 +77,14 @@ func testLoadOrgListTeams(t *testing.T) {
}
func testDoerViewOtherVisibility(t *testing.T) {
viewer := &user_model.User{ID: 1}
other := &user_model.User{ID: 2}
restrictedViewer := &user_model.User{ID: 3, IsRestricted: true}
assert.Equal(t, structs.VisibleTypePublic, organization.DoerViewOtherVisibility(nil, nil))
assert.Equal(t, structs.VisibleTypeLimited, organization.DoerViewOtherVisibility(&user_model.User{ID: 1}, &user_model.User{ID: 2}))
assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(&user_model.User{ID: 1}, &user_model.User{ID: 1}))
assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(&user_model.User{ID: 1, IsAdmin: true}, &user_model.User{ID: 2}))
assert.Equal(t, structs.VisibleTypeLimited, organization.DoerViewOtherVisibility(viewer, other))
assert.Equal(t, structs.VisibleTypePublic, organization.DoerViewOtherVisibility(restrictedViewer, other))
assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(viewer, viewer))
assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(restrictedViewer, restrictedViewer))
assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(&user_model.User{ID: 4, IsAdmin: true, IsRestricted: true}, other))
}
+9
View File
@@ -10,7 +10,9 @@ import (
"gitea.dev/models/db"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
@@ -626,3 +628,10 @@ func TestCreateOrganization4(t *testing.T) {
assert.True(t, db.IsErrNameReserved(err))
unittest.CheckConsistencyFor(t, &organization.Organization{}, &organization.Team{})
}
func TestOrAnyRepoUnitPermission(t *testing.T) {
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
org := organization.Organization{Visibility: structs.VisibleTypeLimited}
assert.Equal(t, perm.AccessModeNone, org.AnyRepoUnitPermission(t.Context(), nil, unit.TypeWiki))
assert.Equal(t, perm.AccessModeRead, org.AnyRepoUnitPermission(t.Context(), &user_model.User{}, unit.TypeWiki))
}
-14
View File
@@ -69,20 +69,6 @@ func IsOrganizationOwner(ctx context.Context, orgID, uid int64) (bool, error) {
return IsTeamMember(ctx, orgID, ownerTeam.ID, uid)
}
// IsOrganizationAdmin returns true if given user is in the owner team or an admin team.
func IsOrganizationAdmin(ctx context.Context, orgID, uid int64) (bool, error) {
teams, err := GetUserOrgTeams(ctx, orgID, uid)
if err != nil {
return false, err
}
for _, t := range teams {
if t.HasAdminAccess() {
return true, nil
}
}
return false, nil
}
// IsOrganizationMember returns true if given user is member of organization.
func IsOrganizationMember(ctx context.Context, orgID, uid int64) (bool, error) {
return db.GetEngine(ctx).
+33 -41
View File
@@ -74,19 +74,25 @@ const OwnerTeamName = "Owners"
// Team represents a organization team.
type Team struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"INDEX"`
LowerName string
Name string
Description string
AccessMode perm.AccessMode `xorm:"'authorize'"`
Members []*user_model.User `xorm:"-"`
NumRepos int
NumMembers int
Units []*TeamUnit `xorm:"-"`
IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"`
CanCreateOrgRepo bool `xorm:"NOT NULL DEFAULT false"`
Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 2"`
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"INDEX"`
LowerName string
Name string
Description string
AccessMode perm.AccessMode `xorm:"'authorize'"`
Members []*user_model.User `xorm:"-"`
NumRepos int
NumMembers int
Units []*TeamUnit `xorm:"-"`
// All repos in the org are included in this team automatically
IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"`
// Any user with CanCreateOrgRepo permission can create a repository in the organization, regardless of team membership.
// And the user will become the repo's admin (via collaborator) after the creation.
CanCreateOrgRepo bool `xorm:"NOT NULL DEFAULT false"`
Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 2"`
}
func (t *Team) IsPublic() bool { return t.Visibility.IsPublic() }
@@ -149,29 +155,14 @@ func (t *Team) LoadUnits(ctx context.Context) (err error) {
return err
}
// GetUnitNames returns the team units names
func (t *Team) GetUnitNames() (res []string) {
if t.HasAdminAccess() {
return unit.AllUnitKeyNames()
}
for _, u := range t.Units {
res = append(res, unit.Units[u.Type].NameKey)
}
return res
}
// GetUnitsMap returns the team units permissions
func (t *Team) GetUnitsMap() map[string]string {
if len(t.Units) == 0 {
return nil
}
m := make(map[string]string)
if t.HasAdminAccess() {
for _, u := range unit.Units {
m[u.NameKey] = t.AccessMode.ToString()
}
} else {
for _, u := range t.Units {
m[u.Unit().NameKey] = u.AccessMode.ToString()
}
for _, u := range t.Units {
m[u.Unit().NameKey] = u.AccessMode.ToString()
}
return m
}
@@ -191,10 +182,6 @@ func (t *Team) IsMember(ctx context.Context, userID int64) bool {
return isMember
}
func (t *Team) HasAdminAccess() bool {
return t.AccessMode >= perm.AccessModeAdmin
}
// LoadMembers returns paginated members in team of organization.
func (t *Team) LoadMembers(ctx context.Context) (err error) {
t.Members, err = GetTeamMembers(ctx, &SearchMembersOptions{
@@ -214,16 +201,21 @@ func (t *Team) UnitAccessMode(ctx context.Context, tp unit.Type) perm.AccessMode
return accessMode
}
func (t *Team) UnitAccessModeEx(ctx context.Context, tp unit.Type) (accessMode perm.AccessMode, exist bool) {
func (t *Team) UnitAccessModeEx(ctx context.Context, tp unit.Type) (mode perm.AccessMode, exist bool) {
if err := t.LoadUnits(ctx); err != nil {
log.Warn("Error loading team (ID: %d) units: %s", t.ID, err.Error())
log.Error("Error loading team (ID: %d) units: %v", t.ID, err)
}
for _, u := range t.Units {
if u.Type == tp {
return u.AccessMode, true
mode, exist = u.AccessMode, true
break
}
}
return perm.AccessModeNone, false
mode = max(mode, t.AccessMode)
if unitDef, ok := unit.Units[tp]; ok {
mode = min(mode, unitDef.MaxPerm())
}
return mode, exist || t.AccessMode > perm.AccessModeNone
}
// IsUsableTeamName tests if a name could be as team name
+15 -9
View File
@@ -27,24 +27,30 @@ func (t TeamList) LoadUnits(ctx context.Context) error {
return nil
}
func (t TeamList) UnitMaxAccess(tp unit.Type) perm.AccessMode {
func (t TeamList) AnyRepoUnitMaxAccess(ctx context.Context, tp unit.Type) perm.AccessMode {
// FIXME: ORG-TEAM-UNIT-MAX-PERMISSION: this function is not right, team can access repo1's code doesn't mean it can access repo2's code
maxAccess := perm.AccessModeNone
for _, team := range t {
if team.IsOwnerTeam() {
return perm.AccessModeOwner
}
for _, teamUnit := range team.Units {
if teamUnit.Type != tp {
continue
}
if teamUnit.AccessMode > maxAccess {
maxAccess = teamUnit.AccessMode
}
}
maxAccess = max(maxAccess, team.UnitAccessMode(ctx, tp))
}
return maxAccess
}
func (t TeamList) HasAllRepoAdminAccess() bool {
for _, team := range t {
if team.IsOwnerTeam() || team.AccessMode == perm.AccessModeOwner {
return true
}
if team.IncludesAllRepositories && team.AccessMode >= perm.AccessModeAdmin {
return true
}
}
return false
}
// SearchTeamOptions holds the search options
type SearchTeamOptions struct {
db.ListOptions
+20
View File
@@ -7,6 +7,8 @@ import (
"testing"
org_model "gitea.dev/models/organization"
"gitea.dev/models/perm"
"gitea.dev/models/unit"
"gitea.dev/models/unittest"
"github.com/stretchr/testify/assert"
@@ -22,3 +24,21 @@ func Test_GetTeamsByIDs(t *testing.T) {
assert.Equal(t, "Owners", teams[1].Name)
assert.Equal(t, "team1", teams[2].Name)
}
func TestTeamList_UnitMaxAccess(t *testing.T) {
ctx := t.Context()
adminTeam := &org_model.Team{AccessMode: perm.AccessModeAdmin, Units: nil}
writeTeam := &org_model.Team{AccessMode: perm.AccessModeWrite, Units: nil}
granularTeam := &org_model.Team{
AccessMode: perm.AccessModeNone,
Units: []*org_model.TeamUnit{
{Type: unit.TypeCode, AccessMode: perm.AccessModeWrite},
},
}
assert.Equal(t, perm.AccessModeAdmin, org_model.TeamList{adminTeam}.AnyRepoUnitMaxAccess(ctx, unit.TypeActions))
assert.Equal(t, perm.AccessModeWrite, org_model.TeamList{writeTeam}.AnyRepoUnitMaxAccess(ctx, unit.TypeActions))
assert.Equal(t, perm.AccessModeWrite, org_model.TeamList{granularTeam}.AnyRepoUnitMaxAccess(ctx, unit.TypeCode))
assert.Equal(t, perm.AccessModeNone, org_model.TeamList{granularTeam}.AnyRepoUnitMaxAccess(ctx, unit.TypeActions))
assert.Equal(t, perm.AccessModeAdmin, org_model.TeamList{granularTeam, adminTeam}.AnyRepoUnitMaxAccess(ctx, unit.TypeActions))
}
-1
View File
@@ -52,7 +52,6 @@ func RemoveTeamRepo(ctx context.Context, teamID, repoID int64) error {
// GetTeamsWithAccessToAnyRepoUnit returns all teams in an organization that have given access level to the repository special unit.
// This function is only used for finding some teams that can be used as branch protection allowlist or reviewers, it isn't really used for access control.
// FIXME: TEAM-UNIT-PERMISSION this logic is not complete, search the fixme keyword to see more details
func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (teams []*Team, err error) {
teamIDs, err := getTeamIDsWithAccessToAnyRepoUnit(ctx, orgID, repoID, mode, unitType, unitTypesMore...)
if err != nil {
+27
View File
@@ -8,7 +8,9 @@ import (
"gitea.dev/models/db"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/structs"
@@ -291,3 +293,28 @@ func TestIsUsableTeamName(t *testing.T) {
assert.NoError(t, organization.IsUsableTeamName("usable"))
assert.True(t, db.IsErrNameReserved(organization.IsUsableTeamName("new")))
}
func TestTeam_UnitAccessModeEx(t *testing.T) {
team := &organization.Team{
AccessMode: perm.AccessModeWrite, Units: []*organization.TeamUnit{
{Type: unit.TypeIssues, AccessMode: perm.AccessModeRead}, // team mode wins
{Type: unit.TypeWiki, AccessMode: perm.AccessModeAdmin}, // unit mode wins
},
}
mode, exist := team.UnitAccessModeEx(t.Context(), unit.TypeActions)
assert.True(t, exist)
assert.Equal(t, perm.AccessModeWrite, mode)
assert.Equal(t, perm.AccessModeWrite, team.UnitAccessMode(t.Context(), unit.TypeIssues))
assert.Equal(t, perm.AccessModeAdmin, team.UnitAccessMode(t.Context(), unit.TypeWiki))
assert.Equal(t, perm.AccessModeRead, team.UnitAccessMode(t.Context(), unit.TypeExternalWiki)) // limited by unit definition
team = &organization.Team{AccessMode: perm.AccessModeOwner, Units: []*organization.TeamUnit{}}
mode, exist = team.UnitAccessModeEx(t.Context(), unit.TypePackages)
assert.True(t, exist)
assert.Equal(t, perm.AccessModeAdmin, mode)
team = &organization.Team{AccessMode: perm.AccessModeNone, Units: []*organization.TeamUnit{}}
mode, exist = team.UnitAccessModeEx(t.Context(), unit.TypeActions)
assert.False(t, exist)
assert.Equal(t, perm.AccessModeNone, mode)
}
+5 -1
View File
@@ -126,6 +126,10 @@ func IsBlobAccessibleForUser(ctx context.Context, blobID int64, user *user_model
if user.IsAdmin {
return true, nil
}
ownerVisibilities := []structs.VisibleType{structs.VisibleTypePublic}
if !user.IsRestricted {
ownerVisibilities = append(ownerVisibilities, structs.VisibleTypeLimited)
}
maxTeamAuthorize := builder.
Select("max(team.authorize)").
@@ -144,7 +148,7 @@ func IsBlobAccessibleForUser(ctx context.Context, blobID int64, user *user_model
// owner = user
builder.Eq{"`user`.id": user.ID}.
// user can see owner
Or(builder.Eq{"`user`.visibility": structs.VisibleTypePublic}.Or(builder.Eq{"`user`.visibility": structs.VisibleTypeLimited})).
Or(builder.In("`user`.visibility", ownerVisibilities)).
// owner is an organization and user has access to it
Or(builder.Eq{"`user`.type": user_model.UserTypeOrganization}.
And(builder.Lte{strconv.Itoa(int(perm.AccessModeRead)): maxTeamAuthorize}.Or(builder.Lte{strconv.Itoa(int(perm.AccessModeRead)): maxTeamUnitAccessMode}))),
+23
View File
@@ -7,6 +7,7 @@ import (
"testing"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -49,3 +50,25 @@ func TestGetOrInsertBlobConcurrent(t *testing.T) {
}
assert.Equal(t, numGoroutines-1, existedCount)
}
func TestIsBlobAccessibleForRestrictedUser(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 33})
pkg, err := TryInsertPackage(t.Context(), &Package{OwnerID: owner.ID, Type: TypeContainer, Name: "limited", LowerName: "limited"})
require.NoError(t, err)
version, err := GetOrInsertVersion(t.Context(), &PackageVersion{PackageID: pkg.ID, Version: "1", LowerVersion: "1"})
require.NoError(t, err)
blob, _, err := GetOrInsertBlob(t.Context(), &PackageBlob{Size: 1, HashMD5: "md5", HashSHA1: "sha1", HashSHA256: "sha256", HashSHA512: "sha512"})
require.NoError(t, err)
_, err = TryInsertFile(t.Context(), &PackageFile{VersionID: version.ID, BlobID: blob.ID, Name: "blob", LowerName: "blob"})
require.NoError(t, err)
accessible, err := IsBlobAccessibleForUser(t.Context(), blob.ID, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}))
require.NoError(t, err)
assert.True(t, accessible)
accessible, err = IsBlobAccessibleForUser(t.Context(), blob.ID, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29}))
require.NoError(t, err)
assert.False(t, accessible)
}
+5 -11
View File
@@ -33,7 +33,9 @@ func init() {
db.RegisterModel(new(Access))
}
func accessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Repository) (perm.AccessMode, error) {
// modeByOwnerAndAccess returns the access mode of a user to a repository,
// considering: the repository's owner (visibility and doer restriction), any explicit "access" records.
func modeByOwnerAndAccess(ctx context.Context, user *user_model.User, repo *repo_model.Repository) (perm.AccessMode, error) {
mode := perm.AccessModeNone
var userID int64
restricted := false
@@ -69,14 +71,6 @@ func accessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Re
return a.Mode, nil
}
func maxAccessMode(modes ...perm.AccessMode) perm.AccessMode {
maxMode := perm.AccessModeNone
for _, mode := range modes {
maxMode = max(maxMode, mode)
}
return maxMode
}
type userAccess struct {
User *user_model.User
Mode perm.AccessMode
@@ -85,7 +79,7 @@ type userAccess struct {
// updateUserAccess updates an access map so that user has at least mode
func updateUserAccess(accessMap map[int64]*userAccess, user *user_model.User, mode perm.AccessMode) {
if ua, ok := accessMap[user.ID]; ok {
ua.Mode = maxAccessMode(ua.Mode, mode)
ua.Mode = max(ua.Mode, mode)
} else {
accessMap[user.ID] = &userAccess{User: user, Mode: mode}
}
@@ -263,7 +257,7 @@ func RecalculateUserAccess(ctx context.Context, repo *repo_model.Repository, uid
t.AccessMode = perm.AccessModeOwner
}
accessMode = maxAccessMode(accessMode, t.AccessMode)
accessMode = max(accessMode, t.AccessMode)
}
}
@@ -122,6 +122,18 @@ func TestGetActionsUserRepoPermission(t *testing.T) {
require.NoError(t, err)
assert.False(t, perm.CanRead(unit.TypeCode))
// Reusable workflows use a separate authorization path and must enforce
// the same fork-PR restriction.
run := &actions_model.ActionRun{RepoID: repo2.ID, IsForkPullRequest: true}
allowed, err := CanReadWorkflowCrossRepo(ctx, repo15, run)
require.NoError(t, err)
assert.False(t, allowed)
run.IsForkPullRequest = false
allowed, err = CanReadWorkflowCrossRepo(ctx, repo15, run)
require.NoError(t, err)
assert.True(t, allowed)
// Restore state for subsequent subtests.
task53.IsForkPullRequest = false
require.NoError(t, actions_model.UpdateTask(ctx, task53, "is_fork_pull_request"))
+67 -36
View File
@@ -33,6 +33,8 @@ type Permission struct {
everyoneAccessMode map[unit.Type]perm_model.AccessMode // the unit's minimal access mode for every signed-in user
anonymousAccessMode map[unit.Type]perm_model.AccessMode // the unit's minimal access mode for anonymous (non-signed-in) user
orgRepoTeams []*organization.Team
}
// IsOwner returns true if current user is the owner of repository.
@@ -445,7 +447,7 @@ func GetIndividualUserRepoPermission(ctx context.Context, repo *repo_model.Repos
}
// plain user TODO: this check should be replaced, only need to check collaborator access mode
perm.AccessMode, err = accessLevel(ctx, user, repo)
perm.AccessMode, err = modeByOwnerAndAccess(ctx, user, repo)
if err != nil {
return perm, err
}
@@ -459,11 +461,11 @@ func GetIndividualUserRepoPermission(ctx context.Context, repo *repo_model.Repos
perm.AccessMode = max(perm.AccessMode, minAccessMode)
// get units mode from teams
teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID)
perm.orgRepoTeams, err = organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID)
if err != nil {
return perm, err
}
if len(teams) == 0 {
if len(perm.orgRepoTeams) == 0 {
return perm, nil
}
@@ -477,8 +479,8 @@ func GetIndividualUserRepoPermission(ctx context.Context, repo *repo_model.Repos
}
// if user in an owner team
for _, team := range teams {
if team.HasAdminAccess() {
for _, team := range perm.orgRepoTeams {
if team.IsOwnerTeam() || team.AccessMode == perm_model.AccessModeOwner {
perm.AccessMode = perm_model.AccessModeOwner
perm.unitsMode = nil
return perm, nil
@@ -486,7 +488,7 @@ func GetIndividualUserRepoPermission(ctx context.Context, repo *repo_model.Repos
}
for _, u := range repo.Units {
for _, team := range teams {
for _, team := range perm.orgRepoTeams {
teamMode, _ := team.UnitAccessModeEx(ctx, u.Type)
unitAccessMode := max(perm.unitsMode[u.Type], minAccessMode, teamMode)
perm.unitsMode[u.Type] = unitAccessMode
@@ -496,52 +498,35 @@ func GetIndividualUserRepoPermission(ctx context.Context, repo *repo_model.Repos
return perm, err
}
// IsUserRealRepoAdmin check if this user is real repo admin
func IsUserRealRepoAdmin(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (bool, error) {
if repo.OwnerID == user.ID {
return true, nil
}
if err := repo.LoadOwner(ctx); err != nil {
return false, err
}
accessMode, err := accessLevel(ctx, user, repo)
if err != nil {
return false, err
}
return accessMode >= perm_model.AccessModeAdmin, nil
func IsUserRepoAdmin(ctx context.Context, repo *repo_model.Repository, user *user_model.User) bool {
return (user != nil && user.IsAdmin) || IsUserRealRepoAdmin(ctx, repo, user)
}
// IsUserRepoAdmin return true if user has admin right of a repo
func IsUserRepoAdmin(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (bool, error) {
// IsUserRealRepoAdmin check if this user is real repo admin (but not a site admin who also has repo admin access)
func IsUserRealRepoAdmin(ctx context.Context, repo *repo_model.Repository, user *user_model.User) bool {
if user == nil || repo == nil {
return false, nil
}
if user.IsAdmin {
return true, nil
return false
}
mode, err := accessLevel(ctx, user, repo)
mode, err := modeByOwnerAndAccess(ctx, user, repo)
if err != nil {
return false, err
return false
}
if mode >= perm_model.AccessModeAdmin {
return true, nil
return true
}
teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID)
if err != nil {
return false, err
return false
}
for _, team := range teams {
if team.HasAdminAccess() {
return true, nil
if team.AccessMode >= perm_model.AccessModeAdmin {
return true
}
}
return false, nil
return false
}
// AccessLevel returns the Access a user has to a repository. Will return NoneAccess if the
@@ -658,6 +643,16 @@ func PermissionNoAccess() Permission {
return Permission{AccessMode: perm_model.AccessModeNone}
}
// RepoUserPermissionCacheKey is the cachegroup.RepoUserPermission key of a doer's
// permission on a repository. Producers and consumers must agree on it, so it lives here.
func RepoUserPermissionCacheKey(repoID int64, doer *user_model.User) string {
var doerID int64
if doer != nil {
doerID = doer.ID
}
return fmt.Sprintf("%d-%d", repoID, doerID)
}
// CanReadWorkflowCrossRepo checks whether the run can read workflow files from targetRepo.
func CanReadWorkflowCrossRepo(ctx context.Context, targetRepo *repo_model.Repository, run *actions_model.ActionRun) (bool, error) {
if err := run.LoadRepo(ctx); err != nil {
@@ -675,7 +670,7 @@ func CanReadWorkflowCrossRepo(ctx context.Context, targetRepo *repo_model.Reposi
// logs in a publicly visible run; requiring a private caller keeps private content flowing private -> private.
// This is intentionally stricter than GitHub, which gates on the target repo's access setting (introduced in #32562):
// https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository#allowing-access-to-components-in-a-private-repository
if run.Repo.IsPrivate {
if run.Repo.IsPrivate && !run.IsForkPullRequest {
if actionsUnit, err := targetRepo.GetUnit(ctx, unit.TypeActions); err == nil {
if actionsUnit.ActionsConfig().IsCollaborativeOwner(run.Repo.OwnerID) {
return true, nil
@@ -691,3 +686,39 @@ func CanReadWorkflowCrossRepo(ctx context.Context, targetRepo *repo_model.Reposi
}
return botPerm.AccessMode >= perm_model.AccessModeRead, nil
}
func CanDoerManageRepoDangerZone(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, perm *Permission) bool {
if perm.IsOwner() {
return true
}
// FIXME: ORG-REPO-ADMIN-DANGER-ZONE: this is the legacy logic, "org repo admin" can delete a repo
// Ideally we need a new field in like "AdminManageDangerZone" to control this permission, but for now we keep the legacy logic
for _, team := range perm.orgRepoTeams {
if team.AccessMode >= perm_model.AccessModeAdmin {
return true
}
}
// A special case: if the team allows to create repo, then the doer will be added as a collaborator with admin access.
// For this case, we also allow the doer to manage the danger zone as well, because the doer is effectively a repo admin.
// Since the admin permission from team is already allowed above (legacy logic), here nothing worse.
// Keep in mind: the newly created repo isn't in any org team, it only has the doer as a collaborator with admin access.
// So we need to get all the teams of the doer to check.
allowCreateRepo := false
doerOrgTeams, _ := organization.GetUserOrgTeams(ctx, repo.OwnerID, doer.ID)
for _, team := range doerOrgTeams {
if allowCreateRepo = team.CanCreateOrgRepo; allowCreateRepo {
break
}
}
return allowCreateRepo && perm.IsAdmin()
}
func CanDoerManageOrgRepoCollaboratorTeam(ctx context.Context, repo *repo_model.Repository, perm *Permission) bool {
_ = repo.LoadOwner(ctx)
if repo.Owner == nil || !repo.Owner.IsOrganization() {
return false
}
return perm.IsOwner() || perm.IsAdmin() && repo.Owner.RepoAdminChangeTeamAccess
}
+1 -1
View File
@@ -236,7 +236,7 @@ func (opts SearchOptions) ToConds() builder.Cond {
}
func (opts SearchOptions) ToOrders() string {
return opts.OrderBy.String()
return util.IfZero(opts.OrderBy.String(), "id")
}
func GetSearchOrderByBySortType(sortType string) db.SearchOrderBy {
+15 -39
View File
@@ -43,6 +43,10 @@ type FindCollaborationOptions struct {
CollaboratorID int64
}
func (opts *FindCollaborationOptions) ToOrders() string {
return "collaboration.id"
}
func (opts *FindCollaborationOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.RepoID != 0 {
@@ -111,49 +115,21 @@ func IsCollaborator(ctx context.Context, repoID, userID int64) (bool, error) {
return db.Exist[Collaboration](ctx, builder.Eq{"repo_id": repoID, "user_id": userID})
}
// ChangeCollaborationAccessMode sets new access mode for the collaboration.
func ChangeCollaborationAccessMode(ctx context.Context, repo *Repository, uid int64, mode perm.AccessMode) error {
// Discard invalid input
if mode <= perm.AccessModeNone || mode > perm.AccessModeOwner {
return nil
}
return db.WithTx(ctx, func(ctx context.Context) error {
collaboration, has, err := db.Get[Collaboration](ctx, builder.Eq{"repo_id": repo.ID, "user_id": uid})
if err != nil {
return fmt.Errorf("get collaboration: %w", err)
} else if !has {
return nil
}
if collaboration.Mode == mode {
return nil
}
collaboration.Mode = mode
if _, err = db.GetEngine(ctx).
ID(collaboration.ID).
Cols("mode").
Update(collaboration); err != nil {
return fmt.Errorf("update collaboration: %w", err)
} else if _, err = db.Exec(ctx, "UPDATE access SET mode = ? WHERE user_id = ? AND repo_id = ?", mode, uid, repo.ID); err != nil {
return fmt.Errorf("update access table: %w", err)
}
return nil
})
}
// IsOwnerMemberCollaborator checks if a provided user is the owner, a collaborator or a member of a team in a repository
func IsOwnerMemberCollaborator(ctx context.Context, repo *Repository, userID int64) (bool, error) {
func HasAccessToRepoCodeUnit(ctx context.Context, repo *Repository, userID int64) (bool, error) {
if repo.OwnerID == userID {
return true, nil
}
teamMember, err := db.GetEngine(ctx).Join("INNER", "team_repo", "team_repo.team_id = team_user.team_id").
Join("INNER", "team_unit", "team_unit.team_id = team_user.team_id").
teamMember, err := db.GetEngine(ctx).Table("team_user").
Join("INNER", "team_repo", "team_repo.team_id = team_user.team_id").
Join("INNER", "team", "team.id = team_user.team_id").
Join("LEFT", "team_unit", "team_unit.team_id = team_user.team_id AND team_unit.`type` = ?", unit.TypeCode).
Where("team_repo.repo_id = ?", repo.ID).
And("team_unit.`type` = ?", unit.TypeCode).
And("team_user.uid = ?", userID).Table("team_user").Exist()
And("team_user.uid = ?", userID).
And(builder.Or(
builder.Gt{"team.authorize": perm.AccessModeNone},
builder.Gt{"team_unit.access_mode": perm.AccessModeNone},
)).
Exist()
if err != nil {
return false, err
}
+5 -29
View File
@@ -7,8 +7,6 @@ import (
"testing"
"gitea.dev/models/db"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
@@ -69,59 +67,37 @@ func TestRepository_IsCollaborator(t *testing.T) {
test(4, 4, true)
}
func TestRepository_ChangeCollaborationAccessMode(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
assert.NoError(t, repo_model.ChangeCollaborationAccessMode(t.Context(), repo, 4, perm.AccessModeAdmin))
collaboration := unittest.AssertExistsAndLoadBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: 4})
assert.Equal(t, perm.AccessModeAdmin, collaboration.Mode)
access := unittest.AssertExistsAndLoadBean(t, &access_model.Access{UserID: 4, RepoID: repo.ID})
assert.Equal(t, perm.AccessModeAdmin, access.Mode)
assert.NoError(t, repo_model.ChangeCollaborationAccessMode(t.Context(), repo, 4, perm.AccessModeAdmin))
assert.NoError(t, repo_model.ChangeCollaborationAccessMode(t.Context(), repo, unittest.NonexistentID, perm.AccessModeAdmin))
// Discard invalid input.
assert.NoError(t, repo_model.ChangeCollaborationAccessMode(t.Context(), repo, 4, perm.AccessMode(-1)))
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
}
func TestRepository_IsOwnerMemberCollaborator(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
// Organisation owner.
actual, err := repo_model.IsOwnerMemberCollaborator(t.Context(), repo1, 2)
actual, err := repo_model.HasAccessToRepoCodeUnit(t.Context(), repo1, 2)
assert.NoError(t, err)
assert.True(t, actual)
// Team member.
actual, err = repo_model.IsOwnerMemberCollaborator(t.Context(), repo1, 4)
actual, err = repo_model.HasAccessToRepoCodeUnit(t.Context(), repo1, 4)
assert.NoError(t, err)
assert.True(t, actual)
// Normal user.
actual, err = repo_model.IsOwnerMemberCollaborator(t.Context(), repo1, 1)
actual, err = repo_model.HasAccessToRepoCodeUnit(t.Context(), repo1, 1)
assert.NoError(t, err)
assert.False(t, actual)
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
// Collaborator.
actual, err = repo_model.IsOwnerMemberCollaborator(t.Context(), repo2, 4)
actual, err = repo_model.HasAccessToRepoCodeUnit(t.Context(), repo2, 4)
assert.NoError(t, err)
assert.True(t, actual)
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 15})
// Repository owner.
actual, err = repo_model.IsOwnerMemberCollaborator(t.Context(), repo3, 2)
actual, err = repo_model.HasAccessToRepoCodeUnit(t.Context(), repo3, 2)
assert.NoError(t, err)
assert.True(t, actual)
}
+8
View File
@@ -38,6 +38,10 @@ type PushMirrorOptions struct {
RemoteName string
}
func (opts PushMirrorOptions) ToOrders() string {
return "id"
}
func (opts PushMirrorOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.RepoID > 0 {
@@ -100,6 +104,10 @@ type findPushMirrorOptions struct {
SyncOnCommit optional.Option[bool]
}
func (opts findPushMirrorOptions) ToOrders() string {
return "id"
}
func (opts findPushMirrorOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.RepoID > 0 {
+2 -1
View File
@@ -87,6 +87,7 @@ type Release struct {
IsTag bool `xorm:"NOT NULL DEFAULT false"` // will be true only if the record is a tag and has no related releases
Attachments []*Attachment `xorm:"-"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX"`
PublishedUnix timeutil.TimeStamp `xorm:"NOT NULL DEFAULT 0"`
}
func init() {
@@ -473,7 +474,7 @@ func PushUpdateDeleteTags(ctx context.Context, repo *Repository, tags []string)
if _, err := db.GetEngine(ctx).
Where("repo_id = ? AND is_tag = ?", repo.ID, false).
In("lower_tag_name", lowerTags).
Cols("is_draft", "num_commits", "sha1").
Cols("is_draft", "num_commits", "sha1", "published_unix").
Update(&Release{
IsDraft: true,
}); err != nil {
+16 -12
View File
@@ -169,8 +169,8 @@ type SearchRepoOptions struct {
// False -> include just public
IsPrivate optional.Option[bool]
// None -> include collaborative AND non-collaborative
// True -> include just collaborative
// False -> include just non-collaborative
// True -> include just collaborative (the "OwnerID" is not really an owner, it just means a collaborator who doesn't own the repo)
// False -> include just non-collaborative (the repo must be in the owner's name space)
Collaborate optional.Option[bool]
// What type of unit the user can be collaborative in,
// it is ignored if Collaborate is False.
@@ -310,15 +310,12 @@ func userOrgTeamRepoBuilder(userID int64) *builder.Builder {
}
// 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 {
return userOrgTeamRepoBuilder(userID).
Join("INNER", "team", "`team`.id = `team_repo`.team_id").
Join("LEFT", "team_unit", builder.Expr("`team_unit`.team_id = `team_repo`.team_id AND `team_unit`.`type` = ?", unitType)).
Where(builder.Or(
builder.Gte{"`team`.authorize": int(perm.AccessModeAdmin)},
builder.Gt{"`team`.authorize": int(perm.AccessModeNone)},
builder.Gt{"`team_unit`.`access_mode`": int(perm.AccessModeNone)},
))
}
@@ -620,7 +617,7 @@ func searchRepositoryByCondition(ctx context.Context, opts SearchRepoOptions, co
args = append(args, opts.PriorityOwnerID)
} else if strings.Count(opts.Keyword, "/") == 1 {
// With "owner/repo" search times, prioritise results which match the owner field
orgName := strings.Split(opts.Keyword, "/")[0]
orgName, _, _ := strings.Cut(opts.Keyword, "/")
orderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN owner_name LIKE ? THEN 0 ELSE 1 END, %s", orderBy))
args = append(args, orgName)
}
@@ -655,14 +652,12 @@ func SearchRepositoryIDsByCondition(ctx context.Context, cond builder.Cond) ([]i
Find(&repoIDs)
}
func userAllPublicRepoCond(cond builder.Cond, orgVisibilityLimit []structs.VisibleType) builder.Cond {
func userAllPublicRepoCond(cond builder.Cond, ownerVisibilityLimit []structs.VisibleType) builder.Cond {
return cond.Or(builder.And(
builder.Eq{"`repository`.is_private": false},
// Aren't in a private organisation or limited organisation if we're not logged in
// Exclude owners who are not visible to the caller.
builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where(
builder.And(
builder.Eq{"type": user_model.UserTypeOrganization},
builder.In("visibility", orgVisibilityLimit)),
builder.In("visibility", ownerVisibilityLimit),
))))
}
@@ -771,6 +766,15 @@ func PublicRepoUnderPublicOwnerCond() builder.Cond {
)
}
// NotPublicRepoUnderPublicOwnerCond complements PublicRepoUnderPublicOwnerCond. Spelled positively so
// the owner subquery hashes the limited/private minority, not every public user.
func NotPublicRepoUnderPublicOwnerCond() builder.Cond {
return builder.Or(
builder.Eq{"`repository`.is_private": true},
builder.In("`repository`.owner_id", builder.Select("id").From("`user`").Where(builder.Neq{"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.
+29 -16
View File
@@ -271,6 +271,23 @@ func testSearchRepositoryRestricted(t *testing.T) {
})
}
func TestSearchRepositoryExcludesHiddenIndividualOwners(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
hiddenOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
require.NoError(t, user_model.UpdateUserCols(t.Context(), &user_model.User{
ID: hiddenOwner.ID,
Visibility: structs.VisibleTypePrivate,
}, "visibility"))
repos, _, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{Page: 1, PageSize: 100},
Keyword: "repo1",
})
require.NoError(t, err)
assert.NotContains(t, repoIDs(repos), int64(1))
}
func testSearchRepositoryPrivate(t *testing.T) {
// test search private repository on explore page
repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{
@@ -486,10 +503,7 @@ func TestFindUserActionsAccessibleOwnerRepoIDs(t *testing.T) {
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.
// TestUserOrgUnitRepoCondTeamAuthorize pins team.authorize vs team_unit.access_mode
func TestUserOrgUnitRepoCondTeamAuthorize(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
@@ -500,17 +514,16 @@ func TestUserOrgUnitRepoCondTeamAuthorize(t *testing.T) {
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")
// Owner team5 has no Actions team_unit row but still grants via authorize=owner.
assert.Contains(t, accessibleRepoIDs(18, 17, unit.TypeActions), int64(24))
// 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")
// team2 is "authorize=write" with Projects team_unit but no Actions row.
assert.Contains(t, accessibleRepoIDs(4, 3, unit.TypeProjects), int64(3))
assert.Contains(t, accessibleRepoIDs(4, 3, unit.TypeActions), int64(3))
// now team2 is "authorize=none", no Actions row.
_, err := db.GetEngine(t.Context()).Exec("UPDATE team SET authorize=0 WHERE id=2")
assert.NoError(t, err)
assert.Contains(t, accessibleRepoIDs(4, 3, unit.TypeProjects), int64(3))
assert.NotContains(t, accessibleRepoIDs(4, 3, unit.TypeActions), int64(3))
}
+2 -2
View File
@@ -67,11 +67,11 @@ func TestWatchRepo(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 3})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
assert.NoError(t, WatchRepo(t.Context(), user, repo, true))
assert.NoError(t, WatchRepoAuto(t.Context(), user, repo, true))
unittest.AssertExistsAndLoadBean(t, &Watch{RepoID: repo.ID, UserID: user.ID})
unittest.CheckConsistencyFor(t, &Repository{ID: repo.ID})
assert.NoError(t, WatchRepo(t.Context(), user, repo, false))
assert.NoError(t, WatchRepoAuto(t.Context(), user, repo, false))
unittest.AssertNotExistsBean(t, &Watch{RepoID: repo.ID, UserID: user.ID})
unittest.CheckConsistencyFor(t, &Repository{ID: repo.ID})
}
+5 -1
View File
@@ -72,6 +72,8 @@ type RepoTransfer struct { //nolint:revive // export stutter
TeamIDs []int64
Teams []*organization.Team `xorm:"-"`
RecipientAccessGranted bool `xorm:"NOT NULL DEFAULT false"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX NOT NULL created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX NOT NULL updated"`
}
@@ -221,7 +223,7 @@ func TestRepositoryReadyForTransfer(status RepositoryStatus) error {
// CreatePendingRepositoryTransfer transfer a repo from one owner to a new one.
// it marks the repository transfer as "pending"
func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.User, repoID int64, teams []*organization.Team) error {
func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.User, repoID int64, teams []*organization.Team, recipientAccessGranted bool) error {
return db.WithTx(ctx, func(ctx context.Context) error {
repo, err := GetRepositoryByID(ctx, repoID)
if err != nil {
@@ -270,6 +272,8 @@ func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_m
UpdatedUnix: timeutil.TimeStampNow(),
DoerID: doer.ID,
TeamIDs: make([]int64, 0, len(teams)),
RecipientAccessGranted: recipientAccessGranted,
}
for k := range teams {
+2 -3
View File
@@ -11,13 +11,12 @@ import (
"mime/multipart"
"os"
"path/filepath"
"uuid"
"gitea.dev/models/db"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
gouuid "github.com/google/uuid"
)
// ErrUploadNotExist represents a "UploadNotExist" kind of error.
@@ -60,7 +59,7 @@ func (upload *Upload) LocalPath() string {
// NewUpload creates a new upload object.
func NewUpload(ctx context.Context, name string, buf []byte, file multipart.File) (_ *Upload, err error) {
upload := &Upload{
UUID: gouuid.New().String(),
UUID: uuid.New().String(),
Name: name,
}
+8
View File
@@ -27,6 +27,10 @@ type StarredReposOptions struct {
Actor *user_model.User
}
func (opts *StarredReposOptions) ToOrders() string {
return "`repository`.id"
}
func (opts *StarredReposOptions) ApplyPublicOnly(publicOnly bool) {
if publicOnly {
opts.IncludePrivate = false
@@ -76,6 +80,10 @@ type WatchedReposOptions struct {
Actor *user_model.User
}
func (opts *WatchedReposOptions) ToOrders() string {
return "`repository`.id"
}
func (opts *WatchedReposOptions) ApplyPublicOnly(publicOnly bool) {
if publicOnly {
opts.IncludePrivate = false
+83 -71
View File
@@ -18,14 +18,11 @@ import (
type WatchMode int8
const (
// WatchModeNone don't watch
WatchModeNone WatchMode = iota // 0
// WatchModeNormal watch repository (from other sources)
WatchModeNormal // 1
// WatchModeDont explicit don't auto-watch
WatchModeDont // 2
// WatchModeAuto watch repository (from AutoWatchOnChanges)
WatchModeAuto // 3
WatchModeNone WatchMode = iota // 0 watch nothing unless mentioned
WatchModeNormal // 1 proactively watching (all or custom)
WatchModeDont // 2 ignore the repo
WatchModeAuto // 3 automatically watching (from AutoWatchOnChanges)
)
// WatchType is the `watch` column gating one kind of notification
@@ -39,15 +36,16 @@ const (
// Watch is connection request for receiving repository notification.
type Watch struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"UNIQUE(watch)"`
RepoID int64 `xorm:"UNIQUE(watch)"`
Mode WatchMode `xorm:"SMALLINT NOT NULL DEFAULT 1"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
PullRequests bool `xorm:"NOT NULL DEFAULT true"`
Issues bool `xorm:"NOT NULL DEFAULT true"`
Releases bool `xorm:"NOT NULL DEFAULT true"`
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"UNIQUE(watch)"`
RepoID int64 `xorm:"UNIQUE(watch)"`
Mode WatchMode `xorm:"SMALLINT NOT NULL DEFAULT 1"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
IncludePullRequests bool `xorm:"NOT NULL DEFAULT true"`
IncludeIssues bool `xorm:"NOT NULL DEFAULT true"`
IncludeReleases bool `xorm:"NOT NULL DEFAULT true"`
}
func init() {
@@ -61,7 +59,7 @@ func GetWatch(ctx context.Context, userID, repoID int64) (*Watch, error) {
return watch, err
}
if watch == nil { // the dummy record must mirror the column defaults
watch = &Watch{UserID: userID, RepoID: repoID, PullRequests: true, Issues: true, Releases: true}
watch = &Watch{UserID: userID, RepoID: repoID, IncludePullRequests: true, IncludeIssues: true, IncludeReleases: true}
}
if !has {
watch.Mode = WatchModeNone
@@ -76,12 +74,16 @@ func (w *Watch) IsIgnoring() bool {
// IsWatching reports whether the watch counts the user as a watcher of the repository
func (w *Watch) IsWatching() bool {
return IsWatchMode(w.Mode)
return IsWatchModeWatching(w.Mode)
}
// IsWatchingAll reports whether every event is enabled, which is the "all activity" mode
func (w *Watch) IsWatchingAll() bool {
return w.PullRequests && w.Issues && w.Releases
return w.IncludePullRequests && w.IncludeIssues && w.IncludeReleases
}
func (w *Watch) IsWatchingAny() bool {
return w.IncludePullRequests || w.IncludeIssues || w.IncludeReleases
}
// SelectedMode returns the mode the user picked in the watch menu
@@ -89,7 +91,7 @@ func (w *Watch) SelectedMode() string {
switch {
case w.IsIgnoring():
return "ignore"
case !IsWatchMode(w.Mode), !(w.PullRequests || w.Issues || w.Releases):
case !IsWatchModeWatching(w.Mode), !w.IsWatchingAny():
return "participate" // also the default while there is no watch row
case w.IsWatchingAll():
return "all"
@@ -97,110 +99,105 @@ func (w *Watch) SelectedMode() string {
return "custom"
}
// IsWatchMode Decodes watchability of WatchMode
func IsWatchMode(mode WatchMode) bool {
// IsWatchModeWatching Decodes watchability of WatchMode
func IsWatchModeWatching(mode WatchMode) bool {
return mode != WatchModeNone && mode != WatchModeDont
}
// IsWatching checks if user has watched given repository.
func IsWatching(ctx context.Context, userID, repoID int64) bool {
// IsWatchingRepo checks if user has watched given repository.
func IsWatchingRepo(ctx context.Context, userID, repoID int64) bool {
watch, err := GetWatch(ctx, userID, repoID)
return err == nil && IsWatchMode(watch.Mode)
return err == nil && IsWatchModeWatching(watch.Mode)
}
func watchRepoMode(ctx context.Context, watch *Watch, mode WatchMode) (err error) {
func watchRepoByMode(ctx context.Context, watch *Watch, mode WatchMode) (err error) {
if watch.Mode == mode {
return nil
}
if mode == WatchModeAuto && (watch.Mode == WatchModeDont || IsWatchMode(watch.Mode)) {
if mode == WatchModeAuto && (watch.Mode == WatchModeDont || IsWatchModeWatching(watch.Mode)) {
// Don't auto watch if already watching or deliberately not watching
return nil
}
hadrec := watch.Mode != WatchModeNone
needsrec := mode != WatchModeNone
repodiff := 0
hadWatchModeSet := watch.Mode != WatchModeNone
needSetWatchMode := mode != WatchModeNone
repoWatchDelta := 0
if IsWatchMode(mode) && !IsWatchMode(watch.Mode) {
repodiff = 1
} else if !IsWatchMode(mode) && IsWatchMode(watch.Mode) {
repodiff = -1
if IsWatchModeWatching(mode) && !IsWatchModeWatching(watch.Mode) {
repoWatchDelta = 1
} else if !IsWatchModeWatching(mode) && IsWatchModeWatching(watch.Mode) {
repoWatchDelta = -1
}
if repodiff == 1 { // starting to watch resets the options, otherwise a custom selection survives
watch.PullRequests, watch.Issues, watch.Releases = true, true, true
if repoWatchDelta == 1 { // starting to watch resets the options, otherwise a custom selection survives
watch.IncludePullRequests, watch.IncludeIssues, watch.IncludeReleases = true, true, true
}
watch.Mode = mode
if !hadrec && needsrec {
if !hadWatchModeSet && needSetWatchMode {
if err = db.Insert(ctx, watch); err != nil {
return err
}
} else if needsrec {
} else if needSetWatchMode {
if _, err := db.GetEngine(ctx).ID(watch.ID).AllCols().Update(watch); err != nil {
return err
}
} else if _, err = db.DeleteByID[Watch](ctx, watch.ID); err != nil {
return err
}
if repodiff != 0 {
_, err = db.GetEngine(ctx).Exec("UPDATE `repository` SET num_watches = num_watches + ? WHERE id = ?", repodiff, watch.RepoID)
if repoWatchDelta != 0 {
_, err = db.GetEngine(ctx).Exec("UPDATE `repository` SET num_watches = num_watches + ? WHERE id = ?", repoWatchDelta, watch.RepoID)
}
return err
}
// WatchRepo watch or unwatch repository.
func WatchRepo(ctx context.Context, doer *user_model.User, repo *Repository, doWatch bool) error {
// WatchRepoAuto watch or unwatch repository.
func WatchRepoAuto(ctx context.Context, doer *user_model.User, repo *Repository, doWatch bool) error {
watch, err := GetWatch(ctx, doer.ID, repo.ID)
if err != nil {
return err
}
if !doWatch && watch.Mode == WatchModeAuto {
return watchRepoMode(ctx, watch, WatchModeDont)
return watchRepoByMode(ctx, watch, WatchModeDont)
} else if !doWatch {
return watchRepoMode(ctx, watch, WatchModeNone)
return watchRepoByMode(ctx, watch, WatchModeNone)
}
if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) {
return user_model.ErrBlockedUser
}
return watchRepoMode(ctx, watch, WatchModeNormal)
}
// WatchIgnoreRepo mutes the repository (unwatch), so nothing about it reaches the user.
func WatchIgnoreRepo(ctx context.Context, doer *user_model.User, repo *Repository) error {
watch, err := GetWatch(ctx, doer.ID, repo.ID)
if err != nil {
return err
}
return watchRepoMode(ctx, watch, WatchModeDont)
return watchRepoByMode(ctx, watch, WatchModeNormal)
}
type WatchOptions struct {
PullRequests bool
Issues bool
Releases bool
Mode WatchMode
WatchPullRequests bool
WatchIssues bool
WatchReleases bool
}
// WatchRepoWithOptions starts watching the repository and subscribes to the given events
func WatchRepoWithOptions(ctx context.Context, doer *user_model.User, repo *Repository, opts WatchOptions) error {
return db.WithTx(ctx, func(ctx context.Context) error {
if err := WatchRepo(ctx, doer, repo, true); err != nil {
watch, err := GetWatch(ctx, doer.ID, repo.ID)
if err != nil {
return err
}
return SetWatchOptions(ctx, doer.ID, repo.ID, opts)
err = watchRepoByMode(ctx, watch, opts.Mode)
if err != nil {
return err
}
if opts.Mode == WatchModeNormal {
_, err = db.GetEngine(ctx).Where("user_id=? AND repo_id=?", doer.ID, repo.ID).
Cols("include_pull_requests", "include_issues", "include_releases").
Update(&Watch{IncludePullRequests: opts.WatchPullRequests, IncludeIssues: opts.WatchIssues, IncludeReleases: opts.WatchReleases})
}
return err
})
}
// SetWatchOptions updates the per-event options of a watch, callers must run WatchRepo first
func SetWatchOptions(ctx context.Context, userID, repoID int64, opts WatchOptions) error {
_, err := db.GetEngine(ctx).Where("user_id=? AND repo_id=?", userID, repoID).
Cols(string(WatchPullRequests), string(WatchIssues), string(WatchReleases)).
Update(&Watch{PullRequests: opts.PullRequests, Issues: opts.Issues, Releases: opts.Releases})
return err
}
// GetUserWatches returns the watches of one user, keyed by repository ID
func GetUserWatches(ctx context.Context, userID int64, repoIDs []int64) (map[int64]*Watch, error) {
if len(repoIDs) == 0 {
@@ -225,7 +222,11 @@ func GetWatchers(ctx context.Context, repoID int64) ([]*Watch, error) {
watches := make([]*Watch, 0, 10)
return watches, db.GetEngine(ctx).Where("`watch`.repo_id=?", repoID).
And("`watch`.mode<>?", WatchModeDont).
And(builder.Or(builder.Eq{"`watch`.pull_requests": true}, builder.Eq{"`watch`.issues": true}, builder.Eq{"`watch`.releases": true})).
And(builder.Or(
builder.Eq{"`watch`.include_pull_requests": true},
builder.Eq{"`watch`.include_issues": true},
builder.Eq{"`watch`.include_releases": true},
)).
And("`user`.is_active=?", true).
And("`user`.prohibit_login=?", false).
Join("INNER", "`user`", "`user`.id = `watch`.user_id").
@@ -247,10 +248,21 @@ func GetRepoIgnorersIDs(ctx context.Context, repoID int64) ([]int64, error) {
// User permissions must be verified elsewhere if required
func GetRepoWatchersIDs(ctx context.Context, repoID int64, watchType WatchType) ([]int64, error) {
ids := make([]int64, 0, 64)
var watchColName string
switch watchType {
case WatchPullRequests:
watchColName = "include_pull_requests"
case WatchIssues:
watchColName = "include_issues"
case WatchReleases:
watchColName = "include_releases"
default:
panic("invalid WatchType")
}
return ids, db.GetEngine(ctx).Table("watch").
Where("watch.repo_id=?", repoID).
And("watch.mode<>?", WatchModeDont).
And(builder.Eq{"watch." + string(watchType): true}).
And(builder.Eq{watchColName: true}).
Select("user_id").
Find(&ids)
}
@@ -283,7 +295,7 @@ func WatchIfAuto(ctx context.Context, userID, repoID int64, isWrite bool) error
if watch.Mode != WatchModeNone {
return nil
}
return watchRepoMode(ctx, watch, WatchModeAuto)
return watchRepoByMode(ctx, watch, WatchModeAuto)
}
// ClearRepoWatches clears all watches for a repository and from the user that watched it.
+14 -14
View File
@@ -19,13 +19,13 @@ import (
func TestIsWatching(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
assert.True(t, repo_model.IsWatching(t.Context(), 1, 1))
assert.True(t, repo_model.IsWatching(t.Context(), 4, 1))
assert.True(t, repo_model.IsWatching(t.Context(), 11, 1))
assert.True(t, repo_model.IsWatchingRepo(t.Context(), 1, 1))
assert.True(t, repo_model.IsWatchingRepo(t.Context(), 4, 1))
assert.True(t, repo_model.IsWatchingRepo(t.Context(), 11, 1))
assert.False(t, repo_model.IsWatching(t.Context(), 1, 5))
assert.False(t, repo_model.IsWatching(t.Context(), 8, 1))
assert.False(t, repo_model.IsWatching(t.Context(), unittest.NonexistentID, unittest.NonexistentID))
assert.False(t, repo_model.IsWatchingRepo(t.Context(), 1, 5))
assert.False(t, repo_model.IsWatchingRepo(t.Context(), 8, 1))
assert.False(t, repo_model.IsWatchingRepo(t.Context(), unittest.NonexistentID, unittest.NonexistentID))
}
func TestGetWatchers(t *testing.T) {
@@ -109,7 +109,7 @@ func TestWatchIfAuto(t *testing.T) {
assert.Len(t, watchers, prevCount+1)
// Should remove watch, inhibit from adding auto
assert.NoError(t, repo_model.WatchRepo(t.Context(), user12, repo, false))
assert.NoError(t, repo_model.WatchRepoAuto(t.Context(), user12, repo, false))
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, prevCount)
@@ -145,7 +145,7 @@ func TestWatchOptions(t *testing.T) {
// repo 1 is watched by users 1, 4, 9 and 11, all with every event enabled
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
assert.NoError(t, repo_model.SetWatchOptions(t.Context(), user.ID, repo.ID, repo_model.WatchOptions{PullRequests: true}))
assert.NoError(t, repo_model.WatchRepoWithOptions(t.Context(), user, repo, repo_model.WatchOptions{Mode: repo_model.WatchModeNormal, WatchPullRequests: true}))
for watchType, expected := range map[repo_model.WatchType][]int64{
repo_model.WatchPullRequests: {1, 4, 9, 11},
@@ -160,11 +160,11 @@ func TestWatchOptions(t *testing.T) {
// the options of one user must not show up for another
watches, err := repo_model.GetUserWatches(t.Context(), 4, []int64{repo.ID})
assert.NoError(t, err)
assert.True(t, watches[repo.ID].Issues)
assert.True(t, watches[repo.ID].IncludeIssues)
// watching again resets a custom selection
assert.NoError(t, repo_model.WatchRepo(t.Context(), user, repo, false))
assert.NoError(t, repo_model.WatchRepo(t.Context(), user, repo, true))
assert.NoError(t, repo_model.WatchRepoAuto(t.Context(), user, repo, false))
assert.NoError(t, repo_model.WatchRepoAuto(t.Context(), user, repo, true))
watch, err := repo_model.GetWatch(t.Context(), user.ID, repo.ID)
assert.NoError(t, err)
assert.True(t, watch.IsWatchingAll())
@@ -172,9 +172,9 @@ func TestWatchOptions(t *testing.T) {
func TestWatchSelectedMode(t *testing.T) {
// a user without a watch row gets the dummy record, whose flags are the column defaults
assert.Equal(t, "participate", (&repo_model.Watch{Mode: repo_model.WatchModeNone, PullRequests: true, Issues: true, Releases: true}).SelectedMode())
assert.Equal(t, "participate", (&repo_model.Watch{Mode: repo_model.WatchModeNone, IncludePullRequests: true, IncludeIssues: true, IncludeReleases: true}).SelectedMode())
assert.Equal(t, "participate", (&repo_model.Watch{Mode: repo_model.WatchModeNormal}).SelectedMode())
assert.Equal(t, "ignore", (&repo_model.Watch{Mode: repo_model.WatchModeDont}).SelectedMode())
assert.Equal(t, "custom", (&repo_model.Watch{Mode: repo_model.WatchModeNormal, Issues: true}).SelectedMode())
assert.Equal(t, "all", (&repo_model.Watch{Mode: repo_model.WatchModeAuto, PullRequests: true, Issues: true, Releases: true}).SelectedMode())
assert.Equal(t, "custom", (&repo_model.Watch{Mode: repo_model.WatchModeNormal, IncludeIssues: true}).SelectedMode())
assert.Equal(t, "all", (&repo_model.Watch{Mode: repo_model.WatchModeAuto, IncludePullRequests: true, IncludeIssues: true, IncludeReleases: true}).SelectedMode())
}

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