Compare commits

..

42 Commits

Author SHA1 Message Date
bircni 7641fc3a8c fix(actions): restore pushes to protected branches (#39564)
Use the Actions token's loaded write permission when checking
protected-branch pushes. Preserve push and force-push allowlists and add
regression coverage.

Fixes https://github.com/go-gitea/gitea/issues/39563
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Giteabot <teabot@gitea.io>
2026-10-03 19:54:55 +02:00
bircni cca466caae fix(api): allow bots with pending password changes (#39551)
Allow bot accounts to use the API when a legacy password-change flag is
set, since bots cannot complete the interactive password-change flow.
Preserve password-change enforcement for human accounts and restrictions
for inactive or prohibited accounts.

Fixes: https://github.com/go-gitea/gitea/issues/39542
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-10-03 17:22:07 +00:00
silverwind 516a4883fa enhance(ui): cleanup navbar template and styling (#39554)
Clean up the navbar template and styling, and fix the navbar stopwatch,
which navigated to the issue instead of opening its popup since
https://github.com/go-gitea/gitea/pull/36965.

1. Add hover background to the create and user menus
2. Simplify navbar HTML and CSS and remove Fomantic styles
3. Render the notification and stopwatch icons once instead of separate
mobile and desktop copies
4. Make the stopwatch a keyboard accessible button whose popup updates
on push and closes when the stopwatch stops

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: bircni <bircni@icloud.com>
2026-10-03 18:35:54 +02:00
Timur Moziev f65e01226a enhance(webhook): add select all and deselect all buttons for custom events (#35980)
Adds `Select All` and `Deselect All` buttons to the custom events
section of the webhook form. This is useful when you need all events but
one.

- The buttons toggle every event checkbox and keep the unsaved-changes
prompt working
- Fix the "Trigger On" radio spacing by removing a leftover Fomantic
checkbox margin

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-10-03 14:27:17 +00:00
okxint ab1979bcfb fix(api): normalize all API due dates to end of day (#38677)
Due dates set through the API were stored inconsistently. The create
endpoints kept the raw timestamp, issue and pull request edits used end
of day in the client's offset, and only the deadline and milestone edit
endpoints normalized to end of day in the server's UI timezone like the
web UI does. All API due dates now go through
`ParseAPIDeadlineToEndOfDay`.

Editing a pull request with `unset_due_date: false` and no `due_date`
dereferenced a nil pointer and returned 500. It now shares the issue
edit logic and returns 400.

Related to https://github.com/go-gitea/gitea/issues/37620, which is
about the web sidebar and isn't fixed here.

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-10-03 11:59:43 +00:00
silverwind ad38b60983 refactor!: remove go-git backend (#39487)
Removes the go-git backend so every build uses the git CLI backend. Its
Windows performance advantage is gone, it lacks SHA-256 support, and it
breaks repositories on Windows.

The performance changes moved to
https://github.com/go-gitea/gitea/pull/39526.

Fixes https://github.com/go-gitea/gitea/issues/38359
Fixes https://github.com/go-gitea/gitea/issues/34694

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-10-03 10:04:24 +00:00
silverwind 3932624947 ci: relay fork PR reviews to giteabot through workflow_run (#39546)
`pull_request_review` runs on fork PRs, which includes all backport PRs,
get a read-only token and no secrets, so giteabot cannot write lgtm
labels and statuses there. A no-op `giteabot-review` workflow now
triggers giteabot through `workflow_run`, which gets both. This allows
retiring the legacy fly.io webhook bot.

Part of https://github.com/go-gitea/giteabot/issues/15
2026-10-03 09:36:38 +00:00
kiara e0e18fc286 perf(actions): index action_run.commit_sha (#39559)
The API filter `head_sha` on `GET /repos/{owner}/{repo}/actions/runs`
selects runs by `commit_sha`, so `commit_sha` needs an index. For a
action_run table with 212k rows:

- Without the index: the query read 212k rows, and the API request took
35-52 s.
- With the index: the query read 94 rows in 0.14 s, and the API request
took 2-4 s.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-10-03 11:19:27 +02:00
silverwind 457510fa09 fix: use READ_COMMITTED_SNAPSHOT on MSSQL (#39512)
MSSQL's default READ COMMITTED makes reads wait on writers, so the
runner pickup deadlocks with concurrent claims, flaking
`TestCreateTaskForRunnerConcurrentClaim`.

- Enable `READ_COMMITTED_SNAPSHOT` on MSSQL so it reads like PostgreSQL
and MySQL
- Read the pickup cursor before claiming, a lost claim could skip
waiting jobs
- Add tests that fail without consistent READ COMMITTED

Performance: Writes on MSSQL now also store the previous row version in
tempdb, the same versioning cost PostgreSQL and MySQL always pay, and
Azure SQL enables it by default. Reads no longer block on writers, and a
32-runner pickup stress test ran 2.5x faster with it.

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
2026-10-03 15:25:47 +08:00
silverwind 2baa5a16f8 enhance(citation): render citations server-side (#39373)
Replace citation-js with a Go port of ruby-cff, which GitHub uses for
"Cite this repository", rendering APA and BibTeX server-side and
dropping about 770KB of JS. Output matches GitHub on 1568 of 1571
real-world files, the rest are improvements over GitHub.

- `CITATION.cff` wins over `CITATION.bib`, matched case-insensitively
and through symlinks
- `CITATION.bib` is offered as-is, without APA

Signed-off-by: silverwind <me@silverwind.io>
2026-10-03 02:28:34 +00:00
silverwind fb067e1115 fix(git): tolerate concurrent repacks in go-git storage (#39536)
Since `transfer.fsckObjects` makes fetches keep a pack, git 2.54+
background maintenance repacks a mirror right after its sync fetch, and
go-git then misses objects mid-repack. Fixes these flakes:

-
https://github.com/go-gitea/gitea/actions/runs/36875305717/job/110419770196
-
https://github.com/go-gitea/gitea/actions/runs/36900439473/job/110498845882

Changes:

- Keep reindexing while the pack set changes instead of retrying once
- List packs only once their `.idx` exists and don't fail the listing on
files removed mid-repack
- Look up large objects again when their file is gone before reading
2026-10-03 03:58:36 +02:00
GiteaBot daaed0d7f4 [skip ci] Updated translations via Crowdin 2026-10-03 00:55:34 +00:00
silverwind 99573bde0e chore(lint): apply the main eslint config to vue files (#39545)
Vue files only got `eslint-plugin-vue` rules, so stylistic and
TypeScript rules never ran on them, and `import-x` could not see cycles
between `.ts` and `.vue` files.

- Apply the main ESLint config to `.vue` files
- Let `import-x` parse `.vue` files, which surfaced a cycle between
`repo-findfile.ts` and `RepoFileSearch.vue`
- Fix the resulting lint issues

`vue-eslint-parser` is added as a direct dependency because `import-x`
resolves parsers by package name, see
https://github.com/un-ts/eslint-plugin-import-x/issues/381.
2026-10-03 00:47:13 +00:00
Copilot 986b0bcae0 fix(markup): use installed math fonts for MathML in Chromium (#39491)
On Linux and ChromeOS, Chromium resolves the `math` font family to Latin
Modern Math, which neither installs, so MathML renders with a text font
and brackets and large operators don't stretch, see
https://issues.chromium.org/issues/40069293. The new `--fonts-math`
variable keeps `math` first, so browsers that always resolve it keep
their font. Only Chromium falls through to the math fonts Linux and
ChromeOS install by default:

- `STIX Two Math`: Fedora, and `fonts-stix` on Ubuntu 26.04
- `DejaVu Math TeX Gyre`: Debian 13 and openSUSE
- `Noto Sans Math`: Fedora and ChromeOS, last because Debian and Ubuntu
ship an older version without a `MATH` table

Math fonts also have smaller x-heights than UI fonts, so MathML rendered
smaller than KaTeX and the surrounding text in every browser, see
https://github.com/w3c/mathml-core/issues/41. `font-size-adjust:
ex-height 0.52` scales whichever math font is used to KaTeX's x-height.
KaTeX output is unchanged.

Fixes: https://github.com/go-gitea/gitea/issues/39489
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: bircni <75789103+bircni@users.noreply.github.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-10-02 20:03:22 +00:00
Lunny Xiao 6dad3ad43d refactor: only update sync status columns when syncing push mirror (#39517)
`UpdatePushMirror` used `AllCols()`, so a sync could overwrite columns
changed concurrently (e.g. `interval`) with stale values. It now updates
only `last_update` and `last_error`, and is renamed to
`UpdatePushMirrorSyncStatus` to match.

Co-authored-by: Giteabot <teabot@gitea.io>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 5) <noreply@anthropic.com>
2026-10-02 19:28:51 +00:00
silverwind cf89ecd887 refactor!: move go-chi/session into Gitea (#39504)
The `gitea.com/go-chi/session` package only exists for Gitea, so it
moves into `modules/session` to fix its bugs directly. Fixes the flake
in
https://github.com/go-gitea/gitea/actions/runs/36726154500/job/109923538400.

- Sessions are only written back when changed, so a read-only request
can't revert a concurrent change or restore a logged-out session, like
https://github.com/go-macaron/session/commit/ae808a4a4660c802965c834299ab08f167effd12
- The session cookie is only set once a session holds data
- Every backend refreshes the expiry on load and file sessions are
written atomically
- Also fix  https://github.com/go-gitea/gitea/issues/36176

## ⚠️ BREAKING ⚠️

* the `mysql`, `postgres`, `couchbase` and `memcache` session providers
are removed, use `file`, `db` or `redis` instead
* login-related cookies are renamed to `gitea_session` and
`gitea_remember`, if you'd like to use the old names, set `COOKIE_NAME`
and `COOKIE_REMEMBER_NAME` in app.ini

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-10-02 21:08:14 +02:00
silverwind bea6fcaa84 refactor: move go-chi/cache into Gitea (#39530)
The `gitea.com/go-chi/cache` package only exists for Gitea, so it moves
into `modules/cache`.

- `ITEM_TTL = -1` disables caching as documented
- Sub-second TTLs round up instead of never expiring
- The Redis adapter no longer grows a `MacaronCache` hash
- Use two-queue cache for in-memory cache

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-10-02 18:16:34 +02:00
silverwind 873efed5a6 refactor: move go-chi/captcha into Gitea (#39529)
The `gitea.com/go-chi/captcha` package only exists for Gitea,
so it moves into `modules/imagecaptcha`.

- Reloading an expired challenge shows a new image instead of a broken one
- Every answer is consumed on its first check
- OpenID registration stops after a failed captcha

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-10-02 22:21:41 +08:00
silverwind e6ffbea888 refactor: move go-chi/binding into Gitea (#39528)
The `gitea.com/go-chi/binding` package only exists for Gitea, so it
moves into `modules/web/binding` to fix its bugs directly. Split out of
https://github.com/go-gitea/gitea/pull/39504.

- GET and HEAD always bind the query
- JSON `null` slice elements and nested `TrimSpace` fields bind
correctly
- Integer fields reject out-of-range values instead of wrapping
- An empty JSON body binds nothing and an unknown binding rule is an
error

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: bircni <bircni@icloud.com>
2026-10-02 15:37:41 +02:00
silverwind 2f01ec38bd fix: use READ COMMITTED transactions on MySQL and MariaDB (#39506)
MariaDB 11.6.2+ defaults `innodb_snapshot_isolation` to `ON`, which
fails REPEATABLE READ transactions with error 1020 when a row they write
changed after their first read. Gitea's background work like push
processing writes the same rows, so merges, issue closes and workflow
runs fail sporadically.

- Use READ COMMITTED on MySQL and MariaDB, like PostgreSQL and MSSQL
- Update xorm to v1.4.3

Replaces: https://github.com/go-gitea/gitea/pull/39494
Fixes: https://github.com/go-gitea/gitea/issues/39492

---------

Signed-off-by: silverwind <me@silverwind.io>
2026-10-01 19:55:58 +00:00
Calvin Tjoaquinn fc81f2832a fix(git): avoid unnecessary timers during language stats (#39531)
Replace time.After with a stoppable time.Timer in BatchChecker.CheckPath.
Avoid accumulating up to 6 pending 5-second timers per file on the normal path.
Preserve the existing per-attribute timeout behavior.

---------

Signed-off-by: Calvin Tjoaquinn <calvintjoa23@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-10-01 17:34:38 +00:00
silverwind 075ff8f516 perf(git): speed up activity top authors and subdirectory listings (#39526)
Speeds up two slow git paths. Results match `main` apart
from the `log.follow` fix.

- Activity top authors skip `--numstat` and an unused `rev-list
--count`, they only need names and emails
- Subdirectory listings pass only the directory as pathspec, which
already matches all its entries
- Directory listings pass `--no-follow` to `git log`, a configured
`log.follow` disabled parent rewriting and gave wrong last commits

| Benchmark | main | PR | Change |
|---|--:|--:|--:|
| Top authors, gitea, 1 month | 632 ms | 21 ms | -97% |
| Top authors, gitea, 1 year | 3216 ms | 68 ms | -98% |
| Top authors, tea | 62 ms | 12 ms | -80% |
| Listing, git `Documentation/technical` | 189 ms | 91 ms | -52% |
| Listing, gitea `options/license` | 329 ms | 208 ms | -37% |
| Listing, gitea `templates/repo` | 294 ms | 223 ms | -24% |

Tested with unit and sqlite integration tests in default and `gogit`
builds on git 2.25 and 2.56, and by comparing listing results and cache
writes with `main` on randomized histories and the gitea, tea and git
repos. Benchmarks are medians of 8 interleaved macOS runs.
2026-10-01 15:51:07 +00:00
silverwind 2f5cdbd5c1 fix(git): reindex go-git storage when a concurrent repack removes packs (#39510)
Improve the go-git workaround to fix these flakes:

- https://github.com/go-gitea/gitea/actions/runs/36721877142/job/109908823684
- https://github.com/go-gitea/gitea/actions/runs/36799665163/job/110170983591
2026-10-01 11:04:41 +00:00
Jon Fuller aae0a218c3 fix(api): add index tiebreaker to commit status ordering (#39508)
Commit status list orders only by `created_unix`/`updated_unix`, which
have 1-second resolution while CI often posts many statuses per second.
With LIMIT/OFFSET paging, databases (e.g. PostgreSQL using a Sort plan)
may order tied rows differently per page, so `GET
/repos/{owner}/{repo}/commits/{ref}/statuses` returns some statuses
twice and never returns others.

This became visible after https://github.com/go-gitea/gitea/pull/36521
made requests without `page` paginated. Clients like Renovate that page
until `X-Total-Count` can miss a context's newest status and see a stale
`pending`, blocking automerge.

Fix: add `index` (unique per commit) as a tiebreaker to the
timestamp-based orders.

Co-authored-by: silverwind <me@silverwind.io>
2026-10-01 10:43:01 +00:00
wxiaoguang 9b5c87a6b6 fix: trace git command correctly (#39520)
Help  #39410
2026-10-01 10:01:45 +00:00
silverwind 51b93d1d27 enhance(packages/npm): improve npm client compatibility (#39434)
Aligns the npm registry with what npm, pnpm and yarn expect:

1. Raise the publish body cap from
https://github.com/go-gitea/gitea/pull/37890 to 256 MiB like npmjs,
larger bodies get 413
2. Pick the tarball attachment by name, `npm publish --provenance`
failed at random
3. Store and serve `libc`, so mismatched glibc/musl optional binaries
are skipped
4. Treat root `*.gyp` files as an install script, like npm does
5. Always serve a `latest` dist-tag, yarn and pnpm fail without it
6. Take top-level metadata from `latest` and drop the per-version readme
7. Serve tarballs at the npmjs path `/<name>/-/<file>`, former URLs keep
working
8. Add ETag revalidation for metadata, `npm ping` and `npm whoami`

Tested with npm 12.1, pnpm 12.4, yarn 1.22 and yarn 4.18.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-10-01 07:50:47 +00:00
Roshan Ramani f3aed8b81d docs: fix the default REPOSITORY_AVATAR_FALLBACK_IMAGE path in app.example.ini (#39514)
The example shows `REPOSITORY_AVATAR_FALLBACK_IMAGE =
/img/repo_default.png`, but public files moved under `/assets` in
https://github.com/go-gitea/gitea/pull/15219 and nothing serves `/img/`
anymore. The value is also used as-is without the sub-path, so even
`/assets/img/repo_default.png` 404s when `ROOT_URL` has one. Leave the
key empty and document the real default
`{AppSubURL}/assets/img/repo_default.png` from
`modules/setting/picture.go`.

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-10-01 09:31:10 +02:00
Harsh Sharma fc68608603 fix(oauth2): allow users to approve scope changes (#38942)
Lets users approve an OAuth2 scope change on an existing grant instead
of failing with `a grant exists with different scope`.

- Approving a different scope updates the existing grant. Issued tokens
follow immediately, since their scope is read from the grant.
- Confidential and trusted apps show the consent page when the scope set
changes, instead of silently reusing the old grant.
- An omitted `scope` reuses the existing grant's scope, like GitHub.
- The consent page lists newly added scopes.

Fixes: https://github.com/go-gitea/gitea/issues/38940
Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: Giteabot <teabot@gitea.io>
Co-authored-by: silverwind <me@silverwind.io>
2026-10-01 09:08:33 +02:00
wxiaoguang fe31237fd8 fix: handle git branch name with special chars correctly (#39483)
Fix the bugs:
* Commit graph page doesn't show
* PR command line instructions are wrong

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-10-01 04:01:16 +00:00
Zettat123 a71c5c94c5 fix(actions): reject jobs without runs-on (#39480)
Align job and `runs-on` validation with github.com, as implemented by
the parser in https://github.com/actions/runner. A job without `runs-on`
could be claimed by any runner, so a job meant for a container could run
on the host.

- Jobs without `runs-on` fail with `Required property is missing:
runs-on`, called workflows included
- Unknown job keys and callers (`uses:`) mixed with steps-only keys like
`runs-on` are rejected
- Empty, null and nested `runs-on` values are rejected
- A `runs-on` evaluating to such a value fails only that job
- Called workflows are validated at run creation, an invalid one fails
the run as an invalid workflow file
- Zero labels (`runs-on: []` or `{}`) never match a runner, including
jobs queued before upgrading

<img width="960" alt="image"
src="https://github.com/user-attachments/assets/e746ce5a-b711-4e8b-aab8-81336ff53d86"
/>

**Behavior Change:** workflows that omit `runs-on`, use unknown job keys
or mix `uses` with `runs-on` stop running until fixed.

---------

Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-09-30 21:42:07 -06:00
GiteaBot f81a2ab69a [skip ci] Updated translations via Crowdin 2026-10-01 01:08:18 +00:00
Zain Qureshi b0d6cc1d22 docs: correct three stale defaults in app.example.ini (#39500)
Three commented defaults in `custom/conf/app.example.ini` differ from
what Gitea actually uses, so the file says they default to something
else:

- `MINIMUM_KEY_SIZE_CHECK`: example `false`, code `true`
(`modules/setting/ssh.go:55`, read at `:152`).
- `SSH_SERVER_HOST_KEYS`: example lists `ssh/gitea.rsa, ssh/gogs.rsa`,
code also loads `ssh/gitea.ed25519` and `ssh/gitea.ecdsa`
(`modules/setting/ssh.go:57`).
- `[queue] DATADIR`: example `queues/`, code `queues/common`
(`modules/setting/queue.go:33`), which the comment above it already
states.

Co-authored-by: silverwind <me@silverwind.io>
2026-09-30 15:49:22 -07:00
silverwind 8936303510 fix: add missing checks to several API and web handlers (#39501)
Several handlers skipped checks that their sibling routes or settings already enforce. This brings them in line.

- Push mirror API honors `DISABLE_NEW_PUSH` and checks the caller's permission
- Media API serves small files with the usual content headers
- Issue attachment API ignores comment attachments
- Push-to-create respects `FORCE_PRIVATE`
- Profile feeds and follow actions respect `ENABLE_FEED` and owner visibility
- Tag delete route refuses release tags
- Refresh token grant only accepts refresh tokens
- Gitea migrations bound the source's page size

Co-authored-by: bircni <bircni@icloud.com>
2026-09-30 20:25:14 +02:00
Zettat123 3d085dbaf1 feat(admin): show and filter users by authentication source (#38900) 2026-09-30 11:28:35 -06:00
Nico Schlömer 9b2a3c267b fix(markup): don't escape ambiguous characters in MathML (#39493)
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-09-30 16:37:30 +00:00
silverwind 590d2984d9 fix(ui): ignore code line anchors below 1, show JS errors in vite dev mode (#39432) 2026-09-30 15:03:40 +00:00
Nico Schlömer 61e0343580 fix(markup): skip post-processing inside MathML (#39497)
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-09-30 16:41:41 +02:00
wxiaoguang 0b43bde974 fix: copy new access token to clipboard (#39496)
Co-authored-by: silverwind <me@silverwind.io>
2026-09-30 21:41:55 +08:00
wxiaoguang cc95f141f8 fix: npm route (#39488) 2026-09-30 19:49:50 +08:00
Roshan Ramani a25fbd43c4 docs: update app.example.ini for defaults changed in #39400 (#39456)
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-09-30 11:28:59 +00:00
JerryLien f365a6b9c8 fix(actions): keep runs order after auto refresh (#39479)
On a repository's Actions tab, runs are sorted newest first on initial
page load. After the first auto refresh (added in #38329, every 3
seconds while runs are active and every 12 seconds otherwise), the same
runs may appear in a different order and move again as their status
changes.

Example with four runs (Gitea 28.0.0, SQLite):

```
page load:     #10 success, #9 failure, #8 success, #7 running
after refresh: #8 success, #10 success, #9 failure, #7 running
```

To reproduce, open the Actions tab of a repository with runs in
different statuses and wait for an auto refresh. On SQLite, the runs may
be regrouped by status, with each group ordered oldest first.

`preparePartialRefreshRuns` reloads the runs currently shown on the page
using `GetRunsByRepoAndID`. That query has no `ORDER BY`, while the
initial page load uses `FindRunOptions.ToOrders` and sorts by index
descending.

With SQLite, the query planner used the `(repo_id, status)` index, so
the returned row order differed from the original page order. Since the
query has no explicit ordering, this behavior is database-dependent. I
have not tested MySQL or PostgreSQL.

This change orders `GetRunsByRepoAndID` by index descending, the same
order `FindRunOptions.ToOrders` uses for the initial page load. The
refresh only reloads the runs already on the page, so they come back in
the original order, with or without filters and on any page.

The other caller of `GetRunsByRepoAndID`, run approval, does not depend
on result ordering.

Testing:

- Added `TestPreparePartialRefreshRunsKeepsRequestedOrder`. Without the
fix, runs 794, 793, 792, 791 are returned as 791, 792, 794, 793; with
the fix, the test passes.
- `go test` passes for `./routers/web/repo/actions/`,
`./models/actions/` and `./services/actions/`.
- `go vet` and `golangci-lint v2.13.2` pass for the changed packages.
- Manually tested by building Gitea 28.0.0 with this patch and running
it on our SQLite instance. The runs list keeps its newest-first order
across auto refreshes. The official 28.0.0 binary reproduces the
reordering.

AI-assisted: drafted with Claude Code (claude-opus-5-5), reviewed by me.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-09-30 09:17:04 +02:00
bircni e0095af8c3 ci: Also release for other versions than 1 majors (#39475) 2026-09-29 21:47:12 +02:00
314 changed files with 6353 additions and 5673 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ runs:
env: env:
# pgsql is chosen to be the unlucky one to run with the slow "race detector", it is about 60% slower. # pgsql is chosen to be the unlucky one to run with the slow "race detector", it is about 60% slower.
GOTEST_FLAGS: -race -timeout=40m GOTEST_FLAGS: -race -timeout=40m
TAGS: bindata gogit TAGS: bindata
TEST_LDAP: 1 TEST_LDAP: 1
TEST_SHARD: ${{ inputs.shard }} TEST_SHARD: ${{ inputs.shard }}
TEST_TOTAL_SHARDS: ${{ inputs.total-shards }} TEST_TOTAL_SHARDS: ${{ inputs.total-shards }}
+1 -8
View File
@@ -37,22 +37,15 @@ jobs:
- uses: ./.github/actions/go-setup - uses: ./.github/actions/go-setup
- run: make deps-backend deps-tools - run: make deps-backend deps-tools
- run: TAGS="bindata" make backend - run: TAGS="bindata" make backend
- run: TAGS="bindata gogit" make backend
- name: warm test compile cache (bindata) - name: warm test compile cache (bindata)
env: env:
TAGS: bindata TAGS: bindata
GOTEST_FLAGS: -race -list=^$$ -count=1 GOTEST_FLAGS: -race -list=^$$ -count=1
run: make test-backend run: make test-backend
- name: warm test compile cache (bindata gogit)
env:
TAGS: bindata gogit
GOTEST_FLAGS: -race -list=^$$ -count=1
run: make test-backend
- name: warm integration compile cache - name: warm integration compile cache
run: | run: |
TAGS="bindata" make test-integration-compile TAGS="bindata" make test-integration-compile
TAGS="bindata gogit" make test-integration-compile TAGS="bindata" GOTEST_FLAGS="-race" make test-integration-compile
TAGS="bindata gogit" GOTEST_FLAGS="-race" make test-integration-compile
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
- uses: go-gitea/giteabot@4c9d4d3fd913b5c35f59dcc0b004a3d11d5b22bf # v1.0.7 - uses: go-gitea/giteabot@f48c6a15e0d384f037aea19cc05ff5e9551096b9 # v1.1.0
with: with:
github_token: ${{ secrets.GITEABOT_TOKEN }} github_token: ${{ secrets.GITEABOT_TOKEN }}
gitea_fork: giteabot/gitea gitea_fork: giteabot/gitea
+19
View File
@@ -0,0 +1,19 @@
name: giteabot-review
# Relays PR reviews to giteabot.yml through its workflow_run trigger, because review
# runs on fork PRs get no secrets and a read-only token. The job itself does nothing.
on:
pull_request_review:
types:
- submitted
- edited
- dismissed
permissions: {}
jobs:
relay:
runs-on: ubuntu-latest
steps:
- run: "true"
+9 -12
View File
@@ -20,13 +20,12 @@ on:
- closed - closed
- review_requested - review_requested
- review_request_removed - review_request_removed
# Review events keep review-derived state such as lgtm labels and status checks # Reviews arrive through giteabot-review because fork PR review runs get no secrets
# in sync after approvals, edits, or dismissals. workflow_run:
pull_request_review: workflows:
- giteabot-review
types: types:
- submitted - requested
- edited
- dismissed
# Periodic maintenance is still useful as a backstop for queue cleanup and # Periodic maintenance is still useful as a backstop for queue cleanup and
# other housekeeping, even though main pushes now trigger it promptly. # other housekeeping, even though main pushes now trigger it promptly.
schedule: schedule:
@@ -43,12 +42,12 @@ on:
permissions: {} permissions: {}
concurrency: concurrency:
group: ${{ format('{0}-{1}', github.workflow, (github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review') && format('pr-{0}', github.event.pull_request.number) || 'maintenance') }} group: ${{ format('{0}-{1}', github.workflow, github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number) || github.event_name == 'workflow_run' && format('review-{0}', github.event.workflow_run.head_sha) || 'maintenance') }}
cancel-in-progress: false cancel-in-progress: false
jobs: jobs:
giteabot: giteabot:
if: github.repository == 'go-gitea/gitea' if: github.repository == 'go-gitea/gitea' && (github.event_name != 'workflow_run' || github.event.workflow_run.event == 'pull_request_review')
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30 timeout-minutes: 30
permissions: permissions:
@@ -57,9 +56,7 @@ jobs:
pull-requests: write pull-requests: write
statuses: write statuses: write
steps: steps:
# pull_request_review runs without repository secrets on fork PRs, so fall - uses: go-gitea/giteabot@f48c6a15e0d384f037aea19cc05ff5e9551096b9 # v1.1.0
# back to the workflow token for the non-backport checks handled here.
- uses: go-gitea/giteabot@4c9d4d3fd913b5c35f59dcc0b004a3d11d5b22bf # v1.0.7
with: with:
github_token: ${{ secrets.GITEABOT_TOKEN || github.token }} github_token: ${{ secrets.GITEABOT_TOKEN }}
checks: ${{ github.event.inputs.checks || 'labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }} checks: ${{ github.event.inputs.checks || 'labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }}
+2 -2
View File
@@ -93,13 +93,13 @@ jobs:
env: env:
GOOS: linux GOOS: linux
GOARCH: arm64 GOARCH: arm64
TAGS: bindata gogit TAGS: bindata
- name: build-backend-windows - name: build-backend-windows
run: go build -ldflags '-s -w' -o gitea-windows run: go build -ldflags '-s -w' -o gitea-windows
env: env:
GOOS: windows GOOS: windows
GOARCH: amd64 GOARCH: amd64
TAGS: bindata gogit TAGS: bindata
- name: build-backend-386 - name: build-backend-386
run: go build -ldflags '-s -w' -o gitea-linux-386 run: go build -ldflags '-s -w' -o gitea-linux-386
env: env:
+3 -10
View File
@@ -95,17 +95,17 @@ jobs:
- run: make deps-backend - run: make deps-backend
- run: make backend - run: make backend
env: env:
TAGS: bindata gogit TAGS: bindata
- run: GITEA_TEST_DATABASE=sqlite make test-migration - run: GITEA_TEST_DATABASE=sqlite make test-migration
env: env:
TAGS: bindata gogit TAGS: bindata
- name: run tests - name: run tests
run: GITEA_TEST_DATABASE=sqlite make test-integration run: GITEA_TEST_DATABASE=sqlite make test-integration
timeout-minutes: 50 timeout-minutes: 50
env: env:
# sqlite driver can contain large amount of Golang code, so don't use race detector for it, otherwise, extremely slow # sqlite driver can contain large amount of Golang code, so don't use race detector for it, otherwise, extremely slow
GOTEST_FLAGS: -timeout=40m GOTEST_FLAGS: -timeout=40m
TAGS: bindata gogit TAGS: bindata
test-unit: test-unit:
if: needs.files-changed.outputs.backend == 'true' if: needs.files-changed.outputs.backend == 'true'
@@ -163,13 +163,6 @@ jobs:
GOTEST_FLAGS: -race -timeout=20m GOTEST_FLAGS: -race -timeout=20m
TAGS: bindata TAGS: bindata
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }} GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
- name: unit-tests-gogit
run: make test-backend
env:
GOTEST_FLAGS: -race -timeout=20m
TAGS: bindata gogit
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
GITEA_TEST_CI_SKIP_EXTERNAL: true
- run: make test-check - run: make test-check
test-mysql: test-mysql:
+1
View File
@@ -12,6 +12,7 @@
- In `options/locale`, only edit `locale_en-US.json`, other locales are synced automatically - In `options/locale`, only edit `locale_en-US.json`, other locales are synced automatically
- In TS, use `!` instead of `?.`/`??` when a value always exists - In TS, use `!` instead of `?.`/`??` when a value always exists
- In Go, prefer to use modern language features wherever possible - In Go, prefer to use modern language features wherever possible
- In Go, function-name prefixes in errors like `fmt.Errorf("Foo: %w", err)` must always name the function they are in
- Write sizes as multiplications like `64 * 1024`, not bit shifts like `64 << 10` - Write sizes as multiplications like `64 * 1024`, not bit shifts like `64 << 10`
- Prefer `tw-*` utilities over inline `style` and `flex-*` helpers over per-child `tw-ml-*`/`tw-mr-*` margins, falling back to `tw-*` where specificity requires `!important` - Prefer `tw-*` utilities over inline `style` and `flex-*` helpers over per-child `tw-ml-*`/`tw-mr-*` margins, falling back to `tw-*` where specificity requires `!important`
- Run `make fmt` after `.go` edits, `make tidy` after `go.mod` edits, `make generate-swagger` after API changes, and lint what changed with `make lint-go`, `lint-js`, `lint-css` or `lint-templates` - Run `make fmt` after `.go` edits, `make tidy` after `go.mod` edits, `make generate-swagger` after API changes, and lint what changed with `make lint-go`, `lint-js`, `lint-css` or `lint-templates`
-45
View File
File diff suppressed because one or more lines are too long
+2 -8
View File
@@ -13,13 +13,11 @@ import (
"gitea.dev/models/db" "gitea.dev/models/db"
"gitea.dev/modules/dump" "gitea.dev/modules/dump"
"gitea.dev/modules/json"
"gitea.dev/modules/log" "gitea.dev/modules/log"
"gitea.dev/modules/setting" "gitea.dev/modules/setting"
"gitea.dev/modules/storage" "gitea.dev/modules/storage"
"gitea.dev/modules/util" "gitea.dev/modules/util"
"gitea.com/go-chi/session"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
@@ -249,12 +247,8 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
log.Info("Packing data directory...%s", setting.AppDataPath) log.Info("Packing data directory...%s", setting.AppDataPath)
var excludes []string var excludes []string
if setting.SessionConfig.OriginalProvider == "file" { if setting.SessionConfig.Provider == "file" {
var opts session.Options excludes = append(excludes, setting.SessionConfig.ProviderConfig)
if err = json.Unmarshal([]byte(setting.SessionConfig.ProviderConfig), &opts); err != nil {
return err
}
excludes = append(excludes, opts.ProviderConfig)
} }
if cmd.IsSet("skip-index") && cmd.Bool("skip-index") { if cmd.IsSet("skip-index") && cmd.Bool("skip-index") {
+21 -26
View File
@@ -155,7 +155,7 @@
;; Username to use for the builtin SSH server. If blank, then it is the value of RUN_USER. ;; Username to use for the builtin SSH server. If blank, then it is the value of RUN_USER.
;BUILTIN_SSH_SERVER_USER = ;BUILTIN_SSH_SERVER_USER =
;; ;;
;; Domain name to be exposed in clone URL, defaults to DOMAIN or the domain part of ROOT_URL ;; Domain name to be exposed in clone URL, defaults to the domain part of ROOT_URL
;SSH_DOMAIN = ;SSH_DOMAIN =
;; ;;
;; Port number to be exposed in clone URL. ;; Port number to be exposed in clone URL.
@@ -198,7 +198,7 @@
;; For the built-in SSH server, choose the keypair to offer as the host key ;; For the built-in SSH server, choose the keypair to offer as the host key
;; The private key should be at SSH_SERVER_HOST_KEY and the public SSH_SERVER_HOST_KEY.pub ;; The private key should be at SSH_SERVER_HOST_KEY and the public SSH_SERVER_HOST_KEY.pub
;; relative paths are made absolute relative to the APP_DATA_PATH ;; relative paths are made absolute relative to the APP_DATA_PATH
;SSH_SERVER_HOST_KEYS=ssh/gitea.rsa, ssh/gogs.rsa ;SSH_SERVER_HOST_KEYS=ssh/gitea.rsa, ssh/gitea.ed25519, ssh/gitea.ecdsa, ssh/gogs.rsa
;; ;;
;; Enable SSH Authorized Key Backup when rewriting all keys, default is false ;; Enable SSH Authorized Key Backup when rewriting all keys, default is false
;SSH_AUTHORIZED_KEYS_BACKUP = false ;SSH_AUTHORIZED_KEYS_BACKUP = false
@@ -237,7 +237,7 @@
;SSH_PER_WRITE_PER_KB_TIMEOUT = 30s ;SSH_PER_WRITE_PER_KB_TIMEOUT = 30s
;; ;;
;; Indicate whether to check minimum key size with corresponding type ;; Indicate whether to check minimum key size with corresponding type
;MINIMUM_KEY_SIZE_CHECK = false ;MINIMUM_KEY_SIZE_CHECK = true
;; ;;
;; TLS Settings: Either ACME or manual ;; TLS Settings: Either ACME or manual
;; (Other common TLS configuration are found before) ;; (Other common TLS configuration are found before)
@@ -459,7 +459,7 @@ INTERNAL_TOKEN =
;LOGIN_REMEMBER_DAYS = 31 ;LOGIN_REMEMBER_DAYS = 31
;; ;;
;; Name of cookie used to store authentication information. ;; Name of cookie used to store authentication information.
;COOKIE_REMEMBER_NAME = gitea_incredible ;COOKIE_REMEMBER_NAME = gitea_remember
;; ;;
;; URL or path that Gitea should redirect users to *after* performing its own logout. ;; URL or path that Gitea should redirect users to *after* performing its own logout.
;; Use this, if needed, when authentication is handled by a reverse proxy or SSO. ;; Use this, if needed, when authentication is handled by a reverse proxy or SSO.
@@ -780,8 +780,6 @@ LEVEL = Info
;; Respond to pushes to a non-default branch with a URL for creating a Pull Request (if the repository has them enabled) ;; Respond to pushes to a non-default branch with a URL for creating a Pull Request (if the repository has them enabled)
;PULL_REQUEST_PUSH_MESSAGE = true ;PULL_REQUEST_PUSH_MESSAGE = true
;; ;;
;; (Go-Git only) Don't cache objects greater than this in memory. (Set to 0 to disable.)
;LARGE_OBJECT_THRESHOLD = 1048576
;; Set to true to forcibly set core.protectNTFS=false ;; Set to true to forcibly set core.protectNTFS=false
;DISABLE_CORE_PROTECT_NTFS=false ;DISABLE_CORE_PROTECT_NTFS=false
;; Disable the usage of using partial clones for git. ;; Disable the usage of using partial clones for git.
@@ -836,7 +834,7 @@ LEVEL = Info
;EMAIL_DOMAIN_BLOCKLIST = ;EMAIL_DOMAIN_BLOCKLIST =
;; ;;
;; Disallow registration, only allow admins to create accounts. ;; Disallow registration, only allow admins to create accounts.
;DISABLE_REGISTRATION = false ;DISABLE_REGISTRATION = true
;; ;;
;; Allow registration only using gitea itself, it works only when DISABLE_REGISTRATION is false ;; Allow registration only using gitea itself, it works only when DISABLE_REGISTRATION is false
;ALLOW_ONLY_INTERNAL_REGISTRATION = false ;ALLOW_ONLY_INTERNAL_REGISTRATION = false
@@ -968,12 +966,11 @@ LEVEL = Info
;; Value for the domain part of the user's email address in the git log if user ;; Value for the domain part of the user's email address in the git log if user
;; has set KeepEmailPrivate to true. The user's email will be replaced with a ;; has set KeepEmailPrivate to true. The user's email will be replaced with a
;; concatenation of the user name in lower case, "@" and NO_REPLY_ADDRESS. Default ;; concatenation of the user name in lower case, "@" and NO_REPLY_ADDRESS. Default
;; value is "noreply." + DOMAIN, where DOMAIN resolves to the value from server.DOMAIN ;; value is "noreply." + the domain part of ROOT_URL
;; Note: do not use the <DOMAIN> notation below ;NO_REPLY_ADDRESS =
;NO_REPLY_ADDRESS = ; noreply.<DOMAIN>
;; ;;
;; Show Registration button ;; Show Registration button, defaults to true only if both DISABLE_REGISTRATION and ALLOW_ONLY_EXTERNAL_REGISTRATION are false
;SHOW_REGISTRATION_BUTTON = true ;SHOW_REGISTRATION_BUTTON = false
;; ;;
;; Show milestones dashboard page - a view of all the user's milestones ;; Show milestones dashboard page - a view of all the user's milestones
;SHOW_MILESTONES_DASHBOARD_PAGE = true ;SHOW_MILESTONES_DASHBOARD_PAGE = true
@@ -1670,13 +1667,13 @@ LEVEL = Info
;; ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; ;;
;; General queue queue type, currently support: persistable-channel, channel, level, redis, dummy ;; General queue type, currently support: level, channel, redis, dummy
;; default to persistable-channel ;; default to level
;TYPE = persistable-channel ;TYPE = level
;; ;;
;; data-dir for storing persistable queues and level queues, individual queues will default to `queues/common` meaning the queue is shared. ;; data-dir for storing level queues, individual queues will default to `queues/common` meaning the queue is shared.
;; Relative paths will be made absolute against "APP_DATA_PATH" ;; Relative paths will be made absolute against "APP_DATA_PATH"
;DATADIR = queues/ ;DATADIR = queues/common
;; ;;
;; Default queue length before a channel queue will block ;; Default queue length before a channel queue will block
;LENGTH = 100000 ;LENGTH = 100000
@@ -1684,7 +1681,7 @@ LEVEL = Info
;; Batch size to send for batched queues ;; Batch size to send for batched queues
;BATCH_LENGTH = 20 ;BATCH_LENGTH = 20
;; ;;
;; When `TYPE` is `persistable-channel`, this provides a directory for the underlying leveldb ;; When `TYPE` is `level`, this provides a directory for the underlying leveldb
;; or additional options of the form `leveldb://path/to/db?option=value&....`, and will override `DATADIR`. ;; or additional options of the form `leveldb://path/to/db?option=value&....`, and will override `DATADIR`.
;; When `TYPE` is `redis` and this is left empty, it falls back to the shared [redis] CONN_STR. ;; When `TYPE` is `redis` and this is left empty, it falls back to the shared [redis] CONN_STR.
;CONN_STR = ;CONN_STR =
@@ -1752,7 +1749,6 @@ LEVEL = Info
;ENABLE_OPENID_SIGNIN = false ;ENABLE_OPENID_SIGNIN = false
;; ;;
;; Whether to allow registering via OpenID ;; Whether to allow registering via OpenID
;; Do not include to rely on rhw DISABLE_REGISTRATION setting
;;ENABLE_OPENID_SIGNUP = false ;;ENABLE_OPENID_SIGNUP = false
;; ;;
;; Allowed URI patterns (POSIX regexp). ;; Allowed URI patterns (POSIX regexp).
@@ -1983,7 +1979,7 @@ LEVEL = Info
;; Either "memory", "redis", "memcache", or "twoqueue". default is "memory" ;; Either "memory", "redis", "memcache", or "twoqueue". default is "memory"
;ADAPTER = memory ;ADAPTER = memory
;; ;;
;; For "memory" only, GC interval in seconds, default is 60 ;; For "memory" and "twoqueue", GC interval in seconds, default is 60
;INTERVAL = 60 ;INTERVAL = 60
;; ;;
;; For "redis" and "memcache", connection host address ;; For "redis" and "memcache", connection host address
@@ -2014,19 +2010,17 @@ LEVEL = Info
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; ;;
;; Either "memory", "file", "redis", "db", "mysql", "couchbase", "memcache" or "postgres" ;; Either "memory", "file", "redis" or "db", default is "file". "db" will reuse the configuration in [database]
;; Default is "memory". "db" will reuse the configuration in [database] ;PROVIDER = file
;PROVIDER = memory
;; ;;
;; Provider config options ;; Provider config options
;; memory: doesn't have any config yet ;; memory: doesn't have any config yet
;; file: session file path, e.g. `data/sessions`, relative paths will be made absolute against _`AppWorkPath`_. ;; file: session file path, e.g. `data/sessions`, relative paths will be made absolute against _`AppWorkPath`_.
;; redis: default to [redis] CONN_STR ;; redis: default to [redis] CONN_STR
;; mysql: go-sql-driver/mysql dsn config string, e.g. `root:password@/session_table`
;PROVIDER_CONFIG = ;PROVIDER_CONFIG =
;; ;;
;; Session cookie name ;; Session cookie name
;COOKIE_NAME = i_like_gitea ;COOKIE_NAME = gitea_session
;; ;;
;; If you use session in https only: true or false. If not set, it defaults to `true` if the ROOT_URL is an HTTPS URL. ;; If you use session in https only: true or false. If not set, it defaults to `true` if the ROOT_URL is an HTTPS URL.
;COOKIE_SECURE = ;COOKIE_SECURE =
@@ -2052,7 +2046,8 @@ LEVEL = Info
;; How Gitea deals with missing repository avatars ;; How Gitea deals with missing repository avatars
;; none = no avatar will be displayed; random = random avatar will be displayed; image = default image will be used ;; none = no avatar will be displayed; random = random avatar will be displayed; image = default image will be used
;REPOSITORY_AVATAR_FALLBACK = none ;REPOSITORY_AVATAR_FALLBACK = none
;REPOSITORY_AVATAR_FALLBACK_IMAGE = /img/repo_default.png ;; Image URL for the "image" fallback, used as-is, defaults to Gitea's builtin repository avatar
;REPOSITORY_AVATAR_FALLBACK_IMAGE =
;; ;;
;; Max Width and Height of uploaded avatars. ;; Max Width and Height of uploaded avatars.
;; This is to limit the amount of RAM used when resizing the image. ;; This is to limit the amount of RAM used when resizing the image.
-8
View File
@@ -25,7 +25,6 @@ Depending on requirements, the following build tags can be included.
- `bindata`: Build a single monolithic binary, with all assets included. Required for distribution and production build. - `bindata`: Build a single monolithic binary, with all assets included. Required for distribution and production build.
- `pam`: Enable support for PAM (Linux Pluggable Authentication Modules). - `pam`: Enable support for PAM (Linux Pluggable Authentication Modules).
Can be used to authenticate local users or extend authentication to methods available to PAM. Can be used to authenticate local users or extend authentication to methods available to PAM.
- `gogit`: (EXPERIMENTAL) Use go-git variants of Git commands.
To include all assets, use the `bindata` tag: To include all assets, use the `bindata` tag:
@@ -33,13 +32,6 @@ To include all assets, use the `bindata` tag:
TAGS="bindata" make build TAGS="bindata" make build
``` ```
Tag `gogit` is used to try to resolve some Windows-specific performance problems, POSIX systems don't need it.
You can build a Windows binary by:
```bash
GOOS=windows TAGS="bindata gogit" make build
```
## Changing default paths ## Changing default paths
Gitea will search for a number of things from the _`CustomPath`_. Gitea will search for a number of things from the _`CustomPath`_.
+4
View File
@@ -62,6 +62,10 @@ Operations that must roll back together should run inside `db.WithTx()` (or
Functions that participate in a transaction take a `context.Context` as their first Functions that participate in a transaction take a `context.Context` as their first
parameter so the transaction can be propagated. parameter so the transaction can be propagated.
PostgreSQL, MySQL and MSSQL (via `READ_COMMITTED_SNAPSHOT`) read the last committed
row version, so reads never wait for writers. Guard read-then-write logic with a
conditional `UPDATE` or a lock.
### XORM gotchas ### XORM gotchas
- Never call `x.Update(exemplar)` without an explicit `WHERE` clause — it updates - Never call `x.Update(exemplar)` without an explicit `WHERE` clause — it updates
+12 -10
View File
@@ -17,6 +17,7 @@ import unescapedHtmlLiteral from './tools/eslint-rules/unescaped-html-literal.ts
const jsExts = ['js', 'mjs', 'cjs'] as const; const jsExts = ['js', 'mjs', 'cjs'] as const;
const tsExts = ['ts', 'mts', 'cts'] as const; const tsExts = ['ts', 'mts', 'cts'] as const;
const vueExts = ['vue'] as const;
const restrictedGlobals = [ const restrictedGlobals = [
{name: 'localStorage', message: 'Use `modules/user-settings.ts` instead.'}, {name: 'localStorage', message: 'Use `modules/user-settings.ts` instead.'},
@@ -36,7 +37,7 @@ export default defineConfig([
'public/assets/js', 'public/assets/js',
]), ]),
{ {
files: [`**/*.{${[...jsExts, ...tsExts].join(',')}}`], files: [`**/*.{${[...jsExts, ...tsExts, ...vueExts].join(',')}}`],
ignores: ['dist/*'], ignores: ['dist/*'],
languageOptions: { languageOptions: {
ecmaVersion: 'latest', ecmaVersion: 'latest',
@@ -48,7 +49,9 @@ export default defineConfig([
ecmaFeatures: { ecmaFeatures: {
impliedStrict: true, impliedStrict: true,
}, },
parser: typescriptParser,
project: true, project: true,
extraFileExtensions: vueExts.map((ext) => `.${ext}`),
}, },
}, },
linterOptions: { linterOptions: {
@@ -66,8 +69,11 @@ export default defineConfig([
wc, wc,
}, },
settings: { settings: {
'import-x/extensions': [...jsExts, ...tsExts].map((ext) => `.${ext}`), 'import-x/extensions': [...jsExts, ...tsExts, ...vueExts].map((ext) => `.${ext}`),
'import-x/parsers': {'@typescript-eslint/parser': [...jsExts, ...tsExts].map((ext) => `.${ext}`)}, 'import-x/parsers': {
'@typescript-eslint/parser': [...jsExts, ...tsExts].map((ext) => `.${ext}`),
'vue-eslint-parser': vueExts.map((ext) => `.${ext}`),
},
'import-x/resolver': {'typescript': true}, 'import-x/resolver': {'typescript': true},
}, },
rules: { rules: {
@@ -958,7 +964,7 @@ export default defineConfig([
'unicorn/prefer-iterable-in-constructor': [2], 'unicorn/prefer-iterable-in-constructor': [2],
'unicorn/prefer-iterator-concat': [0], // too opinionated 'unicorn/prefer-iterator-concat': [0], // too opinionated
'unicorn/prefer-iterator-helpers': [0], 'unicorn/prefer-iterator-helpers': [0],
'unicorn/prefer-iterator-to-array': [2], 'unicorn/prefer-iterator-to-array': [0], // Iterator#toArray requires ES2025
'unicorn/prefer-iterator-to-array-at-end': [2], 'unicorn/prefer-iterator-to-array-at-end': [2],
'unicorn/prefer-iterator-zip': [0], 'unicorn/prefer-iterator-zip': [0],
'unicorn/prefer-json-import': [0], 'unicorn/prefer-json-import': [0],
@@ -1096,17 +1102,13 @@ export default defineConfig([
}, },
}, },
{ {
files: ['**/*.vue'], files: vueExts.map((ext) => `**/*.${ext}`),
languageOptions: {
parserOptions: {
parser: '@typescript-eslint/parser',
},
},
extends: [ extends: [
vue.configs['flat/recommended'], vue.configs['flat/recommended'],
vueScopedCss.configs.recommended, vueScopedCss.configs.recommended,
], ],
rules: { rules: {
'@typescript-eslint/no-redundant-type-constituents': [0], // types imported from .vue files resolve to any via typescript-eslint's *.vue shim
'vue/attributes-order': [0], 'vue/attributes-order': [0],
'vue/html-closing-bracket-spacing': [2, {startTag: 'never', endTag: 'never', selfClosingTag: 'never'}], 'vue/html-closing-bracket-spacing': [2, {startTag: 'never', endTag: 'never', selfClosingTag: 'never'}],
'vue/max-attributes-per-line': [0], 'vue/max-attributes-per-line': [0],
+4 -25
View File
@@ -6,10 +6,6 @@ toolchain go1.27.1
require ( require (
connectrpc.com/connect v1.21.0 connectrpc.com/connect v1.21.0
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
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
gitea.dev/actionslib v1.2.1 gitea.dev/actionslib v1.2.1
@@ -25,6 +21,7 @@ require (
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
github.com/blevesearch/bleve/v2 v2.6.1 github.com/blevesearch/bleve/v2 v2.6.1
github.com/bohde/codel v0.2.0 github.com/bohde/codel v0.2.0
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c
github.com/buildkite/terminal-to-html/v3 v3.17.1 github.com/buildkite/terminal-to-html/v3 v3.17.1
github.com/caddyserver/certmagic v0.25.4 github.com/caddyserver/certmagic v0.25.4
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20260812203852-971c0284dc33 github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20260812203852-971c0284dc33
@@ -35,7 +32,6 @@ require (
github.com/dustin/go-humanize v1.1.0 github.com/dustin/go-humanize v1.1.0
github.com/editorconfig/editorconfig-core-go/v2 v2.6.5 github.com/editorconfig/editorconfig-core-go/v2 v2.6.5
github.com/emersion/go-imap v1.2.1 github.com/emersion/go-imap v1.2.1
github.com/emirpasic/gods v1.18.1
github.com/felixge/fgprof v0.9.5 github.com/felixge/fgprof v0.9.5
github.com/fsnotify/fsnotify v1.10.1 github.com/fsnotify/fsnotify v1.10.1
github.com/getkin/kin-openapi v0.149.0 github.com/getkin/kin-openapi v0.149.0
@@ -43,8 +39,6 @@ require (
github.com/go-chi/cors v1.2.2 github.com/go-chi/cors v1.2.2
github.com/go-co-op/gocron/v2 v2.22.0 github.com/go-co-op/gocron/v2 v2.22.0
github.com/go-enry/go-enry/v2 v2.9.6 github.com/go-enry/go-enry/v2 v2.9.6
github.com/go-git/go-billy/v5 v5.9.1
github.com/go-git/go-git/v5 v5.19.2
github.com/go-ldap/ldap/v3 v3.4.14 github.com/go-ldap/ldap/v3 v3.4.14
github.com/go-redsync/redsync/v4 v4.18.0 github.com/go-redsync/redsync/v4 v4.18.0
github.com/go-sql-driver/mysql v1.10.1 github.com/go-sql-driver/mysql v1.10.1
@@ -109,15 +103,13 @@ require (
modernc.org/sqlite v1.59.0 modernc.org/sqlite v1.59.0
mvdan.cc/xurls/v2 v2.6.0 mvdan.cc/xurls/v2 v2.6.0
xorm.io/builder v0.3.13 xorm.io/builder v0.3.13
xorm.io/xorm v1.4.1 xorm.io/xorm v1.4.3
) )
require ( require (
cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.2.0 // indirect filippo.io/edwards25519 v1.2.0 // indirect
github.com/DataDog/zstd v1.5.7 // indirect github.com/DataDog/zstd v1.5.7 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/RoaringBitmap/roaring/v2 v2.18.2 // indirect github.com/RoaringBitmap/roaring/v2 v2.18.2 // indirect
github.com/STARRY-S/zip v0.2.3 // indirect github.com/STARRY-S/zip v0.2.3 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect github.com/andybalholm/brotli v1.2.1 // indirect
@@ -148,18 +140,13 @@ require (
github.com/bodgit/sevenzip v1.6.4 // indirect github.com/bodgit/sevenzip v1.6.4 // indirect
github.com/bodgit/windows v1.0.1 // indirect github.com/bodgit/windows v1.0.1 // indirect
github.com/boombuler/barcode v1.1.0 // indirect github.com/boombuler/barcode v1.1.0 // indirect
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c // indirect
github.com/caddyserver/zerossl v0.1.5 // indirect github.com/caddyserver/zerossl v0.1.5 // indirect
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect github.com/cloudflare/circl v1.6.3 // indirect
github.com/couchbase/go-couchbase v0.1.1 // indirect
github.com/couchbase/gomemcached v0.3.4 // indirect
github.com/couchbase/goutils v0.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
@@ -168,7 +155,6 @@ require (
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 // indirect github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect
github.com/go-enry/go-oniguruma v1.2.1 // indirect github.com/go-enry/go-oniguruma v1.2.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-openapi/jsonpointer v0.23.1 // indirect github.com/go-openapi/jsonpointer v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.26.1 // indirect github.com/go-openapi/swag/jsonname v0.26.1 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
@@ -176,9 +162,7 @@ require (
github.com/goccy/go-json v0.10.6 // indirect github.com/goccy/go-json v0.10.6 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/snappy v1.0.0 // indirect github.com/golang/snappy v1.0.0 // indirect
github.com/google/flatbuffers v25.12.19+incompatible // indirect
github.com/google/go-querystring v1.2.0 // indirect github.com/google/go-querystring v1.2.0 // indirect
github.com/google/go-tpm v0.9.8 // indirect github.com/google/go-tpm v0.9.8 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
@@ -188,10 +172,8 @@ require (
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/inbucket/html2text v1.0.0 // indirect github.com/inbucket/html2text v1.0.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.4.0 // indirect github.com/klauspost/cpuid/v2 v2.4.0 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect github.com/klauspost/crc32 v1.3.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect github.com/klauspost/pgzip v1.2.6 // indirect
@@ -219,9 +201,9 @@ require (
github.com/olekukonko/ll v0.1.8 // indirect github.com/olekukonko/ll v0.1.8 // indirect
github.com/olekukonko/tablewriter v1.1.4 // indirect github.com/olekukonko/tablewriter v1.1.4 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.34.1 // indirect
github.com/philhofer/fwd v1.2.0 // indirect github.com/philhofer/fwd v1.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.27 // indirect github.com/pierrec/lz4/v4 v4.1.27 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
@@ -232,15 +214,12 @@ require (
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.10.2 // indirect github.com/sirupsen/logrus v1.10.2 // indirect
github.com/skeema/knownhosts v1.3.2 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spf13/afero v1.15.0 // indirect github.com/spf13/afero v1.15.0 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/stangelandcl/ppmd v0.1.1 // indirect github.com/stangelandcl/ppmd v0.1.1 // indirect
github.com/tinylib/msgp v1.6.4 // 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 github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect github.com/zeebo/blake3 v0.2.4 // indirect
github.com/zeebo/xxh3 v1.1.0 // indirect github.com/zeebo/xxh3 v1.1.0 // indirect
@@ -251,10 +230,10 @@ require (
go.uber.org/zap/exp v0.3.0 // indirect go.uber.org/zap/exp v0.3.0 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/time v0.15.0 // indirect golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.49.0 // indirect golang.org/x/tools v0.49.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
modernc.org/libc v1.75.7 // indirect modernc.org/libc v1.75.7 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.12.1 // indirect modernc.org/memory v1.12.1 // indirect
+2 -116
View File
@@ -4,18 +4,8 @@ code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM= code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
connectrpc.com/connect v1.21.0 h1:LhqSJt7jHf5NJBo9Jq/t/9FjcYAideif0mg+qe2jCUs= connectrpc.com/connect v1.21.0 h1:LhqSJt7jHf5NJBo9Jq/t/9FjcYAideif0mg+qe2jCUs=
connectrpc.com/connect v1.21.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= connectrpc.com/connect v1.21.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
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 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
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=
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098/go.mod h1:LjzIOHlRemuUyO7WR12fmm18VZIlCAaOt9L3yKw40pk=
gitea.com/go-chi/session v0.0.0-20260708011333-ebced8a7a2d6 h1:YWzVGeC/8SZThrJS48ZmQYLkzssdeABxHPhbdnxPDIU=
gitea.com/go-chi/session v0.0.0-20260708011333-ebced8a7a2d6/go.mod h1:KDvcfMUoXfATPHs2mbMoXFTXT45/FAFAS39waz9tPk0=
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:+wWBi6Qfruqu7xJgjOIrKVQGiLUZdpKYCZewJ4clqhw= gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:+wWBi6Qfruqu7xJgjOIrKVQGiLUZdpKYCZewJ4clqhw=
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY= gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY=
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o= gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o=
@@ -46,9 +36,6 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0 h1:Nljr4q1GRA/
github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0/go.mod h1:Y33QHnf0FfdVewFFISOGe20mkZbxX4H839o955/PoeI= github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0/go.mod h1:Y33QHnf0FfdVewFFISOGe20mkZbxX4H839o955/PoeI=
github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347 h1:3JhDl+JysaO8nhNU1XMaw35VSGjV4IEQAefaG4Lyok4= github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347 h1:3JhDl+JysaO8nhNU1XMaw35VSGjV4IEQAefaG4Lyok4=
github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347/go.mod h1:2ErI0aycD43Ufr6CFK5lT/NrHGmoZuVbn1nlPThw69o= github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347/go.mod h1:2ErI0aycD43Ufr6CFK5lT/NrHGmoZuVbn1nlPThw69o=
github.com/ProtonMail/go-crypto v1.5.0 h1:sKmuvjOgsnrtpMvZ+84MCnTJCpjxZ1qCFn076lz1yT0= github.com/ProtonMail/go-crypto v1.5.0 h1:sKmuvjOgsnrtpMvZ+84MCnTJCpjxZ1qCFn076lz1yT0=
@@ -75,10 +62,6 @@ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eT
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/cascadia v1.3.4 h1:vM2lgh0Vru9Vwyfm4cQqWP2HHMW0u0+2PAW7Q38Qufg= github.com/andybalholm/cascadia v1.3.4 h1:vM2lgh0Vru9Vwyfm4cQqWP2HHMW0u0+2PAW7Q38Qufg=
github.com/andybalholm/cascadia v1.3.4/go.mod h1:BLRmbRjpEtNKieZOCCvYj4RqN+KRA41GBe/5O+G93kM= github.com/andybalholm/cascadia v1.3.4/go.mod h1:BLRmbRjpEtNKieZOCCvYj4RqN+KRA41GBe/5O+G93kM=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
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/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -173,17 +156,8 @@ github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA= github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/couchbase/go-couchbase v0.1.1 h1:ClFXELcKj/ojyoTYbsY34QUrrYCBi/1G749sXSCkdhk=
github.com/couchbase/go-couchbase v0.1.1/go.mod h1:+/bddYDxXsf9qt0xpDUtRR47A2GjaXmGGAqQ/k3GJ8A=
github.com/couchbase/gomemcached v0.3.4 h1:VGdrZUJbt5lLyI/MXnyVCZKHKYXg/vaud08lJIAeZps=
github.com/couchbase/gomemcached v0.3.4/go.mod h1:pISAjweI42vljCumsJIo7CVhqIMIIP9g3Wfhl1JJw68=
github.com/couchbase/goutils v0.1.2/go.mod h1:h89Ek/tiOxxqjz30nPPlwZdQbdB8BwgnuBxeoUe/ViE=
github.com/couchbase/goutils v0.3.0 h1:rsv72B6BDjW9jmwlfiDUrdu3EpNvPuo5WLULHzQ0DLE=
github.com/couchbase/goutils v0.3.0/go.mod h1:7Gm+D3vXfV4HS+hQWvKfy6e6ILCptGXNqBKvQXhplhk=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -205,8 +179,6 @@ github.com/dustin/go-humanize v1.1.0 h1:dbKTrvD0klcbBV/h4AWJdMuZogJACoMlvWIWZ5b2
github.com/dustin/go-humanize v1.1.0/go.mod h1:hc1CvRkJMsgxqjmjMQF3QNRAZBwY8AXBAzKYoSX9sFI= github.com/dustin/go-humanize v1.1.0/go.mod h1:hc1CvRkJMsgxqjmjMQF3QNRAZBwY8AXBAzKYoSX9sFI=
github.com/editorconfig/editorconfig-core-go/v2 v2.6.5 h1:MTcuJQkIFRLfNn9FfAvdO0p3FhNyaCf+IGTwy0TxX6E= github.com/editorconfig/editorconfig-core-go/v2 v2.6.5 h1:MTcuJQkIFRLfNn9FfAvdO0p3FhNyaCf+IGTwy0TxX6E=
github.com/editorconfig/editorconfig-core-go/v2 v2.6.5/go.mod h1:SizrS3EM1vFF0v/JZlJ5LfK6tXhwln2823YZwNh/vBE= github.com/editorconfig/editorconfig-core-go/v2 v2.6.5/go.mod h1:SizrS3EM1vFF0v/JZlJ5LfK6tXhwln2823YZwNh/vBE=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA= github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY= github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
@@ -214,8 +186,6 @@ github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTe
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk= github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
@@ -230,8 +200,6 @@ github.com/getkin/kin-openapi v0.149.0 h1:ZbhmVJ4yq5RZDUsyP8lcBcGMsjsaTqXEFt6isd
github.com/getkin/kin-openapi v0.149.0/go.mod h1:1+BHDzstro+P5CKtPy1X4PfofnFgmRe6uvMy9+r9fKY= github.com/getkin/kin-openapi v0.149.0/go.mod h1:1+BHDzstro+P5CKtPy1X4PfofnFgmRe6uvMy9+r9fKY=
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 h1:mtDjlmloH7ytdblogrMz1/8Hqua1y8B4ID+bh3rvod0= 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/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1/go.mod h1:fenKRzpXDjNpsIBhuhUzvjCKlDjKam0boRAenTE0Q6A=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-asn1-ber/asn1-ber v1.5.8 h1:H9AZkK22UOmfX8J84ubyaZxKJZ3FMHVwn8swoMML7iQ= github.com/go-asn1-ber/asn1-ber v1.5.8 h1:H9AZkK22UOmfX8J84ubyaZxKJZ3FMHVwn8swoMML7iQ=
github.com/go-asn1-ber/asn1-ber v1.5.8/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.8/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
@@ -245,14 +213,6 @@ github.com/go-enry/go-enry/v2 v2.9.6 h1:np63eOtMV56zfYDHnFVgpEVOk8fr2kmylcMnAZUD
github.com/go-enry/go-enry/v2 v2.9.6/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8= github.com/go-enry/go-enry/v2 v2.9.6/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8=
github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo= github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo=
github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4= github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.9.1 h1:8U73XiOTfINdItHVa6z4Gv7ToObcZ6grkqQbLryLCdA=
github.com/go-git/go-billy/v5 v5.9.1/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-ldap/ldap/v3 v3.4.14 h1:D6PYdEgsaVzsXyr6w/yDC06Ria4uUhWm+Rb+er8lfAs= github.com/go-ldap/ldap/v3 v3.4.14 h1:D6PYdEgsaVzsXyr6w/yDC06Ria4uUhWm+Rb+er8lfAs=
@@ -297,8 +257,6 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
@@ -314,9 +272,6 @@ github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8= github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8=
github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -343,8 +298,6 @@ github.com/google/pprof v0.0.0-20260906184651-6331bc6350fe h1:QAinXoAFJdGQYztXn3
github.com/google/pprof v0.0.0-20260906184651-6331bc6350fe/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk= github.com/google/pprof v0.0.0-20260906184651-6331bc6350fe/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
@@ -381,8 +334,6 @@ github.com/huandu/xstrings v1.6.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
github.com/inbucket/html2text v1.0.0 h1:N5kza++4uBBDJ2Z3KUnTRyPNoBcW+YfOgNiNmNB+sgs= github.com/inbucket/html2text v1.0.0 h1:N5kza++4uBBDJ2Z3KUnTRyPNoBcW+YfOgNiNmNB+sgs=
github.com/inbucket/html2text v1.0.0/go.mod h1:5TrhXQKGU+LXurODaSm55Y9eXoPBRnYiOz4x2XfUoJU= github.com/inbucket/html2text v1.0.0/go.mod h1:5TrhXQKGU+LXurODaSm55Y9eXoPBRnYiOz4x2XfUoJU=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
@@ -402,13 +353,8 @@ github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7X
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
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.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
@@ -526,8 +472,6 @@ github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk= github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -573,16 +517,8 @@ github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo= github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q= github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY=
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik= github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU= github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
@@ -606,7 +542,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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.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/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 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 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
@@ -619,8 +554,6 @@ github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77ro
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.17 h1:flR0y/x1hgM8EGV1AW3Xll6T413G0glV8UfBwR617V4= github.com/ulikunitz/xz v0.5.17 h1:flR0y/x1hgM8EGV1AW3Xll6T413G0glV8UfBwR617V4=
github.com/ulikunitz/xz v0.5.17/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw= github.com/ulikunitz/xz v0.5.17/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw=
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 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw=
github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to= github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to=
github.com/urfave/cli/v3 v3.13.0 h1:Dr6jqMfIyyFsRVn7Nz5mqLsMY+ZMpfh3a0aMs+umPVY= github.com/urfave/cli/v3 v3.13.0 h1:Dr6jqMfIyyFsRVn7Nz5mqLsMY+ZMpfh3a0aMs+umPVY=
@@ -629,8 +562,6 @@ github.com/wneessen/go-mail v0.8.1 h1:tVcncj02/QySVFw3zr/kXOzZcuFQqBNT6K+Rbgm/pc
github.com/wneessen/go-mail v0.8.1/go.mod h1:dWZ61zadzCIyvB4y1/YzC5O7MrbbzBfPkARmbosdf8w= github.com/wneessen/go-mail v0.8.1/go.mod h1:dWZ61zadzCIyvB4y1/YzC5O7MrbbzBfPkARmbosdf8w=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
@@ -681,11 +612,6 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
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.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
@@ -694,10 +620,6 @@ golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew= golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
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.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -706,14 +628,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
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.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98= golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
@@ -723,11 +639,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -736,37 +647,22 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -774,11 +670,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
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.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
@@ -787,9 +678,6 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.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.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= 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/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-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -817,8 +705,6 @@ gopkg.in/ini.v1 v1.67.3 h1:iM9Lhz5MRSGhHVGGwCuzG9KO8PoirCXj/m/qTmOJJQw=
gopkg.in/ini.v1 v1.67.3/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/ini.v1 v1.67.3/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -862,5 +748,5 @@ pgregory.net/rapid v0.4.2 h1:lsi9jhvZTYvzVpeG93WWgimPRmiJQfGFRNTEZh1dtY0=
pgregory.net/rapid v0.4.2/go.mod h1:UYpPVyjFHzYBGHIxLFoupi8vwk6rXNzRY9OMvVxFIOU= pgregory.net/rapid v0.4.2/go.mod h1:UYpPVyjFHzYBGHIxLFoupi8vwk6rXNzRY9OMvVxFIOU=
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo= xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/xorm v1.4.1 h1:m7QlNd0eBGb31IV4Q/ow0Du83rtdC1CiwlvJZGvYde8= xorm.io/xorm v1.4.3 h1:MwWFWzVr+/6D07qGCDhBAfABcuT0gvqY3XmTy1215BM=
xorm.io/xorm v1.4.1/go.mod h1:cs0ePc8O4a0jD78cNvD+0VFwhqotTvLQZv372QsDw7Q= xorm.io/xorm v1.4.3/go.mod h1:cs0ePc8O4a0jD78cNvD+0VFwhqotTvLQZv372QsDw7Q=
+3
View File
@@ -429,6 +429,9 @@ func prepareMigrationTasks() []*migration {
newMigration(353, "Add audit event table", v28.AddAuditEventTable), newMigration(353, "Add audit event table", v28.AddAuditEventTable),
newMigration(354, "Add Actions job queue indexes", v28.AddActionQueueIndexes), newMigration(354, "Add Actions job queue indexes", v28.AddActionQueueIndexes),
newMigration(355, "Add AutoMerge merged_commit_id column", v28.AddAutoMergeMergedCommitID), newMigration(355, "Add AutoMerge merged_commit_id column", v28.AddAutoMergeMergedCommitID),
// Gitea 28.0.0 ends at migration ID number 355 (database version 356)
newMigration(356, "Add index on action_run commit_sha", v28.AddActionRunCommitSHAIndex),
} }
return preparedMigrations return preparedMigrations
} }
+25
View File
@@ -0,0 +1,25 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v28
import (
"context"
"gitea.dev/modelmigration/base"
"xorm.io/xorm"
)
// AddActionRunCommitSHAIndex indexes the runs lookup by commit, which the API `head_sha` filter uses.
func AddActionRunCommitSHAIndex(_ context.Context, x base.EngineMigration) error {
type ActionRun struct {
CommitSHA string `xorm:"index"`
}
_, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
IgnoreConstrains: true,
}, new(ActionRun))
return err
}
+3 -4
View File
@@ -41,7 +41,7 @@ type ActionRun struct {
ScheduleID int64 ScheduleID int64
Ref string `xorm:"index"` // the commit/tag/… that caused the run Ref string `xorm:"index"` // the commit/tag/… that caused the run
IsRefDeleted bool `xorm:"-"` IsRefDeleted bool `xorm:"-"`
CommitSHA string CommitSHA string `xorm:"index"`
IsForkPullRequest bool // If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow. IsForkPullRequest bool // If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow.
NeedApproval bool // may need approval if it's a fork pull request NeedApproval bool // may need approval if it's a fork pull request
ApprovedBy int64 `xorm:"index"` // who approved ApprovedBy int64 `xorm:"index"` // who approved
@@ -295,9 +295,8 @@ func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, er
return &run, nil return &run, nil
} }
func GetRunsByRepoAndID(ctx context.Context, repoID int64, runIDs []int64) ([]*ActionRun, error) { func GetRunsByRepoAndID(ctx context.Context, repoID int64, runIDs []int64) (runs []*ActionRun, err error) {
var runs []*ActionRun err = db.GetEngine(ctx).In("id", runIDs).Where("repo_id=?", repoID).OrderBy("id").Find(&runs)
err := db.GetEngine(ctx).In("id", runIDs).Where("repo_id=?", repoID).Find(&runs)
return runs, err return runs, err
} }
+1 -1
View File
@@ -200,7 +200,7 @@ func (r *ActionRunner) GenerateAndFillToken() {
// CanMatchLabels checks whether the runner's labels can match a job's "runs-on" // CanMatchLabels checks whether the runner's labels can match a job's "runs-on"
// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idruns-on // See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idruns-on
func (r *ActionRunner) CanMatchLabels(jobRunsOn []string) bool { func (r *ActionRunner) CanMatchLabels(jobRunsOn []string) bool {
return !slices.ContainsFunc(jobRunsOn, func(label string) bool { return !util.SliceContainsString(r.AgentLabels, label, true) }) return len(jobRunsOn) > 0 && !slices.ContainsFunc(jobRunsOn, func(label string) bool { return !util.SliceContainsString(r.AgentLabels, label, true) })
} }
func init() { func init() {
+1
View File
@@ -86,4 +86,5 @@ func TestCanMatchLabelsCaseInsensitive(t *testing.T) {
runner := &ActionRunner{AgentLabels: []string{"self-hosted", "Linux", "X64"}} runner := &ActionRunner{AgentLabels: []string{"self-hosted", "Linux", "X64"}}
assert.True(t, runner.CanMatchLabels([]string{"SELF-HOSTED", "linux"})) assert.True(t, runner.CanMatchLabels([]string{"SELF-HOSTED", "linux"}))
assert.False(t, runner.CanMatchLabels([]string{"linux", "arm64"})) assert.False(t, runner.CanMatchLabels([]string{"linux", "arm64"}))
assert.False(t, runner.CanMatchLabels(nil))
} }
+7 -4
View File
@@ -298,6 +298,12 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
if err := e.Where(cond).Asc("updated", "id").Limit(pickTaskBatchSize).Find(&jobs); err != nil { if err := e.Where(cond).Asc("updated", "id").Limit(pickTaskBatchSize).Find(&jobs); err != nil {
return nil, false, err return nil, false, err
} }
// A short page means no waiting jobs remain beyond it.
isLastPage := len(jobs) < pickTaskBatchSize
if !isLastPage {
last := jobs[len(jobs)-1] // read before a lost claim bumps Updated
cursorUpdated, cursorID = last.Updated, last.ID
}
for _, v := range jobs { for _, v := range jobs {
if !runner.CanMatchLabels(v.RunsOn) { if !runner.CanMatchLabels(v.RunsOn) {
@@ -313,12 +319,9 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
// Another runner claimed this job concurrently; try the next one. // Another runner claimed this job concurrently; try the next one.
} }
// A short page means no waiting jobs remain beyond it. if isLastPage {
if len(jobs) < pickTaskBatchSize {
return nil, false, nil return nil, false, nil
} }
last := jobs[len(jobs)-1]
cursorUpdated, cursorID = last.Updated, last.ID
} }
} }
+3 -9
View File
@@ -83,23 +83,17 @@ func GetActivityStats(ctx context.Context, repo *repo_model.Repository, timeFrom
// GetActivityStatsTopAuthors returns top author stats for git commits for all branches // GetActivityStatsTopAuthors returns top author stats for git commits for all branches
func GetActivityStatsTopAuthors(ctx context.Context, repo *repo_model.Repository, timeFrom time.Time, count int) ([]*ActivityAuthorData, error) { func GetActivityStatsTopAuthors(ctx context.Context, repo *repo_model.Repository, timeFrom time.Time, count int) ([]*ActivityAuthorData, error) {
gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo) authors, err := git.GetCodeActivityAuthors(ctx, repo, timeFrom)
if err != nil {
return nil, fmt.Errorf("OpenRepository: %w", err)
}
defer closer.Close()
code, err := gitRepo.GetCodeActivityStats(ctx, timeFrom, "")
if err != nil { if err != nil {
return nil, fmt.Errorf("FillFromGit: %w", err) return nil, fmt.Errorf("FillFromGit: %w", err)
} }
if code.Authors == nil { if authors == nil {
return nil, nil return nil, nil
} }
users := make(map[int64]*ActivityAuthorData) users := make(map[int64]*ActivityAuthorData)
var unknownUserID int64 var unknownUserID int64
unknownUserAvatarLink := user_model.NewGhostUser().AvatarLink(ctx) unknownUserAvatarLink := user_model.NewGhostUser().AvatarLink(ctx)
for _, v := range code.Authors { for _, v := range authors {
if len(v.Email) == 0 { if len(v.Email) == 0 {
continue continue
} }
+6
View File
@@ -564,6 +564,12 @@ func (grant *OAuth2Grant) SetNonce(ctx context.Context, nonce string) error {
return nil return nil
} }
func UpdateGrantScope(ctx context.Context, grant *OAuth2Grant, newScope string) error {
grant.Scope = newScope
_, err := db.GetEngine(ctx).ID(grant.ID).Cols("scope").Update(grant)
return err
}
// GetOAuth2GrantByID returns the grant with the given ID // GetOAuth2GrantByID returns the grant with the given ID
func GetOAuth2GrantByID(ctx context.Context, id int64) (grant *OAuth2Grant, err error) { func GetOAuth2GrantByID(ctx context.Context, id int64) (grant *OAuth2Grant, err error) {
grant = new(OAuth2Grant) grant = new(OAuth2Grant)
+29 -72
View File
@@ -5,7 +5,6 @@ package auth
import ( import (
"context" "context"
"fmt"
"gitea.dev/models/db" "gitea.dev/models/db"
"gitea.dev/modules/timeutil" "gitea.dev/modules/timeutil"
@@ -13,49 +12,46 @@ import (
"xorm.io/builder" "xorm.io/builder"
) )
// Session represents a session compatible for go-chi session
type Session struct { type Session struct {
Key string `xorm:"pk CHAR(16)"` // has to be Key to match with go-chi/session Key string `xorm:"pk CHAR(16)"` // the limit is from legacy go-chi/session
Data []byte `xorm:"BLOB"` // on MySQL this has a maximum size of 64Kb - this may need to be increased Data []byte `xorm:"BLOB"` // on MySQL this has a maximum size of 64Kb
Expiry timeutil.TimeStamp // has to be Expiry to match with go-chi/session LastAccessTime timeutil.TimeStamp `xorm:"expiry"` // last access time, the field name is from legacy go-chi/session, we don't want to change it at the moment
} }
const DbSessionLastAccessTime = "expiry" // maybe we can make a deeper clean up in the future, just keep this PR focused
func init() { func init() {
db.RegisterModel(new(Session)) db.RegisterModel(new(Session))
} }
// UpdateSession updates the session with provided id // UpdateSession stores the data of the session with provided id, creating the session only if create is set
func UpdateSession(ctx context.Context, key string, data []byte) error { func UpdateSession(ctx context.Context, key string, data []byte, create bool) error {
_, err := db.GetEngine(ctx).ID(key).Update(&Session{ session := &Session{Key: key, Data: data, LastAccessTime: timeutil.TimeStampNow()}
Data: data, update := func() (int64, error) {
Expiry: timeutil.TimeStampNow(), return db.GetEngine(ctx).ID(key).Cols("data", DbSessionLastAccessTime).Update(session)
}) }
if updated, err := update(); err != nil || updated > 0 || !create {
return err
}
insertErr := db.Insert(ctx, session)
if insertErr == nil {
return nil
}
// the row exists if a concurrent request inserted it, or if MySQL reported an unchanged row as not updated
if exist, err := db.Exist[Session](ctx, builder.Eq{"`key`": key}); err != nil || !exist {
return insertErr
}
_, err := update()
return err return err
} }
// ReadSession reads the data for the provided session func UpdateSessionLastAccessTime(ctx context.Context, key string) error {
func ReadSession(ctx context.Context, key string) (*Session, error) { _, err := db.GetEngine(ctx).ID(key).Cols(DbSessionLastAccessTime).Update(&Session{LastAccessTime: timeutil.TimeStampNow()})
return db.WithTx2(ctx, func(ctx context.Context) (*Session, error) { return err
session, exist, err := db.Get[Session](ctx, builder.Eq{"`key`": key})
if err != nil {
return nil, err
} else if !exist {
session = &Session{
Key: key,
Expiry: timeutil.TimeStampNow(),
}
if err := db.Insert(ctx, session); err != nil {
return nil, err
}
} }
return session, nil func GetSession(ctx context.Context, key string) (*Session, bool, error) {
}) return db.Get[Session](ctx, builder.Eq{"`key`": key})
}
// ExistSession checks if a session exists
func ExistSession(ctx context.Context, key string) (bool, error) {
return db.Exist[Session](ctx, builder.Eq{"`key`": key})
} }
// DestroySession destroys a session // DestroySession destroys a session
@@ -66,47 +62,8 @@ func DestroySession(ctx context.Context, key string) error {
return err return err
} }
// RegenerateSession regenerates a session from the old id
func RegenerateSession(ctx context.Context, oldKey, newKey string) (*Session, error) {
return db.WithTx2(ctx, func(ctx context.Context) (*Session, error) {
if has, err := db.Exist[Session](ctx, builder.Eq{"`key`": newKey}); err != nil {
return nil, err
} else if has {
return nil, fmt.Errorf("session Key: %s already exists", newKey)
}
if has, err := db.Exist[Session](ctx, builder.Eq{"`key`": oldKey}); err != nil {
return nil, err
} else if !has {
if err := db.Insert(ctx, &Session{
Key: oldKey,
Expiry: timeutil.TimeStampNow(),
}); err != nil {
return nil, err
}
}
if _, err := db.Exec(ctx, "UPDATE `session` SET `key` = ? WHERE `key`=?", newKey, oldKey); err != nil {
return nil, err
}
s, _, err := db.Get[Session](ctx, builder.Eq{"`key`": newKey})
if err != nil {
// is not exist, it should be impossible
return nil, err
}
return s, nil
})
}
// CountSessions returns the number of sessions
func CountSessions(ctx context.Context) (int64, error) {
return db.GetEngine(ctx).Count(&Session{})
}
// CleanupSessions cleans up expired sessions // CleanupSessions cleans up expired sessions
func CleanupSessions(ctx context.Context, maxLifetime int64) error { func CleanupSessions(ctx context.Context, maxLifetime int64) error {
_, err := db.GetEngine(ctx).Where("expiry <= ?", timeutil.TimeStampNow().Add(-maxLifetime)).Delete(&Session{}) _, err := db.GetEngine(ctx).Where(DbSessionLastAccessTime+" <= ?", timeutil.TimeStampNow().Add(-maxLifetime)).Delete(&Session{})
return err return err
} }
+20
View File
@@ -5,7 +5,9 @@ package db
import ( import (
"context" "context"
"database/sql"
"fmt" "fmt"
"time"
"gitea.dev/modules/log" "gitea.dev/modules/log"
"gitea.dev/modules/setting" "gitea.dev/modules/setting"
@@ -59,6 +61,11 @@ func InitEngine(ctx context.Context) error {
xe.SetMaxIdleConns(setting.Database.MaxIdleConns) xe.SetMaxIdleConns(setting.Database.MaxIdleConns)
xe.SetConnMaxLifetime(setting.Database.ConnMaxLifetime) xe.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
if setting.Database.Type.IsMySQL() {
// like PostgreSQL and MSSQL, avoids MariaDB snapshot isolation errors
xe.SetDefaultTxOptions(&sql.TxOptions{Isolation: sql.LevelReadCommitted})
}
if setting.Database.SlowQueryThreshold > 0 { if setting.Database.SlowQueryThreshold > 0 {
xe.AddHook(&EngineHook{ xe.AddHook(&EngineHook{
Threshold: setting.Database.SlowQueryThreshold, Threshold: setting.Database.SlowQueryThreshold,
@@ -103,6 +110,10 @@ func InitEngineWithMigration(ctx context.Context, migrateFunc func(context.Conte
preprocessDatabaseCollation(xormEngine) preprocessDatabaseCollation(xormEngine)
if setting.Database.Type.IsMSSQL() {
enableMSSQLReadCommittedSnapshot(ctx, xormEngine)
}
// We have to run migrateFunc here in case the user is re-running installation on a previously created DB. // We have to run migrateFunc here in case the user is re-running installation on a previously created DB.
// If we do not then table schemas will be changed and there will be conflicts when the migrations run properly. // If we do not then table schemas will be changed and there will be conflicts when the migrations run properly.
// //
@@ -125,3 +136,12 @@ func InitEngineWithMigration(ctx context.Context, migrateFunc func(context.Conte
return nil return nil
} }
// enableMSSQLReadCommittedSnapshot stops MSSQL reads waiting on writers, like PostgreSQL and MySQL
func enableMSSQLReadCommittedSnapshot(ctx context.Context, engine EngineMigration) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second) // ALTER waits for all other connections to close
defer cancel()
if _, err := engine.Context(ctx).Exec("IF (SELECT is_read_committed_snapshot_on FROM sys.databases WHERE database_id = DB_ID()) = 0 ALTER DATABASE CURRENT SET READ_COMMITTED_SNAPSHOT ON"); err != nil {
log.Error("Unable to set READ_COMMITTED_SNAPSHOT=ON: %v", err)
}
}
+4 -4
View File
@@ -311,17 +311,17 @@ func (opts *CommitStatusOptions) ToConds() builder.Cond {
func (opts *CommitStatusOptions) ToOrders() string { func (opts *CommitStatusOptions) ToOrders() string {
switch opts.SortType { switch opts.SortType {
case "oldest": case "oldest":
return "created_unix ASC" return "created_unix ASC, `index` ASC"
case "recentupdate": case "recentupdate":
return "updated_unix DESC" return "updated_unix DESC, `index` DESC"
case "leastupdate": case "leastupdate":
return "updated_unix ASC" return "updated_unix ASC, `index` ASC"
case "leastindex": case "leastindex":
return "`index` DESC" return "`index` DESC"
case "highestindex": case "highestindex":
return "`index` ASC" return "`index` ASC"
default: default:
return "created_unix DESC" return "created_unix DESC, `index` DESC" // timestamps have 1s resolution, `index` keeps paging stable
} }
} }
+7 -20
View File
@@ -32,28 +32,15 @@ func TestGetCommitStatuses(t *testing.T) {
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 5, int(maxResults)) assert.Equal(t, 5, int(maxResults))
assert.Len(t, statuses, 5) var indexes []int64
for _, status := range statuses {
assert.Equal(t, "ci/awesomeness", statuses[0].Context) indexes = append(indexes, status.Index)
assert.Equal(t, commitstatus.CommitStatusPending, statuses[0].State) }
assert.Equal(t, []int64{5, 4, 3, 2, 1}, indexes)
assert.Equal(t, "deploy/awesomeness", statuses[0].Context)
assert.Equal(t, commitstatus.CommitStatusError, statuses[0].State)
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[0].APIURL(t.Context())) assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[0].APIURL(t.Context()))
assert.Equal(t, "cov/awesomeness", statuses[1].Context)
assert.Equal(t, commitstatus.CommitStatusWarning, statuses[1].State)
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[1].APIURL(t.Context()))
assert.Equal(t, "cov/awesomeness", statuses[2].Context)
assert.Equal(t, commitstatus.CommitStatusSuccess, statuses[2].State)
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[2].APIURL(t.Context()))
assert.Equal(t, "ci/awesomeness", statuses[3].Context)
assert.Equal(t, commitstatus.CommitStatusFailure, statuses[3].State)
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[3].APIURL(t.Context()))
assert.Equal(t, "deploy/awesomeness", statuses[4].Context)
assert.Equal(t, commitstatus.CommitStatusError, statuses[4].State)
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[4].APIURL(t.Context()))
statuses, maxResults, err = db.FindAndCount[git_model.CommitStatus](t.Context(), &git_model.CommitStatusOptions{ statuses, maxResults, err = db.FindAndCount[git_model.CommitStatus](t.Context(), &git_model.CommitStatusOptions{
ListOptions: db.ListOptions{Page: 2, PageSize: 50}, ListOptions: db.ListOptions{Page: 2, PageSize: 50},
RepoID: repo1.ID, RepoID: repo1.ID,
+6 -16
View File
@@ -123,23 +123,13 @@ func (protectBranch *ProtectedBranch) LoadRepo(ctx context.Context) (err error)
} }
// CanUserPush returns if some user could push to this protected branch // CanUserPush returns if some user could push to this protected branch
func (protectBranch *ProtectedBranch) CanUserPush(ctx context.Context, user *user_model.User) bool { func (protectBranch *ProtectedBranch) CanUserPush(ctx context.Context, user *user_model.User, permissionInRepo access_model.Permission) bool {
if !protectBranch.CanPush { if !protectBranch.CanPush {
return false return false
} }
if !protectBranch.EnableWhitelist { if !protectBranch.EnableWhitelist {
if err := protectBranch.LoadRepo(ctx); err != nil { return permissionInRepo.CanWrite(unit.TypeCode)
log.Error("LoadRepo: %v", err)
return false
}
writeAccess, err := access_model.HasAccessUnit(ctx, user, protectBranch.Repo, unit.TypeCode, perm.AccessModeWrite)
if err != nil {
log.Error("HasAccessUnit: %v", err)
return false
}
return writeAccess
} }
if slices.Contains(protectBranch.WhitelistUserIDs, user.ID) { if slices.Contains(protectBranch.WhitelistUserIDs, user.ID) {
@@ -160,17 +150,17 @@ func (protectBranch *ProtectedBranch) CanUserPush(ctx context.Context, user *use
// CanUserForcePush returns if some user could force push to this protected branch // CanUserForcePush returns if some user could force push to this protected branch
// Since force-push extends normal push, we also check if user has regular push access // Since force-push extends normal push, we also check if user has regular push access
func (protectBranch *ProtectedBranch) CanUserForcePush(ctx context.Context, user *user_model.User) bool { func (protectBranch *ProtectedBranch) CanUserForcePush(ctx context.Context, user *user_model.User, permissionInRepo access_model.Permission) bool {
if !protectBranch.CanForcePush { if !protectBranch.CanForcePush {
return false return false
} }
if !protectBranch.EnableForcePushAllowlist { if !protectBranch.EnableForcePushAllowlist {
return protectBranch.CanUserPush(ctx, user) return protectBranch.CanUserPush(ctx, user, permissionInRepo)
} }
if slices.Contains(protectBranch.ForcePushAllowlistUserIDs, user.ID) { if slices.Contains(protectBranch.ForcePushAllowlistUserIDs, user.ID) {
return protectBranch.CanUserPush(ctx, user) return protectBranch.CanUserPush(ctx, user, permissionInRepo)
} }
if len(protectBranch.ForcePushAllowlistTeamIDs) == 0 { if len(protectBranch.ForcePushAllowlistTeamIDs) == 0 {
@@ -182,7 +172,7 @@ func (protectBranch *ProtectedBranch) CanUserForcePush(ctx context.Context, user
log.Error("IsUserInTeams: %v", err) log.Error("IsUserInTeams: %v", err)
return false return false
} }
return in && protectBranch.CanUserPush(ctx, user) return in && protectBranch.CanUserPush(ctx, user, permissionInRepo)
} }
// IsUserMergeWhitelisted checks if some user is whitelisted to merge to this branch // IsUserMergeWhitelisted checks if some user is whitelisted to merge to this branch
+15
View File
@@ -407,6 +407,21 @@ func (pr *PullRequest) GetGitHeadRefName() string { // TODO: make it return RefN
return git.RefNameFromPullIndex(pr.Index).String() return git.RefNameFromPullIndex(pr.Index).String()
} }
func (pr *PullRequest) GetInstructionsCliArgs() (ret struct {
BaseBranchArg string
HeadBranchArg string
LocalBranchArg string
},
) {
ret.BaseBranchArg = util.ShellEscape(pr.BaseBranch)
ret.HeadBranchArg = util.ShellEscape(pr.HeadBranch)
ret.LocalBranchArg = ret.HeadBranchArg
if pr.HeadRepo != nil && pr.HeadRepoID != pr.BaseRepoID {
ret.LocalBranchArg = util.ShellEscape(pr.HeadRepo.OwnerName) + "-" + ret.HeadBranchArg
}
return ret
}
// GetReviewCommentsCount returns the number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR) // GetReviewCommentsCount returns the number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR)
func (pr *PullRequest) GetReviewCommentsCount(ctx context.Context) int { func (pr *PullRequest) GetReviewCommentsCount(ctx context.Context) int {
opts := FindCommentsOptions{ opts := FindCommentsOptions{
+1 -1
View File
@@ -50,7 +50,7 @@ func (c *commitChecker) IsCommitIDExisting(commitID string) bool {
c.gitRepo, c.gitRepoCloser = r, closer c.gitRepo, c.gitRepoCloser = r, closer
} }
exist = c.gitRepo.IsReferenceExist(c.ctx, commitID) // Don't use IsObjectExist since it doesn't support short hashes with gogit edition. exist = c.gitRepo.IsReferenceExist(c.ctx, commitID)
c.commitCache[commitID] = exist c.commitCache[commitID] = exist
return exist return exist
} }
+4 -4
View File
@@ -78,13 +78,13 @@ func (m *PushMirror) GetRemoteName() string {
return m.RemoteName return m.RemoteName
} }
// UpdatePushMirror updates the push-mirror // UpdatePushMirrorSyncStatus updates the sync status (last update time and last error) of the push-mirror
func UpdatePushMirror(ctx context.Context, m *PushMirror) error { func UpdatePushMirrorSyncStatus(ctx context.Context, m *PushMirror) error {
_, err := db.GetEngine(ctx).ID(m.ID).AllCols().Update(m) _, err := db.GetEngine(ctx).ID(m.ID).Cols("last_update", "last_error").Update(m)
return err return err
} }
// UpdatePushMirrorInterval updates the push-mirror // UpdatePushMirrorInterval updates the sync interval of the push-mirror
func UpdatePushMirrorInterval(ctx context.Context, m *PushMirror) error { func UpdatePushMirrorInterval(ctx context.Context, m *PushMirror) error {
_, err := db.GetEngine(ctx).ID(m.ID).Cols("interval").Update(m) _, err := db.GetEngine(ctx).ID(m.ID).Cols("interval").Update(m)
return err return err
-1
View File
@@ -60,7 +60,6 @@ func SyncDirs(srcPath, destPath string) error {
} }
// the keep file is used to keep the directory in a git repository, it doesn't need to be synced // the keep file is used to keep the directory in a git repository, it doesn't need to be synced
// and go-git doesn't work with the ".keep" file (it would report errors like "ref is empty")
const keepFile = ".keep" const keepFile = ".keep"
// find and delete all untracked files // find and delete all untracked files
+3 -3
View File
@@ -41,7 +41,7 @@ type SearchUserOptions struct {
Types []UserType Types []UserType
UID int64 UID int64
LoginName string // this option should be used only for admin user LoginName string // this option should be used only for admin user
SourceID int64 // this option should be used only for admin user SourceID optional.Option[int64] // this option should be used only for admin user, Some(0) means local users
OrderBy db.SearchOrderBy OrderBy db.SearchOrderBy
Visible []structs.VisibleType Visible []structs.VisibleType
Actor *User // The user doing the search Actor *User // The user doing the search
@@ -106,8 +106,8 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) db.Session
cond = cond.And(builder.Eq{"id": opts.UID}) cond = cond.And(builder.Eq{"id": opts.UID})
} }
if opts.SourceID > 0 { if opts.SourceID.Has() {
cond = cond.And(builder.Eq{"login_source": opts.SourceID}) cond = cond.And(builder.Eq{"login_source": opts.SourceID.Value()})
} }
if opts.LoginName != "" { if opts.LoginName != "" {
cond = cond.And(builder.Eq{"login_name": opts.LoginName}) cond = cond.And(builder.Eq{"login_name": opts.LoginName})
+4 -4
View File
@@ -225,7 +225,6 @@ func replaceScalars(node *yaml.Node, replace func(string) string) {
// buildMatrixCombos builds one Job per matrix combination from src, baking the combination into the // buildMatrixCombos builds one Job per matrix combination from src, baking the combination into the
// strategy and interpolating the name, runs-on and continue-on-error with it. // strategy and interpolating the name, runs-on and continue-on-error with it.
func buildMatrixCombos(jobID string, src *Job, matrixes []map[string]any, gitCtx *model.GithubContext, results map[string]*JobResult, vars map[string]string, inputs map[string]any) ([]*Job, error) { func buildMatrixCombos(jobID string, src *Job, matrixes []map[string]any, gitCtx *model.GithubContext, results map[string]*JobResult, vars map[string]string, inputs map[string]any) ([]*Job, error) {
srcRunsOn := model.RunsOnFromNode(src.RawRunsOn)
order, names := make([]int, len(matrixes)), make([]string, len(matrixes)) order, names := make([]int, len(matrixes)), make([]string, len(matrixes))
for index, matrix := range matrixes { for index, matrix := range matrixes {
order[index], names[index] = index, matrixName(matrix) order[index], names[index] = index, matrixName(matrix)
@@ -259,15 +258,16 @@ func buildMatrixCombos(jobID string, src *Job, matrixes []map[string]any, gitCtx
if err := evaluator.EvaluateYamlNode(&rawRunsOn); err != nil { if err := evaluator.EvaluateYamlNode(&rawRunsOn); err != nil {
return nil, fmt.Errorf("interpolate runs-on for job %q: %w", jobID, err) return nil, fmt.Errorf("interpolate runs-on for job %q: %w", jobID, err)
} }
if rawRunsOn.Kind != 0 && runsOnProblem(&rawRunsOn) != "" {
combo.RawRunsOn = rawRunsOn
} else {
runsOn := model.RunsOnFromNode(rawRunsOn) runsOn := model.RunsOnFromNode(rawRunsOn)
if len(runsOn) == 0 && len(srcRunsOn) > 0 { // match no runner rather than every runner
runsOn = []string{""}
}
for i := range runsOn { for i := range runsOn {
runsOn[i] = escapeExpressions(runsOn[i]) runsOn[i] = escapeExpressions(runsOn[i])
} }
combo.RawRunsOn = model.RunsOnNode(runsOn, "") combo.RawRunsOn = model.RunsOnNode(runsOn, "")
} }
}
if err := evaluator.EvaluateYamlNode(&combo.RawContinueOnError); err != nil { if err := evaluator.EvaluateYamlNode(&combo.RawContinueOnError); err != nil {
return nil, fmt.Errorf("evaluate continue-on-error for job %q: %w", jobID, err) return nil, fmt.Errorf("evaluate continue-on-error for job %q: %w", jobID, err)
} }
+42 -2
View File
@@ -290,17 +290,26 @@ func TestParseInterpolatesRunName(t *testing.T) {
assert.Empty(t, result[0].RunName) assert.Empty(t, result[0].RunName)
} }
func TestParseRunsOnFromJSONArray(t *testing.T) { func TestParseRunsOnFromJSONKeepsWhatGitHubRejectsForTheJobToFail(t *testing.T) {
content := []byte("on: push\njobs:\n build:\n runs-on: ${{ fromJSON(vars.RUNNER) }}\n steps: [{run: echo}]\n") content := []byte("on: push\njobs:\n build:\n runs-on: ${{ fromJSON(vars.RUNNER) }}\n steps: [{run: echo}]\n")
_, err := Parse(content) _, err := Parse(content)
require.NoError(t, err) require.NoError(t, err)
for runner, want := range map[string][]string{`["self-hosted", "linux"]`: {"self-hosted", "linux"}, "[]": {""}} { for runner, want := range map[string][]string{`["self-hosted", "linux"]`: {"self-hosted", "linux"}, "[]": {}, "{}": {}} {
result, err := Parse(content, WithGitContext(&model.GithubContext{}), WithVars(map[string]string{"RUNNER": runner})) result, err := Parse(content, WithGitContext(&model.GithubContext{}), WithVars(map[string]string{"RUNNER": runner}))
require.NoError(t, err) require.NoError(t, err)
require.Len(t, result, 1) require.Len(t, result, 1)
_, job := result[0].Job() _, job := result[0].Job()
assert.Equal(t, want, job.RunsOn(), runner) assert.Equal(t, want, job.RunsOn(), runner)
} }
for runner, problem := range map[string]string{`["a"]`: "", `""`: "Unexpected value ''", `[["a"]]`: "A sequence was not expected"} {
result, err := Parse(content, WithGitContext(&model.GithubContext{}), WithVars(map[string]string{"RUNNER": runner}))
require.NoError(t, err)
payload, err := result[0].Marshal()
require.NoError(t, err)
_, job, err := ParseRawSingleWorkflow(payload)
require.NoError(t, err)
assert.Equal(t, problem, job.RunsOnProblem(), runner)
}
} }
func TestJobFieldsWithoutMatrix(t *testing.T) { func TestJobFieldsWithoutMatrix(t *testing.T) {
@@ -458,6 +467,37 @@ func TestReadWorkflowJobConditionContexts(t *testing.T) {
} }
} }
func TestValidateWorkflowStaticJobKindAndRunsOnLikeGitHub(t *testing.T) {
for job, want := range map[string]string{
"{runs-on: x, steps: [{run: echo}]}": "",
"{uses: o/r/.gitea/workflows/c.yml@main}": "",
"{runs-on: []}": "",
"{runs-on: {}}": "",
"{runs-on: {group: org/g, labels: [a, 1]}}": "",
"{runs-on: {group: '${{ vars.G }}'}}": "",
"{steps: [{run: echo}]}": "Required property is missing: runs-on",
"{with: {}}": "Required property is missing: uses",
"{runs-on: x, uses: o/r/.gitea/workflows/c.yml@main}": "Unexpected value 'uses'",
"{Runs-On: x}": "Unexpected value 'Runs-On'",
"{runs-on: ~}": "runs-on: Unexpected value ''",
"{runs-on: ['']}": "runs-on: Unexpected value ''",
"{runs-on: [[a]]}": "runs-on: A sequence was not expected",
"{runs-on: {labels: {a: b}}}": "runs-on: A mapping was not expected",
"{runs-on: {foo: x}}": "runs-on: Unexpected value 'foo'",
"{runs-on: {group: org/}}": "runs-on: Invalid runs-on group name 'org/'.",
"{runs-on: {group: a/b/c}}": "runs-on: Invalid runs-on group name 'a/b/c'. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.",
"{if: true}": "There's not enough info to determine what you meant. Add one of these properties: " +
"cancel-timeout-minutes, container, continue-on-error, defaults, env, environment, outputs, runs-on, secrets, services, snapshot, steps, timeout-minutes, uses, with",
} {
_, err := ValidateWorkflowStatic([]byte("on: push\njobs:\n build: " + job + "\n"))
if want == "" {
assert.NoError(t, err, job)
} else {
assert.EqualError(t, err, "job build: "+want, job)
}
}
}
func TestRejectsUnevaluatedMatrixFilters(t *testing.T) { func TestRejectsUnevaluatedMatrixFilters(t *testing.T) {
for _, filter := range []string{"include", "exclude"} { for _, filter := range []string{"include", "exclude"} {
t.Run(filter, func(t *testing.T) { t.Run(filter, func(t *testing.T) {
+8
View File
@@ -174,6 +174,14 @@ func (j *Job) EraseNeeds() *Job {
return j return j
} }
// RunsOnProblem returns github.com's error for the job's runs-on, "" if valid.
func (j *Job) RunsOnProblem() string {
if j.RawRunsOn.Kind == 0 {
return ""
}
return runsOnProblem(&j.RawRunsOn)
}
// RunsOn returns the labels Gitea matches runners against, unescaped like DisplayName. // RunsOn returns the labels Gitea matches runners against, unescaped like DisplayName.
func (j *Job) RunsOn() []string { func (j *Job) RunsOn() []string {
runsOn := model.RunsOnFromNode(j.RawRunsOn) runsOn := model.RunsOnFromNode(j.RawRunsOn)
+120 -2
View File
@@ -7,10 +7,13 @@ import (
"errors" "errors"
"fmt" "fmt"
"slices" "slices"
"strings"
"gitea.dev/actionslib/pkg/expreval" "gitea.dev/actionslib/pkg/expreval"
"gitea.dev/actionslib/pkg/exprparser" "gitea.dev/actionslib/pkg/exprparser"
"gitea.dev/actionslib/pkg/model" "gitea.dev/actionslib/pkg/model"
"go.yaml.in/yaml/v4"
) )
// jobConditionContexts are what github.com gives `jobs.<job_id>.if`, which it decides before the matrix, plus the `gitea` alias. // jobConditionContexts are what github.com gives `jobs.<job_id>.if`, which it decides before the matrix, plus the `gitea` alias.
@@ -21,7 +24,7 @@ func ValidateWorkflowStatic(content []byte) ([]*Event, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Keep unknown and case-distinct keys accepted for existing Gitea workflows. // Keep unknown and case-distinct keys outside of jobs accepted for existing Gitea workflows.
workflow, err := readWorkflowDoc(doc) workflow, err := readWorkflowDoc(doc)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -33,6 +36,9 @@ func ValidateWorkflowStatic(content []byte) ([]*Event, error) {
if err := validateWorkflowStructure(workflow); err != nil { if err := validateWorkflowStructure(workflow); err != nil {
return nil, err return nil, err
} }
if err := validateJobKinds(doc); err != nil {
return nil, err
}
var header struct { var header struct {
RunName string `yaml:"run-name"` RunName string `yaml:"run-name"`
} }
@@ -76,7 +82,6 @@ func validateWorkflowStructure(workflow *model.Workflow) error {
if job == nil { if job == nil {
return fmt.Errorf("job %q has no configuration", id) return fmt.Errorf("job %q has no configuration", id)
} }
// a job without runs-on is accepted and runs on any runner, github.com rejects it
for _, dependency := range job.Needs() { for _, dependency := range job.Needs() {
if _, ok := workflow.Jobs[dependency]; !ok { if _, ok := workflow.Jobs[dependency]; !ok {
return fmt.Errorf("job %q needs unknown job %q", id, dependency) return fmt.Errorf("job %q needs unknown job %q", id, dependency)
@@ -110,3 +115,116 @@ func validateWorkflowStructure(workflow *model.Workflow) error {
} }
return nil return nil
} }
// job keys of github.com's workflow schema, by the kind of job allowing them
var (
stepsJobKeys = []string{"cancel-timeout-minutes", "container", "continue-on-error", "defaults", "env", "environment", "outputs", "runs-on", "services", "snapshot", "steps", "timeout-minutes"}
callerJobKeys = []string{"secrets", "uses", "with"}
sharedJobKeys = []string{"concurrency", "if", "name", "needs", "permissions", "strategy"}
)
// validateJobKinds applies github.com's job kinds, decided by the first kind-specific key.
func validateJobKinds(doc *yaml.Node) error {
jobs := mappingValue(doc.Content[0], "jobs")
for i := 0; i+1 < len(jobs.Content); i += 2 {
id, job := jobs.Content[i].Value, jobs.Content[i+1]
var required string
for j := 0; j+1 < len(job.Content); j += 2 {
key := job.Content[j].Value
isStepsKey, isCallerKey := slices.Contains(stepsJobKeys, key), slices.Contains(callerJobKeys, key)
switch {
case required == "runs-on" && isCallerKey, required == "uses" && isStepsKey, !isStepsKey && !isCallerKey && !slices.Contains(sharedJobKeys, key):
return fmt.Errorf("job %s: Unexpected value '%s'", id, key)
case required == "" && isStepsKey:
required = "runs-on"
case required == "" && isCallerKey:
required = "uses"
}
}
if required == "" {
keys := slices.Concat(stepsJobKeys, callerJobKeys)
slices.Sort(keys)
return fmt.Errorf("job %s: There's not enough info to determine what you meant. Add one of these properties: %s", id, strings.Join(keys, ", "))
}
value := mappingValue(job, required)
if value == nil {
return fmt.Errorf("job %s: Required property is missing: %s", id, required)
}
if required != "runs-on" {
continue
}
if problem := runsOnProblem(value); problem != "" {
return fmt.Errorf("job %s: runs-on: %s", id, problem)
}
}
return nil
}
// runsOnProblem returns github.com's schema error for a runs-on, "" if valid.
func runsOnProblem(node *yaml.Node) string {
if node.Kind != yaml.MappingNode {
return runsOnLabelsProblem(node)
}
for i := 0; i+1 < len(node.Content); i += 2 {
var problem string
switch key := node.Content[i].Value; key {
case "labels":
problem = runsOnLabelsProblem(node.Content[i+1])
case "group":
problem = runsOnGroupProblem(node.Content[i+1])
default:
problem = fmt.Sprintf("Unexpected value '%s'", key)
}
if problem != "" {
return problem
}
}
return ""
}
func runsOnLabelsProblem(node *yaml.Node) string {
if node.Kind != yaml.SequenceNode {
return nonEmptyStringProblem(node)
}
for _, label := range node.Content {
if problem := nonEmptyStringProblem(label); problem != "" {
return problem
}
}
return ""
}
func runsOnGroupProblem(node *yaml.Node) string {
if problem := nonEmptyStringProblem(node); problem != "" || hasExpression(node.Value) {
return problem
}
switch prefix, name, found := strings.Cut(node.Value, "/"); {
case found && name == "":
return fmt.Sprintf("Invalid runs-on group name '%s'.", node.Value)
case found && (strings.Contains(name, "/") || !slices.Contains([]string{"org", "organization", "ent", "enterprise"}, prefix)):
return fmt.Sprintf("Invalid runs-on group name '%s'. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.", node.Value)
}
return ""
}
// nonEmptyStringProblem mirrors github.com's non-empty-string, which also accepts non-string scalars.
func nonEmptyStringProblem(node *yaml.Node) string {
switch {
case node.Kind == yaml.SequenceNode:
return "A sequence was not expected"
case node.Kind == yaml.MappingNode:
return "A mapping was not expected"
case node.Value == "" || node.ShortTag() == "!!null":
return "Unexpected value ''"
}
return ""
}
func mappingValue(node *yaml.Node, key string) *yaml.Node {
for i := 0; i+1 < len(node.Content); i += 2 {
if node.Content[i].Value == key {
return node.Content[i+1]
}
}
return nil
}
+11 -6
View File
@@ -31,17 +31,22 @@ jobs:
func TestReadWorkflowEventsStaticErrors(t *testing.T) { func TestReadWorkflowEventsStaticErrors(t *testing.T) {
for content, static := range map[string]bool{ for content, static := range map[string]bool{
"on: push\njobs: {}": true, "on: push\njobs: {}": true,
"on: push\njobs: {test: {needs: absent}}": true, "on: push\njobs: {test: {runs-on: x, needs: absent}}": true,
"on: push\njobs: {one: {needs: two}, two: {needs: one}}": true, "on: push\njobs: {one: {runs-on: x, needs: two}, two: {runs-on: x, needs: one}}": true,
"on: push\njobs: {test: {strategy: {matrix: {os: []}}}}": true, "on: push\njobs: {test: {runs-on: x, strategy: {matrix: {os: []}}}}": true,
"on: push\nrun-name: ${{ secrets.TOKEN }}\njobs: {test: {}}": true, "on: push\nrun-name: ${{ secrets.TOKEN }}\njobs: {test: {runs-on: x}}": true,
"on: push\nrun-name: ${{ fromJSON(inputs.x) }}\njobs: {test: {steps: [{run: echo}]}}": false, "on: push\njobs: {test: {steps: [{run: echo}]}}": true,
"on: push\nrun-name: ${{ fromJSON(inputs.x) }}\njobs: {test: {runs-on: x, steps: [{run: echo}]}}": false,
} { } {
_, gotStatic, err := readWorkflowEvents([]byte(content)) _, gotStatic, err := readWorkflowEvents([]byte(content))
require.Error(t, err, content) require.Error(t, err, content)
assert.Equal(t, static, gotStatic, content) assert.Equal(t, static, gotStatic, content)
} }
for _, content := range []string{"on: push\njobs: {test: {steps: [{run: echo}]}}", "on: push\nrun-name: ${{ github.ref }}\njobs: {test: {}}"} { for _, content := range []string{
"on: push\njobs: {test: {runs-on: x, steps: [{run: echo}]}}",
"on: push\nrun-name: ${{ github.ref }}\njobs: {test: {runs-on: x}}",
"on: push\njobs: {call: {uses: ./.gitea/workflows/called.yml}}",
} {
_, _, err := readWorkflowEvents([]byte(content)) _, _, err := readWorkflowEvents([]byte(content))
assert.NoError(t, err, content) assert.NoError(t, err, content)
} }
+1 -3
View File
@@ -13,8 +13,6 @@ import (
"gitea.dev/modules/setting" "gitea.dev/modules/setting"
"gitea.dev/modules/util" "gitea.dev/modules/util"
_ "gitea.com/go-chi/cache/memcache" //nolint:depguard // memcache plugin for cache, it is required for config "ADAPTER=memcache"
) )
var defaultCache StringCache var defaultCache StringCache
@@ -83,7 +81,7 @@ func GetCache() StringCache {
// GetString returns the key value from cache with callback when no key exists in cache // GetString returns the key value from cache with callback when no key exists in cache
func GetString(key string, getFunc func() (string, error)) (string, error) { func GetString(key string, getFunc func() (string, error)) (string, error) {
if defaultCache == nil || setting.CacheService.TTL == 0 { if defaultCache == nil || setting.CacheService.TTL <= 0 {
return getFunc() return getFunc()
} }
cached, exist := defaultCache.Get(key) cached, exist := defaultCache.Get(key)
+70
View File
@@ -0,0 +1,70 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cache
import (
"errors"
"fmt"
"math"
"strings"
"github.com/bradfitz/gomemcache/memcache"
)
const memcacheMaxRelativeTTL = 30 * 24 * 60 * 60
type memcacheCache struct {
client *memcache.Client
}
func newMemcacheCache(conn string) (backend, error) {
if conn == "" {
return nil, errors.New("cache adapter memcache requires [cache] HOST, e.g. 127.0.0.1:11211")
}
servers := &memcache.ServerList{}
if err := servers.SetServers(strings.Split(conn, ";")...); err != nil {
return nil, fmt.Errorf("invalid memcache HOST %q: %w", conn, err)
}
return &memcacheCache{client: memcache.NewFromSelector(servers)}, nil
}
// memcacheExpiration converts a TTL beyond memcached's 30-day relative limit into an absolute unix time
func memcacheExpiration(ttl int64) int32 {
if ttl > memcacheMaxRelativeTTL {
ttl += timeNow().Unix()
}
return int32(min(ttl, math.MaxInt32))
}
func (c *memcacheCache) Get(key string) (string, bool) {
item, err := c.client.Get(key)
if err != nil {
return "", false
}
return string(item.Value), true
}
func (c *memcacheCache) Put(key, value string, ttl int64) error {
return c.client.Set(&memcache.Item{Key: key, Value: []byte(value), Expiration: memcacheExpiration(ttl)})
}
func (c *memcacheCache) Delete(key string) error {
if err := c.client.Delete(key); err != nil && !errors.Is(err, memcache.ErrCacheMiss) {
return err
}
return nil
}
func (c *memcacheCache) IsExist(key string) bool {
_, err := c.client.Get(key)
return err == nil
}
func (c *memcacheCache) Ping() error {
const key = "__gitea_cache_ping"
if err := c.Put(key, "ping", 10); err != nil {
return err
}
return c.Delete(key)
}
+18 -133
View File
@@ -4,159 +4,44 @@
package cache package cache
import ( import (
"fmt"
"strconv"
"time" "time"
"gitea.dev/modules/graceful" "gitea.dev/modules/graceful"
"gitea.dev/modules/nosql" "gitea.dev/modules/nosql"
"gitea.com/go-chi/cache" //nolint:depguard // we wrap this package here
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
) )
// RedisCacher represents a redis cache adapter implementation. type redisCache struct {
type RedisCacher struct { client redis.UniversalClient
c redis.UniversalClient
prefix string prefix string
hsetName string
occupyMode bool
} }
// toStr convert string/int/int64 interface to string. it's only used by the RedisCacher.Put internally func newRedisCache(conn string) backend {
func toStr(v any) string { uri := nosql.ToRedisURI(conn)
if v == nil { return &redisCache{
return "" client: nosql.GetManager().GetRedisClient(uri.String()),
} prefix: uri.Query().Get("prefix"),
switch v := v.(type) {
case string:
return v
case []byte:
return string(v)
case int:
return strconv.FormatInt(int64(v), 10)
case int64:
return strconv.FormatInt(v, 10)
default:
return fmt.Sprint(v) // as what the old com.ToStr does in most cases
} }
} }
// Put puts value (string type) into cache with key and expire time. func (c *redisCache) Get(key string) (string, bool) {
// If expired is 0, it lives forever. value, err := c.client.Get(graceful.GetManager().HammerContext(), c.prefix+key).Result()
func (c *RedisCacher) Put(key string, val any, expire int64) error { return value, err == nil
// this function is not well-designed, it only puts string values into cache
key = c.prefix + key
if expire == 0 {
if err := c.c.Set(graceful.GetManager().HammerContext(), key, toStr(val), 0).Err(); err != nil {
return err
}
} else {
dur := time.Duration(expire) * time.Second
if err := c.c.Set(graceful.GetManager().HammerContext(), key, toStr(val), dur).Err(); err != nil {
return err
}
} }
if c.occupyMode { func (c *redisCache) Put(key, value string, ttl int64) error {
return nil return c.client.Set(graceful.GetManager().HammerContext(), c.prefix+key, value, time.Duration(ttl)*time.Second).Err()
}
return c.c.HSet(graceful.GetManager().HammerContext(), c.hsetName, key, "0").Err()
} }
// Get gets cached value by given key. func (c *redisCache) Delete(key string) error {
func (c *RedisCacher) Get(key string) any { return c.client.Del(graceful.GetManager().HammerContext(), c.prefix+key).Err()
val, err := c.c.Get(graceful.GetManager().HammerContext(), c.prefix+key).Result()
if err != nil {
return nil
}
return val
} }
// Delete deletes cached value by given key. func (c *redisCache) IsExist(key string) bool {
func (c *RedisCacher) Delete(key string) error { return c.client.Exists(graceful.GetManager().HammerContext(), c.prefix+key).Val() == 1
key = c.prefix + key
if err := c.c.Del(graceful.GetManager().HammerContext(), key).Err(); err != nil {
return err
} }
if c.occupyMode { func (c *redisCache) Ping() error {
return nil return c.client.Ping(graceful.GetManager().HammerContext()).Err()
}
return c.c.HDel(graceful.GetManager().HammerContext(), c.hsetName, key).Err()
}
// Incr increases cached int-type value by given key as a counter.
func (c *RedisCacher) Incr(key string) error {
if !c.IsExist(key) {
return fmt.Errorf("key '%s' not exist", key)
}
return c.c.Incr(graceful.GetManager().HammerContext(), c.prefix+key).Err()
}
// Decr decreases cached int-type value by given key as a counter.
func (c *RedisCacher) Decr(key string) error {
if !c.IsExist(key) {
return fmt.Errorf("key '%s' not exist", key)
}
return c.c.Decr(graceful.GetManager().HammerContext(), c.prefix+key).Err()
}
// IsExist returns true if cached value exists.
func (c *RedisCacher) IsExist(key string) bool {
if c.c.Exists(graceful.GetManager().HammerContext(), c.prefix+key).Val() == 1 {
return true
}
if !c.occupyMode {
c.c.HDel(graceful.GetManager().HammerContext(), c.hsetName, c.prefix+key)
}
return false
}
// Flush deletes all cached data.
func (c *RedisCacher) Flush() error {
if c.occupyMode {
return c.c.FlushDB(graceful.GetManager().HammerContext()).Err()
}
keys, err := c.c.HKeys(graceful.GetManager().HammerContext(), c.hsetName).Result()
if err != nil {
return err
}
if err = c.c.Del(graceful.GetManager().HammerContext(), keys...).Err(); err != nil {
return err
}
return c.c.Del(graceful.GetManager().HammerContext(), c.hsetName).Err()
}
// StartAndGC starts GC routine based on config string settings.
// AdapterConfig: network=tcp,addr=:6379,password=macaron,db=0,pool_size=100,idle_timeout=180,hset_name=MacaronCache,prefix=cache:
func (c *RedisCacher) StartAndGC(opts cache.Options) error {
c.hsetName = "MacaronCache"
c.occupyMode = opts.OccupyMode
uri := nosql.ToRedisURI(opts.AdapterConfig)
c.c = nosql.GetManager().GetRedisClient(uri.String())
for k, v := range uri.Query() {
switch k {
case "hset_name":
c.hsetName = v[0]
case "prefix":
c.prefix = v[0]
}
}
return c.c.Ping(graceful.GetManager().HammerContext()).Err()
}
// Ping tests if the cache is alive.
func (c *RedisCacher) Ping() error {
return c.c.Ping(graceful.GetManager().HammerContext()).Err()
}
func init() {
cache.Register("redis", &RedisCacher{})
} }
+95
View File
@@ -8,9 +8,12 @@ import (
"testing" "testing"
"time" "time"
"gitea.dev/modules/nosql"
"gitea.dev/modules/setting" "gitea.dev/modules/setting"
"gitea.dev/modules/test"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func createTestCache() { func createTestCache() {
@@ -32,6 +35,98 @@ func TestNewContext(t *testing.T) {
}) })
assert.Error(t, err) assert.Error(t, err)
assert.Nil(t, con) assert.Nil(t, con)
_, err = NewStringCache(setting.Cache{Adapter: "memcache"})
assert.ErrorContains(t, err, "requires [cache] HOST")
}
func TestStringCacheAdapters(t *testing.T) {
now := time.Now()
defer test.MockVariableValue(&timeNow, func() time.Time { return now })()
cases := []struct {
adapter string
conns func(t *testing.T) (string, string)
}{
{adapter: "memory", conns: func(*testing.T) (string, string) { return "", "" }},
{adapter: "twoqueue", conns: func(*testing.T) (string, string) { return "100", `{"size":100}` }},
{adapter: "redis", conns: func(t *testing.T) (string, string) {
conn := test.PrepareTestRedis(t) + "?prefix=gitea-test-cache-"
return conn + "first:", conn + "second:"
}},
}
for _, tc := range cases {
t.Run(tc.adapter, func(t *testing.T) {
firstConn, secondConn := tc.conns(t)
first, err := NewStringCache(setting.Cache{Adapter: tc.adapter, Conn: firstConn, Interval: -1})
require.NoError(t, err)
second, err := NewStringCache(setting.Cache{Adapter: tc.adapter, Conn: secondConn, Interval: -1})
require.NoError(t, err)
require.NoError(t, first.Ping())
require.NoError(t, first.Put("key", "value", 0))
value, ok := first.Get("key")
assert.True(t, ok)
assert.Equal(t, "value", value)
assert.True(t, first.IsExist("key"))
assert.False(t, second.IsExist("key"))
require.NoError(t, first.Delete("key"))
_, ok = first.Get("key")
assert.False(t, ok)
assert.False(t, first.IsExist("key"))
require.NoError(t, first.Delete("key"))
require.NoError(t, first.Put("expiring", "value", 10))
if tc.adapter == "redis" {
uri := nosql.ToRedisURI(firstConn)
ttl := nosql.GetManager().GetRedisClient(uri.String()).TTL(t.Context(), uri.Query().Get("prefix")+"expiring").Val()
assert.Positive(t, ttl)
assert.LessOrEqual(t, ttl, 10*time.Second)
require.NoError(t, first.Delete("expiring"))
return
}
now = now.Add(9 * time.Second)
assert.True(t, first.IsExist("expiring"))
now = now.Add(time.Second)
assert.False(t, first.IsExist("expiring"))
_, ok = first.Get("expiring")
assert.False(t, ok)
require.NoError(t, first.Put("expiring", "renewed", 0))
value, ok = first.Get("expiring")
assert.True(t, ok)
assert.Equal(t, "renewed", value)
})
}
}
func TestNegativeItemTTLDisablesOnlyItemTTLCaching(t *testing.T) {
createTestCache()
defer test.MockVariableValue(&setting.CacheService.TTL, -1)()
require.NoError(t, defaultCache.Put("key", "stale", 0))
require.NoError(t, defaultCache.Put("key", "value", setting.CacheService.TTLSeconds()))
assert.False(t, defaultCache.IsExist("key"))
calls := 0
for range 2 {
data, err := GetString("key", func() (string, error) {
calls++
return "value", nil
})
assert.NoError(t, err)
assert.Equal(t, "value", data)
}
assert.Equal(t, 2, calls)
assert.False(t, defaultCache.IsExist("key"))
require.NoError(t, defaultCache.Put("captcha", "value", 600))
assert.True(t, defaultCache.IsExist("captcha"))
}
func TestMemcacheExpiration(t *testing.T) {
defer test.MockVariableValue(&timeNow, func() time.Time { return time.Unix(1000, 0) })()
assert.EqualValues(t, memcacheMaxRelativeTTL, memcacheExpiration(memcacheMaxRelativeTTL))
assert.EqualValues(t, 1000+memcacheMaxRelativeTTL+1, memcacheExpiration(memcacheMaxRelativeTTL+1))
} }
func TestTest(t *testing.T) { func TestTest(t *testing.T) {
+78 -160
View File
@@ -4,205 +4,123 @@
package cache package cache
import ( import (
"fmt"
"strconv" "strconv"
"sync"
"time" "time"
"gitea.dev/modules/json" "gitea.dev/modules/json"
mc "gitea.com/go-chi/cache" //nolint:depguard // we wrap this package here
lru "github.com/hashicorp/golang-lru/v2" lru "github.com/hashicorp/golang-lru/v2"
) )
// TwoQueueCache represents a LRU 2Q cache adapter implementation const twoQueueDefaultSize = 50000
type TwoQueueCache struct {
lock sync.Mutex type twoQueueCache struct {
cache *lru.TwoQueueCache[string, any] cache *lru.TwoQueueCache[string, memoryItem] // wrap the existing thread-safe 2Q (two-queue) cache directly
interval int
} }
// TwoQueueCacheConfig describes the configuration for TwoQueueCache type twoQueueCacheConfig struct {
type TwoQueueCacheConfig struct { Size int `json:"size"`
Size int `ini:"SIZE" json:"size"` RecentRatio float64 `json:"recent_ratio"`
RecentRatio float64 `ini:"RECENT_RATIO" json:"recent_ratio"` GhostRatio float64 `json:"ghost_ratio"`
GhostRatio float64 `ini:"GHOST_RATIO" json:"ghost_ratio"`
} }
// MemoryItem represents a memory cache item. func newTwoQueueCache(conn string, gcInterval time.Duration) (backend, error) {
type MemoryItem struct { lruCache, err := newTwoQueueLRU(conn)
Val any if err != nil {
Created int64 return nil, err
Timeout int64 }
cache := &twoQueueCache{cache: lruCache}
startGC(gcInterval, cache.deleteExpired)
return cache, nil
} }
func (item *MemoryItem) hasExpired() bool { func newTwoQueueLRU(conn string) (*lru.TwoQueueCache[string, memoryItem], error) {
return item.Timeout > 0 && if conn == "" {
(time.Now().Unix()-item.Created) >= item.Timeout return lru.New2Q[string, memoryItem](twoQueueDefaultSize)
}
if size, err := strconv.Atoi(conn); err == nil {
return lru.New2Q[string, memoryItem](size)
}
if !json.Valid([]byte(conn)) {
return nil, fmt.Errorf("invalid two-queue cache HOST %q, expected a size or a JSON config", conn)
}
config := twoQueueCacheConfig{
Size: twoQueueDefaultSize,
RecentRatio: lru.Default2QRecentRatio,
GhostRatio: lru.Default2QGhostEntries,
}
_ = json.Unmarshal([]byte(conn), &config)
return lru.New2QParams[string, memoryItem](config.Size, config.RecentRatio, config.GhostRatio)
} }
var _ mc.Cache = &TwoQueueCache{} func (c *twoQueueCache) Get(key string) (string, bool) {
item, ok := c.cache.Get(key)
// Put puts value into cache with key and expire time. if !ok {
func (c *TwoQueueCache) Put(key string, val any, timeout int64) error { return "", false
item := &MemoryItem{
Val: val,
Created: time.Now().Unix(),
Timeout: timeout,
} }
c.lock.Lock() if item.expired(timeNow()) {
defer c.lock.Unlock() c.cache.Remove(key)
return "", false
}
return item.value, true
}
func (c *twoQueueCache) Put(key, value string, ttl int64) error {
item := newMemoryItem(value, ttl)
c.cache.Add(key, item) c.cache.Add(key, item)
return nil return nil
} }
// Get gets cached value by given key. func (c *twoQueueCache) Delete(key string) error {
func (c *TwoQueueCache) Get(key string) any {
c.lock.Lock()
defer c.lock.Unlock()
cached, ok := c.cache.Get(key)
if !ok {
return nil
}
item, ok := cached.(*MemoryItem)
if !ok || item.hasExpired() {
c.cache.Remove(key) c.cache.Remove(key)
return nil return nil
} }
return item.Val func (c *twoQueueCache) IsExist(key string) bool {
item, ok := c.cache.Peek(key)
return ok && !item.expired(timeNow())
} }
// Delete deletes cached value by given key. func (c *twoQueueCache) Ping() error {
func (c *TwoQueueCache) Delete(key string) error {
c.lock.Lock()
defer c.lock.Unlock()
c.cache.Remove(key)
return nil return nil
} }
// Incr increases cached int-type value by given key as a counter. func (c *twoQueueCache) deleteExpired() {
func (c *TwoQueueCache) Incr(key string) error { now := timeNow()
c.lock.Lock()
defer c.lock.Unlock()
cached, ok := c.cache.Get(key)
if !ok {
return nil
}
item, ok := cached.(*MemoryItem)
if !ok || item.hasExpired() {
c.cache.Remove(key)
return nil
}
var err error
item.Val, err = mc.Incr(item.Val)
return err
}
// Decr decreases cached int-type value by given key as a counter.
func (c *TwoQueueCache) Decr(key string) error {
c.lock.Lock()
defer c.lock.Unlock()
cached, ok := c.cache.Get(key)
if !ok {
return nil
}
item, ok := cached.(*MemoryItem)
if !ok || item.hasExpired() {
c.cache.Remove(key)
return nil
}
var err error
item.Val, err = mc.Decr(item.Val)
return err
}
// IsExist returns true if cached value exists.
func (c *TwoQueueCache) IsExist(key string) bool {
c.lock.Lock()
defer c.lock.Unlock()
cached, ok := c.cache.Peek(key)
if !ok {
return false
}
item, ok := cached.(*MemoryItem)
if !ok || item.hasExpired() {
c.cache.Remove(key)
return false
}
return true
}
// Flush deletes all cached data.
func (c *TwoQueueCache) Flush() error {
c.lock.Lock()
defer c.lock.Unlock()
c.cache.Purge()
return nil
}
func (c *TwoQueueCache) checkAndInvalidate(key string) {
c.lock.Lock()
defer c.lock.Unlock()
cached, ok := c.cache.Peek(key)
if !ok {
return
}
item, ok := cached.(*MemoryItem)
if !ok || item.hasExpired() {
c.cache.Remove(key)
}
}
func (c *TwoQueueCache) startGC() {
if c.interval < 0 {
return
}
for _, key := range c.cache.Keys() { for _, key := range c.cache.Keys() {
c.checkAndInvalidate(key) if item, ok := c.cache.Peek(key); ok && item.expired(now) {
// here might be a slight data-race: the item might have been removed or updated by another goroutine between the Peek and Remove calls,
// but it's acceptable since the cache is not guaranteed to be 100% accurate and any item can be evicted at any time
c.cache.Remove(key)
}
} }
time.AfterFunc(time.Duration(c.interval)*time.Second, c.startGC)
} }
// StartAndGC starts GC routine based on config string settings. type memoryItem struct {
func (c *TwoQueueCache) StartAndGC(opts mc.Options) error { value string
var err error expiresAt time.Time
size := 50000
if opts.AdapterConfig != "" {
size, err = strconv.Atoi(opts.AdapterConfig)
}
if err != nil {
if !json.Valid([]byte(opts.AdapterConfig)) {
return err
} }
cfg := &TwoQueueCacheConfig{ func newMemoryItem(value string, ttl int64) memoryItem {
Size: 50000, item := memoryItem{value: value}
RecentRatio: lru.Default2QRecentRatio, if ttl > 0 {
GhostRatio: lru.Default2QGhostEntries, item.expiresAt = timeNow().Add(time.Duration(ttl) * time.Second)
} }
_ = json.Unmarshal([]byte(opts.AdapterConfig), cfg) return item
c.cache, err = lru.New2QParams[string, any](cfg.Size, cfg.RecentRatio, cfg.GhostRatio)
} else {
c.cache, err = lru.New2Q[string, any](size)
}
c.interval = opts.Interval
if c.interval > 0 {
go c.startGC()
}
return err
} }
// Ping tests if the cache is alive. func (item memoryItem) expired(now time.Time) bool {
func (c *TwoQueueCache) Ping() error { return !item.expiresAt.IsZero() && !now.Before(item.expiresAt)
return mc.GenericPing(c)
} }
func init() { func startGC(interval time.Duration, deleteExpired func()) {
mc.Register("twoqueue", &TwoQueueCache{}) if interval <= 0 {
return
}
go func() {
for range time.Tick(interval) {
deleteExpired()
}
}()
} }
+33 -36
View File
@@ -5,13 +5,14 @@ package cache
import ( import (
"errors" "errors"
"fmt"
"strconv"
"strings" "strings"
"time"
"gitea.dev/modules/json" "gitea.dev/modules/json"
"gitea.dev/modules/setting" "gitea.dev/modules/setting"
"gitea.dev/modules/util" "gitea.dev/modules/util"
chi_cache "gitea.com/go-chi/cache" //nolint:depguard // we wrap this package here
) )
type GetJSONError struct { type GetJSONError struct {
@@ -30,57 +31,57 @@ type StringCache interface {
Ping() error Ping() error
Get(key string) (string, bool) Get(key string) (string, bool)
Put(key, value string, ttl int64) error Put(key, value string, ttl int64) error // ttl in seconds, 0 never expires, negative removes the key
Delete(key string) error Delete(key string) error
IsExist(key string) bool IsExist(key string) bool
PutJSON(key string, v any, ttl int64) error PutJSON(key string, v any, ttl int64) error
GetJSON(key string, ptr any) (exist bool, err *GetJSONError) GetJSON(key string, ptr any) (exist bool, err *GetJSONError)
}
ChiCache() chi_cache.Cache type backend interface {
Get(key string) (string, bool)
Put(key, value string, ttl int64) error
Delete(key string) error
IsExist(key string) bool
Ping() error
} }
type stringCache struct { type stringCache struct {
chiCache chi_cache.Cache backend
} }
func NewStringCache(cacheConfig setting.Cache) (StringCache, error) { func NewStringCache(cacheConfig setting.Cache) (StringCache, error) {
adapter := util.IfZero(cacheConfig.Adapter, "memory") cacheBackend, err := newBackend(cacheConfig)
interval := util.IfZero(cacheConfig.Interval, 60)
cc, err := chi_cache.NewCacher(chi_cache.Options{
Adapter: adapter,
AdapterConfig: cacheConfig.Conn,
Interval: interval,
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &stringCache{chiCache: cc}, nil return &stringCache{backend: cacheBackend}, nil
} }
func (sc *stringCache) Ping() error { func newBackend(cacheConfig setting.Cache) (backend, error) {
return sc.chiCache.Ping() gcInterval := time.Duration(util.IfZero(cacheConfig.Interval, 60)) * time.Second
switch adapter := util.IfZero(cacheConfig.Adapter, "memory"); adapter {
case "memory":
// the old "memory" adapter doesn't have a limit, which can lead to OOM
// now, use two-queue cache for in-memory cache with items limit, at most a few GB of memory will be used
return newTwoQueueCache(strconv.FormatInt(10*1024*1024, 10), gcInterval)
case "twoqueue":
return newTwoQueueCache(cacheConfig.Conn, gcInterval)
case "redis":
return newRedisCache(cacheConfig.Conn), nil
case "memcache":
return newMemcacheCache(cacheConfig.Conn)
default:
return nil, fmt.Errorf("unknown cache adapter %q", adapter)
} }
func (sc *stringCache) Get(key string) (string, bool) {
v := sc.chiCache.Get(key)
if v == nil {
return "", false
}
s, ok := v.(string)
return s, ok
} }
func (sc *stringCache) Put(key, value string, ttl int64) error { func (sc *stringCache) Put(key, value string, ttl int64) error {
return sc.chiCache.Put(key, value, ttl) if ttl < 0 {
return sc.backend.Delete(key)
} }
return sc.backend.Put(key, value, ttl)
func (sc *stringCache) Delete(key string) error {
return sc.chiCache.Delete(key)
}
func (sc *stringCache) IsExist(key string) bool {
return sc.chiCache.IsExist(key)
} }
const cachedErrorPrefix = "<CACHED-ERROR>:" const cachedErrorPrefix = "<CACHED-ERROR>:"
@@ -97,7 +98,7 @@ func (sc *stringCache) PutJSON(key string, v any, ttl int64) error {
} }
s = util.UnsafeBytesToString(b) s = util.UnsafeBytesToString(b)
} }
return sc.chiCache.Put(key, s, ttl) return sc.Put(key, s, ttl)
} }
func (sc *stringCache) GetJSON(key string, ptr any) (exist bool, getErr *GetJSONError) { func (sc *stringCache) GetJSON(key string, ptr any) (exist bool, getErr *GetJSONError) {
@@ -114,7 +115,3 @@ func (sc *stringCache) GetJSON(key string, ptr any) (exist bool, getErr *GetJSON
} }
return true, nil return true, nil
} }
func (sc *stringCache) ChiCache() chi_cache.Cache {
return sc.chiCache
}
+40 -1
View File
@@ -8,11 +8,13 @@ import (
"fmt" "fmt"
"html" "html"
"io" "io"
"strings"
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
"gitea.dev/modules/setting" "gitea.dev/modules/setting"
"gitea.dev/modules/translation" "gitea.dev/modules/translation"
"gitea.dev/modules/util"
) )
type htmlChunkReader struct { type htmlChunkReader struct {
@@ -30,6 +32,10 @@ type escapeStreamer struct {
ambiguousTables []*AmbiguousTable ambiguousTables []*AmbiguousTable
allowed map[rune]bool allowed map[rune]bool
tagPartial []byte // partial tag content, used to detect if we are in some tags
inTagMath bool // MathML operators like U+2212 are intended and wrapping them breaks the math layout
out io.Writer out io.Writer
} }
@@ -62,6 +68,7 @@ func escapeStream(locale translation.Locale, in io.Reader, out io.Writer, opts .
for i, part := range parts { for i, part := range parts {
if partInTag[i] { if partInTag[i] {
lastIsTag = true lastIsTag = true
es.trackHtmlTag(part)
if _, err := out.Write(part); err != nil { if _, err := out.Write(part); err != nil {
return nil, err return nil, err
} }
@@ -75,7 +82,11 @@ func escapeStream(locale translation.Locale, in io.Reader, out io.Writer, opts .
return nil, err return nil, err
} }
} }
if err = es.detectAndWriteRunes(part); err != nil { if es.inTagMath {
if _, err := out.Write(part); err != nil {
return nil, err
}
} else if err = es.detectAndWriteRunes(part); err != nil {
return nil, err return nil, err
} }
} }
@@ -83,6 +94,34 @@ func escapeStream(locale translation.Locale, in io.Reader, out io.Writer, opts .
} }
} }
// trackHtmlTag receives tag parts, a tag might be split into multiple parts
func (e *escapeStreamer) trackHtmlTag(part []byte) {
const maxHeadLen = 100 // only read the first N bytes of the tag for detection purpose
if part[0] == '<' {
// start a new tag
e.tagPartial = e.tagPartial[:0]
}
if len(e.tagPartial) >= maxHeadLen {
return
}
e.tagPartial = append(e.tagPartial, part[:min(len(part), maxHeadLen-len(e.tagPartial))]...)
isTag := func(prefix string) bool {
if len(e.tagPartial) < len(prefix)+1 {
return false
}
if !util.AsciiEqualFold(e.tagPartial[:len(prefix)], []byte(prefix)) {
return false
}
return strings.IndexByte(" \t\n\r\f>", e.tagPartial[len(prefix)]) != -1
}
if isTag("<math") {
e.inTagMath = true
} else if isTag("</math") {
e.inTagMath = false
}
}
func (e *escapeStreamer) trimAndWriteBom(part []byte) ([]byte, error) { func (e *escapeStreamer) trimAndWriteBom(part []byte) ([]byte, error) {
remaining, ok := bytes.CutPrefix(part, globalVars().utf8Bom) remaining, ok := bytes.CutPrefix(part, globalVars().utf8Bom)
if ok { if ok {
+24
View File
@@ -141,6 +141,12 @@ then resh (ר), and finally heh (ה) (which should appear leftmost).`,
result: `O<span class="ambiguous-code-point" data-tooltip-content="repo.ambiguous_character:𝐾 [U+1D43E],K [U+004B]"><span class="char">𝐾</span></span>`, result: `O<span class="ambiguous-code-point" data-tooltip-content="repo.ambiguous_character:𝐾 [U+1D43E],K [U+004B]"><span class="char">𝐾</span></span>`,
status: EscapeStatus{Escaped: true, HasAmbiguous: true}, status: EscapeStatus{Escaped: true, HasAmbiguous: true},
}, },
{
name: "ambiguous in math",
text: "<math><mo>−</mo><mi>b</mi></math> −",
result: `<math><mo>−</mo><mi>b</mi></math> <span class="ambiguous-code-point" data-tooltip-content="repo.ambiguous_character:− [U+2212],- [U+002D]"><span class="char">−</span></span>`,
status: EscapeStatus{Escaped: true, HasAmbiguous: true},
},
} }
func TestEscapeControlReader(t *testing.T) { func TestEscapeControlReader(t *testing.T) {
@@ -156,6 +162,24 @@ func TestEscapeControlReader(t *testing.T) {
} }
} }
func TestTrackHtmlTag(t *testing.T) {
e := &escapeStreamer{}
for _, tt := range []struct {
parts []string
inMath bool
}{
{[]string{"<ma", `TH display="block">`}, true},
{[]string{"<mo>"}, true},
{[]string{"</MA", "th>"}, false},
{[]string{"<mathx>"}, false},
} {
for _, part := range tt.parts {
e.trackHtmlTag([]byte(part))
}
assert.Equal(t, tt.inMath, e.inTagMath, "%v", tt.parts)
}
}
func TestSettingAmbiguousUnicodeDetection(t *testing.T) { func TestSettingAmbiguousUnicodeDetection(t *testing.T) {
defer test.MockVariableValue(&setting.UI.AmbiguousUnicodeDetection, true)() defer test.MockVariableValue(&setting.UI.AmbiguousUnicodeDetection, true)()
_, out := EscapeControlHTML("a test", &translation.MockLocale{}) _, out := EscapeControlHTML("a test", &translation.MockLocale{})
+468
View File
@@ -0,0 +1,468 @@
// Copyright 2026 The Gitea Authors.
// Copyright 2018-2024 The Ruby Citation File Format Developers. Licensed under the Apache License, Version 2.0
// SPDX-License-Identifier: Apache-2.0
// Package citation formats CITATION.cff files, ported from the formatters of ruby-cff 1.3.0
package citation
import (
"cmp"
"maps"
"regexp"
"slices"
"strconv"
"strings"
"time"
"unicode"
"unicode/utf8"
"gitea.dev/modules/util"
"go.yaml.in/yaml/v4"
"golang.org/x/text/runes"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
)
type date struct{ time.Time }
var dateSeparators = strings.NewReplacer("/", "-", ". ", " ", ".", "-", ",", "")
func (d *date) UnmarshalYAML(node *yaml.Node) error {
value := dateSeparators.Replace(node.Value)
for _, layout := range []string{"2006-1-2", "2-1-2006", "2 Jan 2006", "2 January 2006", "Jan 2 2006", "January 2 2006"} {
if parsed, err := time.Parse(layout, value); err == nil {
d.Time = parsed
break
}
}
return nil
}
type license string
func (l *license) UnmarshalYAML(node *yaml.Node) error {
*l = license(node.Value)
if node.Kind != yaml.ScalarNode {
*l = license(inspectNode(node))
}
return nil
}
type actor struct {
Name string `yaml:"name"`
Alias string `yaml:"alias"`
FamilyNames string `yaml:"family-names"`
GivenNames string `yaml:"given-names"`
NameParticle string `yaml:"name-particle"`
NameSuffix string `yaml:"name-suffix"`
Affiliation string `yaml:"affiliation"`
City string `yaml:"city"`
Region string `yaml:"region"`
Country string `yaml:"country"`
DateStart date `yaml:"date-start"`
DateEnd date `yaml:"date-end"`
}
func (a *actor) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.ScalarNode:
a.Name = node.Value
return nil
case yaml.SequenceNode:
return nil
}
type plainActor actor
return node.Load((*plainActor)(a), yaml.WithUniqueKeys(false))
}
type metadata struct {
Type string `yaml:"type"`
Title string `yaml:"title"`
Authors []actor `yaml:"authors"`
Version string `yaml:"version"`
DOI string `yaml:"doi"`
URL string `yaml:"url"`
RepositoryCode string `yaml:"repository-code"`
License license `yaml:"license"`
DateReleased date `yaml:"date-released"`
}
type reference struct {
metadata `yaml:",inline"`
isTopLevel bool
Editors []actor `yaml:"editors"`
EditorsSeries []actor `yaml:"editors-series"`
DatePublished date `yaml:"date-published"`
Year string `yaml:"year"`
Month string `yaml:"month"`
Status string `yaml:"status"`
Journal string `yaml:"journal"`
Volume string `yaml:"volume"`
Issue string `yaml:"issue"`
Start string `yaml:"start"`
End string `yaml:"end"`
ISBN string `yaml:"isbn"`
Notes string `yaml:"notes"`
CollectionTitle string `yaml:"collection-title"`
ThesisType string `yaml:"thesis-type"`
Publisher actor `yaml:"publisher"`
Institution *actor `yaml:"institution"`
Conference actor `yaml:"conference"`
}
// FormatCFF returns the APA and BibTeX citations of a CITATION.cff file, both empty if it has no title or authors
func FormatCFF(content string) (apa, bibtex string) {
var node yaml.Node
if yaml.Unmarshal([]byte(content), &node) != nil {
return "", ""
}
retagTimestamps(&node)
var file struct {
TopLevel metadata `yaml:",inline"`
PreferredCitation *reference `yaml:"preferred-citation"`
}
if node.Load(&file, yaml.WithUniqueKeys(false)) != nil {
return "", ""
}
ref := file.PreferredCitation
if ref == nil {
ref = &reference{metadata: file.TopLevel, isTopLevel: true}
}
if ref.Title == "" || len(ref.Authors) == 0 {
return "", ""
}
return ref.formatAPA(), ref.formatBibTeX()
}
func retagTimestamps(node *yaml.Node) {
if node.ShortTag() == "!!timestamp" {
node.Tag = "!!str"
}
for _, child := range node.Content {
retagTimestamps(child)
}
}
func inspectNode(node *yaml.Node) string {
var parts []string
switch node.Kind {
case yaml.AliasNode:
if node.Alias.Kind == yaml.ScalarNode { // collection aliases can be recursive
return inspectNode(node.Alias)
}
case yaml.SequenceNode:
for _, child := range node.Content {
parts = append(parts, inspectNode(child))
}
return "[" + strings.Join(parts, ", ") + "]"
case yaml.MappingNode:
for i := 0; i < len(node.Content); i += 2 {
parts = append(parts, inspectNode(node.Content[i])+" => "+inspectNode(node.Content[i+1]))
}
return "{" + strings.Join(parts, ", ") + "}"
}
if node.ShortTag() == "!!null" {
return "nil"
}
return strconv.Quote(node.Value)
}
var statusNotes = map[string]string{
"advance-online": "Advance online publication",
"in-preparation": "Manuscript in preparation.",
"submitted": "Manuscript submitted for publication.",
}
func joinNonEmpty(sep string, parts ...string) string {
return strings.Join(util.SliceRemoveAll(parts, ""), sep)
}
func (r *reference) conferenceDates() (start, end date) {
if r.Type == "conference-paper" {
return r.Conference.DateStart, r.Conference.DateEnd
}
return date{}, date{}
}
func (r *reference) monthAndYear() (month, year string) {
when, _ := r.conferenceDates()
if when.IsZero() {
if r.Status == "in-press" {
return "", "in press"
}
if r.Year != "" {
return r.Month, r.Year
}
when = cmp.Or(r.DateReleased, r.DatePublished)
}
if when.IsZero() {
return "", ""
}
return strconv.Itoa(int(when.Month())), strconv.Itoa(when.Year())
}
func (r *reference) pages(dash string) string {
if r.Start == "" || r.End == "" || r.Start == r.End {
return r.Start
}
return r.Start + dash + r.End
}
func (r *reference) volume() string {
if r.Volume == "" || r.Issue == "" {
return r.Volume
}
return r.Volume + "(" + r.Issue + ")"
}
func (r *reference) institution() string {
if r.Institution == nil {
return r.Authors[0].Affiliation
}
return r.Institution.Name
}
func (r *reference) formatAPA() string {
authors := make([]string, 0, len(r.Authors))
for _, author := range r.Authors {
authors = append(authors, apaAuthor(author))
}
date := r.apaDate()
if date != "" {
date = "(" + date + ")"
}
version := ""
if r.Version != "" {
version = " (Version " + r.Version + ")"
}
url := cmp.Or(r.RepositoryCode, r.URL)
if r.DOI != "" {
url = "https://doi.org/" + r.DOI
}
return joinNonEmpty(". ", combineAuthors(authors), date, r.Title+version+r.apaTypeLabel(), r.apaPublicationData(), url)
}
func apaAuthor(author actor) string {
if author.Name != "" {
return author.Name
}
name := cmp.Or(author.FamilyNames, author.GivenNames, author.Alias)
if author.FamilyNames != "" && author.GivenNames != "" {
name += ", " + initials(author.GivenNames) + "."
}
if author.NameParticle != "" {
name = author.NameParticle + " " + name
}
if author.NameSuffix != "" {
name += ", " + author.NameSuffix
}
return name
}
func initials(names string) string {
parts := splitWords(names)
for i, part := range parts {
first, _ := utf8.DecodeRuneInString(part)
parts[i] = util.ToTitleCase(string(first))
}
return strings.Join(parts, ". ")
}
func splitWords(text string) []string {
return strings.FieldsFunc(text, func(char rune) bool { return char <= unicode.MaxASCII && unicode.IsSpace(char) })
}
func combineAuthors(authors []string) string {
if len(authors) == 1 {
return strings.TrimSuffix(authors[0], ".")
}
return strings.TrimSuffix(strings.Join(authors[:len(authors)-1], ", ")+", & "+authors[len(authors)-1], ".")
}
func (r *reference) apaDate() string {
start, end := r.conferenceDates()
if start.IsZero() || end.IsZero() || !start.Before(end.Time) {
_, year := r.monthAndYear()
return year
}
endLayout := "2"
if end.Month() != start.Month() {
endLayout = "January 2"
}
if end.Year() != start.Year() {
endLayout = "2006, " + endLayout
}
return start.Format("2006, January 2") + "–" + end.Format(endLayout)
}
func (r *reference) apaTypeLabel() string {
switch {
case strings.Contains(r.Type, "data"):
return " [Data set]"
case strings.Contains(r.Type, "conference"):
return " [Conference paper]"
case !r.isTopLevel && !strings.Contains(r.Type, "software"):
return ""
}
return " [Computer software]"
}
func (r *reference) apaPublicationData() string {
switch r.Type {
case "article":
return joinNonEmpty(", ", r.Journal, r.volume(), r.pages("–"), statusNotes[r.Status])
case "book":
return r.Publisher.Name
case "conference-paper":
return joinNonEmpty(", ", r.CollectionTitle, r.volume(), r.pages("–"))
case "report":
return r.institution()
case "phdthesis":
return "[" + cmp.Or(r.ThesisType, "Doctoral dissertation") + ", " + r.institution() + "]"
case "mastersthesis":
return "[" + cmp.Or(r.ThesisType, "Master's thesis") + ", " + r.institution() + "]"
case "unpublished":
return statusNotes[r.Status]
}
return ""
}
var bibtexTypeFields = map[string][]string{
"article": {"journal", "note", "number", "pages", "volume"},
"book": {"address", "editor", "isbn", "number", "pages", "publisher", "volume"},
"booklet": {"address"},
"inproceedings": {"address", "booktitle", "editor", "pages", "publisher", "series"},
"manual": {"address"},
"mastersthesis": {"address", "school", "type"},
"misc": {"pages"},
"phdthesis": {"address", "school", "type"},
"proceedings": {"address", "booktitle", "editor", "pages", "publisher", "series"},
"software": {"license", "version"},
"techreport": {"address", "institution", "number"},
"unpublished": {"note"},
}
var (
bibtexEscaper = strings.NewReplacer("&", `\&`, "%", `\%`, "$", `\$`, "#", `\#`, "_", `\_`, "{", `\{`, "}", `\}`)
keyLetters = strings.NewReplacer(
"Æ", "AE", "æ", "ae", "Ð", "D", "ð", "d", "Ø", "O", "ø", "o", "Þ", "Th", "þ", "th", "ß", "ss", "×", "x",
"Đ", "D", "đ", "d", "Ħ", "H", "ħ", "h", "ı", "i", "IJ", "IJ", "ij", "ij", "ĸ", "k", "Ŀ", "L", "ŀ", "l",
"Ł", "L", "ł", "l", "ʼn", "'n", "Ŋ", "NG", "ŋ", "ng", "Œ", "OE", "œ", "oe", "Ŧ", "T", "ŧ", "t",
)
keyUnsafeChars = regexp.MustCompile(`[^a-zA-Z0-9-]+`)
)
func keyToASCII() transform.Transformer {
return transform.Chain(
runes.Remove(runes.Predicate(func(char rune) bool {
return char > unicode.MaxASCII && (char < 'À' || char > 'ž') && char != 'ệ'
})),
norm.NFD,
runes.Remove(runes.Predicate(func(char rune) bool { return char > unicode.MaxASCII })),
)
}
func (r *reference) formatBibTeX() string {
place := r.Publisher
if r.Type == "conference-paper" {
place = r.Conference
}
editors := r.Editors
if len(editors) == 0 {
editors = r.EditorsSeries
}
typeFields := map[string]string{
"address": joinNonEmpty(", ", place.City, place.Region, place.Country),
"booktitle": bibtexEscaper.Replace(r.CollectionTitle),
"editor": bibtexActors(editors),
"institution": bibtexEscaper.Replace(r.institution()),
"isbn": bibtexEscaper.Replace(r.ISBN),
"journal": bibtexEscaper.Replace(r.Journal),
"license": bibtexEscaper.Replace(string(r.License)),
"note": statusNotes[r.Status],
"number": r.Issue,
"pages": r.pages("--"),
"publisher": bibtexEscaper.Replace(r.Publisher.Name),
"school": bibtexEscaper.Replace(r.institution()),
"series": bibtexEscaper.Replace(r.Conference.Name),
"type": r.ThesisType,
"version": bibtexEscaper.Replace(r.Version),
"volume": bibtexEscaper.Replace(r.Volume),
}
entryType := bibtexType(r.Type)
fields := map[string]string{
"author": bibtexActors(r.Authors),
"title": "{" + bibtexEscaper.Replace(r.Title) + "}",
"doi": bibtexEscaper.Replace(r.DOI),
}
for _, name := range bibtexTypeFields[entryType] {
fields[name] = typeFields[name]
}
month, year := r.monthAndYear()
if num, _ := strconv.Atoi(month); num >= 1 && num <= 12 {
fields["month"] = strings.ToLower(time.Month(num).String()[:3])
}
fields["year"] = year
fields["url"] = cmp.Or(r.RepositoryCode, r.URL)
fields["note"] = cmp.Or(fields["note"], r.Notes)
maps.DeleteFunc(fields, func(_, value string) bool { return value == "" })
lines := []string{bibtexKey(fields)}
for _, name := range slices.Sorted(maps.Keys(fields)) {
value := fields[name]
if name != "month" {
value = "{" + value + "}"
}
lines = append(lines, name+" = "+value)
}
return "@" + entryType + "{" + strings.Join(lines, ",\n") + "\n}"
}
func bibtexType(cffType string) string {
if cffType == "" || strings.Contains(cffType, "software") {
return "software"
}
switch cffType {
case "article", "book", "manual", "unpublished", "phdthesis", "mastersthesis":
return cffType
case "conference", "proceedings":
return "proceedings"
case "conference-paper":
return "inproceedings"
case "magazine-article", "newspaper-article":
return "article"
case "pamphlet":
return "booklet"
case "report":
return "techreport"
}
return "misc"
}
func bibtexActors(actors []actor) string {
names := make([]string, 0, len(actors))
for _, entry := range actors {
switch {
case entry.Name != "":
names = append(names, "{"+bibtexEscaper.Replace(entry.Name)+"}")
case entry.FamilyNames == "" && entry.GivenNames == "":
names = append(names, bibtexEscaper.Replace(entry.Alias))
default:
family := entry.FamilyNames
if entry.NameParticle != "" {
family = entry.NameParticle + " " + family
}
names = append(names, joinNonEmpty(", ", family, entry.NameSuffix, entry.GivenNames))
}
}
return strings.Join(names, " and ")
}
func bibtexKey(fields map[string]string) string {
author, _, _ := strings.Cut(fields["author"], ",")
titleWords := splitWords(fields["title"])
key := joinNonEmpty("_", author, strings.Join(titleWords[:min(3, len(titleWords))], "_"), fields["year"])
key, _, _ = transform.String(keyToASCII(), keyLetters.Replace(key))
return strings.Trim(keyUnsafeChars.ReplaceAllString(key, "_"), "_")
}
+137
View File
@@ -0,0 +1,137 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package citation
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestFormatCFF(t *testing.T) {
cases := []struct{ cff, apa, bibtex string }{
{
cff: `title: Overridden
message: &mit MIT
authors: [{family-names: Haines, given-names: Robert}, {name: "The {curly_braces} Collective"}]
title: "Software that uses the following symbols: &, %, $, #"
version: 2024-01-16
license: [*mit, Apache-2.0]
date-released: 2024-01-16
`,
apa: "Haines, R., & The {curly_braces} Collective. (2024). Software that uses the following symbols: &, %, $, # (Version 2024-01-16) [Computer software]",
bibtex: `@software{Haines_Software_that_uses_2024,
author = {Haines, Robert and {The \{curly\_braces\} Collective}},
license = {["MIT", "Apache-2.0"]},
month = jan,
title = {{Software that uses the following symbols: \&, \%, \$, \#}},
version = {2024-01-16},
year = {2024}
}`,
},
{
cff: `authors:
- family-names: Smith
given-names: Arfon M.
title: "Software citation principles"
license: MIT
preferred-citation:
authors:
- family-names: Smith
given-names: A. M.
- name: "FORCE11 Software Citation Working Group"
doi: "10.7717/peerj-cs.86"
journal: "PeerJ Computer Science"
month: 9
start: e86
title: "Software citation principles"
type: article
volume: 2
issue: 123
year: 2016
publisher:
- name: The Open Journal
`,
apa: "Smith, A. M., & FORCE11 Software Citation Working Group. (2016). Software citation principles. PeerJ Computer Science, 2(123), e86. https://doi.org/10.7717/peerj-cs.86",
bibtex: `@article{Smith_Software_citation_principles_2016,
author = {Smith, A. M. and {FORCE11 Software Citation Working Group}},
doi = {10.7717/peerj-cs.86},
journal = {PeerJ Computer Science},
month = sep,
number = {123},
pages = {e86},
title = {{Software citation principles}},
volume = {2},
year = {2016}
}`,
},
{
cff: `preferred-citation:
type: conference-paper
version: 1.10
title: "Über tools"
authors:
- family-names: Ørsted
given-names: ǰan christian
name-particle: van
name-suffix: Jr.
collection-title: "Proceedings of X & Y"
conference:
name: "Conf_2020"
city: Berlin
country: DE
date-start: 30.06.2020
date-end: 2020-07-02
start: 10
end: 20
editors:
- affiliation: Press
editors-series:
- family-names: Editor
given-names: Eve
`,
apa: "van Ørsted, J̌. C., Jr. (2020, June 30–July 2). Über tools (Version 1.10) [Conference paper]. Proceedings of X & Y, 10–20",
bibtex: `@inproceedings{van_Orsted_Uber_tools_2020,
address = {Berlin, DE},
author = {van Ørsted, Jr., ǰan christian},
booktitle = {Proceedings of X \& Y},
month = jun,
pages = {10--20},
series = {Conf\_2020},
title = {{Über tools}},
year = {2020}
}`,
},
{
cff: `thesis: &thesis {type: phdthesis, title: Thesis}
preferred-citation:
<<: *thesis
authors:
- family-names: Nguyễn
given-names: Sam
affiliation: "Uni_A"
institution: {city: Hanoi}
license: &loop [*loop]
status: in-press
notes: A note
`,
apa: "Nguyễn, S. (in press). Thesis. [Doctoral dissertation, ]",
bibtex: `@phdthesis{Nguyn_Thesis_in_press,
author = {Nguyễn, Sam},
note = {A note},
title = {{Thesis}},
year = {in press}
}`,
},
{cff: "title: No authors\nauthor:\n - name: Typo\n"},
}
for _, tc := range cases {
t.Run("", func(t *testing.T) {
t.Parallel()
apa, bibtex := FormatCFF(tc.cff)
assert.Equal(t, tc.apa, apa)
assert.Equal(t, tc.bibtex, bibtex)
})
}
}
+8 -2
View File
@@ -13,6 +13,7 @@ import (
"gitea.dev/modules/git" "gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd" "gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/log" "gitea.dev/modules/log"
"gitea.dev/modules/setting"
) )
// BatchChecker provides a reader for check-attribute content that can be long running // BatchChecker provides a reader for check-attribute content that can be long running
@@ -120,12 +121,17 @@ func (c *BatchChecker) CheckPath(path string) (rs *Attributes, err error) {
return fmt.Errorf("CheckPath timeout: %s", debugMsg) return fmt.Errorf("CheckPath timeout: %s", debugMsg)
} }
timeout := time.NewTimer(5 * time.Second)
defer timeout.Stop()
rs = NewAttributes() rs = NewAttributes()
for i := 0; i < c.attributesNum; i++ { for i := 0; i < c.attributesNum; i++ {
select { select {
case <-time.After(5 * time.Second): case <-timeout.C:
// there is no "hang" problem now. This code is just used to catch other potential problems. // there is no "hang" problem now. This code is just used to catch other potential problems.
return nil, reportTimeout() err = reportTimeout()
setting.PanicInDevOrTesting("Unexpected timeout, need to investigate: %v", err)
return nil, err
case attr, ok := <-c.stdOut.ReadAttribute(): case attr, ok := <-c.stdOut.ReadAttribute():
if !ok { if !ok {
return nil, c.ctx.Err() return nil, c.ctx.Err()
-5
View File
@@ -15,11 +15,6 @@ func TestReadingBlameOutputSha256(t *testing.T) {
setting.AppDataPath = t.TempDir() setting.AppDataPath = t.TempDir()
ctx := t.Context() ctx := t.Context()
if DefaultFeatures().UsingGogit {
t.Skip("Skipping test since gogit does not support sha256")
return
}
t.Run("Without .git-blame-ignore-revs", func(t *testing.T) { t.Run("Without .git-blame-ignore-revs", func(t *testing.T) {
storage := mockRepository("repo5_pulls_sha256") storage := mockRepository("repo5_pulls_sha256")
repo, err := OpenRepository(ctx, storage) repo, err := OpenRepository(ctx, storage)
+96 -2
View File
@@ -12,11 +12,10 @@ import (
"io" "io"
"strings" "strings"
"gitea.dev/modules/log"
"gitea.dev/modules/util" "gitea.dev/modules/util"
) )
// This file contains common functions between the gogit and !gogit variants for git Blobs
// Name returns name of the tree entry this blob object was created from (or empty string) // Name returns name of the tree entry this blob object was created from (or empty string)
func (b *Blob) Name() string { func (b *Blob) Name() string {
return b.name return b.name
@@ -114,3 +113,98 @@ loop:
_ = encoder.Close() _ = encoder.Close()
return base64buf.String(), nil return base64buf.String(), nil
} }
// Blob represents a Git object.
type Blob struct {
ID ObjectID
gotSize bool
size int64
name string
repo *Repository
}
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
// Calling the Close function on the result will discard all unread output.
func (b *Blob) DataAsync(ctx context.Context) (_ io.ReadCloser, retErr error) {
batch, cancel, err := b.repo.CatFileBatch()
if err != nil {
return nil, err
}
defer func() {
// if there was an error, cancel the batch right away,
// otherwise let the caller close it
if retErr != nil {
cancel()
}
}()
info, contentReader, err := batch.QueryContent(b.ID.String())
if err != nil {
return nil, err
}
b.gotSize = true
b.size = info.Size
return &blobReader{
rd: contentReader,
n: info.Size,
cancel: cancel,
}, nil
}
// Size returns the uncompressed size of the blob
func (b *Blob) Size(ctx context.Context) int64 {
if b.gotSize {
return b.size
}
batch, cancel, err := b.repo.CatFileBatch()
if err != nil {
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.LogString(), err)
return 0
}
defer cancel()
info, err := batch.QueryInfo(b.ID.String())
if err != nil {
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.LogString(), err)
return 0
}
b.gotSize = true
b.size = info.Size
return b.size
}
type blobReader struct {
rd BufferedReader
n int64
cancel func()
}
func (b *blobReader) Read(p []byte) (n int, err error) {
if b.n <= 0 {
return 0, io.EOF
}
if int64(len(p)) > b.n {
p = p[0:b.n]
}
n, err = b.rd.Read(p)
b.n -= int64(n)
return n, err
}
// Close implements io.Closer
func (b *blobReader) Close() error {
if b.rd == nil {
return nil
}
defer b.cancel()
if err := DiscardFull(b.rd, b.n+1); err != nil {
return err
}
b.rd = nil
return nil
}
-47
View File
@@ -1,47 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"context"
"io"
"gitea.dev/modules/log"
"github.com/go-git/go-git/v5/plumbing"
)
// Blob represents a Git object.
type Blob struct {
ID ObjectID
repo *Repository
name string
}
func (b *Blob) gogitEncodedObj() (plumbing.EncodedObject, error) {
return b.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, plumbing.Hash(b.ID.RawValue()))
}
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
// Calling the Close function on the result will discard all unread output.
func (b *Blob) DataAsync(_ context.Context) (io.ReadCloser, error) {
obj, err := b.gogitEncodedObj()
if err != nil {
return nil, err
}
return obj.Reader()
}
// Size returns the uncompressed size of the blob
func (b *Blob) Size(_ context.Context) int64 {
obj, err := b.gogitEncodedObj()
if err != nil {
log.Error("Error getting gogit encoded object for blob %s(%s): %v", b.name, b.ID.String(), err)
return 0
}
return obj.Size()
}
-108
View File
@@ -1,108 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"context"
"io"
"gitea.dev/modules/log"
)
// Blob represents a Git object.
type Blob struct {
ID ObjectID
gotSize bool
size int64
name string
repo *Repository
}
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
// Calling the Close function on the result will discard all unread output.
func (b *Blob) DataAsync(ctx context.Context) (_ io.ReadCloser, retErr error) {
batch, cancel, err := b.repo.CatFileBatch()
if err != nil {
return nil, err
}
defer func() {
// if there was an error, cancel the batch right away,
// otherwise let the caller close it
if retErr != nil {
cancel()
}
}()
info, contentReader, err := batch.QueryContent(b.ID.String())
if err != nil {
return nil, err
}
b.gotSize = true
b.size = info.Size
return &blobReader{
rd: contentReader,
n: info.Size,
cancel: cancel,
}, nil
}
// Size returns the uncompressed size of the blob
func (b *Blob) Size(ctx context.Context) int64 {
if b.gotSize {
return b.size
}
batch, cancel, err := b.repo.CatFileBatch()
if err != nil {
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.LogString(), err)
return 0
}
defer cancel()
info, err := batch.QueryInfo(b.ID.String())
if err != nil {
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.LogString(), err)
return 0
}
b.gotSize = true
b.size = info.Size
return b.size
}
type blobReader struct {
rd BufferedReader
n int64
cancel func()
}
func (b *blobReader) Read(p []byte) (n int, err error) {
if b.n <= 0 {
return 0, io.EOF
}
if int64(len(p)) > b.n {
p = p[0:b.n]
}
n, err = b.rd.Read(p)
b.n -= int64(n)
return n, err
}
// Close implements io.Closer
func (b *blobReader) Close() error {
if b.rd == nil {
return nil
}
defer b.cancel()
if err := DiscardFull(b.rd, b.n+1); err != nil {
return err
}
b.rd = nil
return nil
}
-76
View File
@@ -1,76 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"fmt"
"strings"
"github.com/go-git/go-git/v5/plumbing/object"
)
func convertPGPSignature(c *object.Commit) *CommitSignature {
if c.PGPSignature == "" {
return nil
}
var w strings.Builder
var err error
if _, err = fmt.Fprintf(&w, "tree %s\n", c.TreeHash.String()); err != nil {
return nil
}
for _, parent := range c.ParentHashes {
if _, err = fmt.Fprintf(&w, "parent %s\n", parent.String()); err != nil {
return nil
}
}
if _, err = fmt.Fprint(&w, "author "); err != nil {
return nil
}
if err = c.Author.Encode(&w); err != nil {
return nil
}
if _, err = fmt.Fprint(&w, "\ncommitter "); err != nil {
return nil
}
if err = c.Committer.Encode(&w); err != nil {
return nil
}
if c.Encoding != "" && c.Encoding != "UTF-8" {
if _, err = fmt.Fprintf(&w, "\nencoding %s\n", c.Encoding); err != nil {
return nil
}
}
if _, err = fmt.Fprintf(&w, "\n\n%s", c.Message); err != nil {
return nil
}
return &CommitSignature{
Signature: c.PGPSignature,
Payload: w.String(),
}
}
func convertCommit(c *object.Commit) *Commit {
return &Commit{
ID: ParseGogitHash(c.Hash),
TreeID: ParseGogitHash(c.TreeHash),
CommitMessage: CommitMessage{MessageRaw: c.Message},
Committer: &c.Committer,
Author: &c.Author,
Signature: convertPGPSignature(c),
Parents: ParseGogitHashArray(c.ParentHashes),
}
}
+33
View File
@@ -113,3 +113,36 @@ func getLastCommitForPathsByCache(ctx context.Context, commitID, treePath string
return results, unHitEntryPaths, nil return results, unHitEntryPaths, nil
} }
// GetLastCommitForPaths returns last commit information
func GetLastCommitForPaths(ctx context.Context, gitRepo *Repository, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) {
// We read backwards from the commit to obtain all of the commits
revs, err := walkGitLog(ctx, gitRepo, commit, treePath, paths...)
if err != nil {
return nil, err
}
commitsMap := map[string]*Commit{}
commitsMap[commit.ID.String()] = commit
commitCommits := map[string]*Commit{}
for path, commitID := range revs {
if len(commitID) == 0 {
continue
}
c, ok := commitsMap[commitID]
if ok {
commitCommits[path] = c
continue
}
c, err := gitRepo.GetCommit(ctx, commitID) // Ensure the commit exists in the repository
if err != nil {
return nil, err
}
commitCommits[path] = c
}
return commitCommits, nil
}
-207
View File
@@ -1,207 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"context"
"path"
"github.com/emirpasic/gods/trees/binaryheap"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph"
)
type commitAndPaths struct {
commit cgobject.CommitNode
// Paths that are still on the branch represented by commit
paths []string
// Set of hashes for the paths
hashes map[string]plumbing.Hash
}
func getCommitTree(c cgobject.CommitNode, treePath string) (*object.Tree, error) {
tree, err := c.Tree()
if err != nil {
return nil, err
}
// Optimize deep traversals by focusing only on the specific tree
if treePath != "" {
tree, err = tree.Tree(treePath)
if err != nil {
return nil, err
}
}
return tree, nil
}
func getFileHashes(c cgobject.CommitNode, treePath string, paths []string) (map[string]plumbing.Hash, error) {
tree, err := getCommitTree(c, treePath)
if err == object.ErrDirectoryNotFound {
// The whole tree didn't exist, so return empty map
return make(map[string]plumbing.Hash), nil
}
if err != nil {
return nil, err
}
hashes := make(map[string]plumbing.Hash)
for _, path := range paths {
if path != "" {
entry, err := tree.FindEntry(path)
if err == nil {
hashes[path] = entry.Hash
}
} else {
hashes[path] = tree.Hash
}
}
return hashes, nil
}
// GetLastCommitForPaths returns last commit information
func GetLastCommitForPaths(ctx context.Context, gitRepo *Repository, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) {
commitNodeIndex, closer := gitRepo.CommitNodeIndex()
defer closer()
c, err := commitNodeIndex.Get(plumbing.Hash(commit.ID.RawValue()))
if err != nil {
return nil, err
}
return getLastCommitForPathsByCommitNode(ctx, gitRepo, c, treePath, paths)
}
func getLastCommitForPathsByCommitNode(ctx context.Context, gitRepo *Repository, c cgobject.CommitNode, treePath string, paths []string) (map[string]*Commit, error) {
refSha := c.ID().String()
// We do a tree traversal with nodes sorted by commit time
heap := binaryheap.NewWith(func(a, b any) int {
if a.(*commitAndPaths).commit.CommitTime().Before(b.(*commitAndPaths).commit.CommitTime()) { //nolint:forcetypeassert // this heap only ever holds *commitAndPaths
return 1
}
return -1
})
resultNodes := make(map[string]cgobject.CommitNode)
initialHashes, err := getFileHashes(c, treePath, paths)
if err != nil {
return nil, err
}
// Start search from the root commit and with full set of paths
heap.Push(&commitAndPaths{c, paths, initialHashes})
heaploop:
for {
select {
case <-ctx.Done():
if ctx.Err() == context.DeadlineExceeded {
break heaploop
}
return nil, ctx.Err()
default:
}
cIn, ok := heap.Pop()
if !ok {
break
}
current := cIn.(*commitAndPaths) //nolint:forcetypeassert // this heap only ever holds *commitAndPaths
// Load the parent commits for the one we are currently examining
numParents := current.commit.NumParents()
var parents []cgobject.CommitNode
for i := range numParents {
parent, err := current.commit.ParentNode(i)
if err != nil {
break
}
parents = append(parents, parent)
}
// Examine the current commit and set of interesting paths
pathUnchanged := make([]bool, len(current.paths))
parentHashes := make([]map[string]plumbing.Hash, len(parents))
for j, parent := range parents {
parentHashes[j], err = getFileHashes(parent, treePath, current.paths)
if err != nil {
break
}
for i, path := range current.paths {
if parentHashes[j][path] == current.hashes[path] {
pathUnchanged[i] = true
}
}
}
var remainingPaths []string
for i, pth := range current.paths {
// The results could already contain some newer change for the same path,
// so don't override that and bail out on the file early.
if resultNodes[pth] == nil {
if pathUnchanged[i] {
// The path existed with the same hash in at least one parent so it could
// not have been changed in this commit directly.
remainingPaths = append(remainingPaths, pth)
} else {
// There are few possible cases how can we get here:
// - The path didn't exist in any parent, so it must have been created by
// this commit.
// - The path did exist in the parent commit, but the hash of the file has
// changed.
// - We are looking at a merge commit and the hash of the file doesn't
// match any of the hashes being merged. This is more common for directories,
// but it can also happen if a file is changed through conflict resolution.
resultNodes[pth] = current.commit
if err := gitRepo.LastCommitCache.Put(refSha, path.Join(treePath, pth), current.commit.ID().String()); err != nil {
return nil, err
}
}
}
}
if len(remainingPaths) > 0 {
// Add the parent nodes along with remaining paths to the heap for further
// processing.
for j, parent := range parents {
// Combine remainingPath with paths available on the parent branch
// and make union of them
remainingPathsForParent := make([]string, 0, len(remainingPaths))
newRemainingPaths := make([]string, 0, len(remainingPaths))
for _, path := range remainingPaths {
if parentHashes[j][path] == current.hashes[path] {
remainingPathsForParent = append(remainingPathsForParent, path)
} else {
newRemainingPaths = append(newRemainingPaths, path)
}
}
if remainingPathsForParent != nil {
heap.Push(&commitAndPaths{parent, remainingPathsForParent, parentHashes[j]})
}
if len(newRemainingPaths) == 0 {
break
}
remainingPaths = newRemainingPaths
}
}
}
// Post-processing
result := make(map[string]*Commit)
for path, commitNode := range resultNodes {
commit, err := commitNode.Commit()
if err != nil {
return nil, err
}
result[path] = convertCommit(commit)
}
return result, nil
}
-43
View File
@@ -1,43 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"context"
)
// GetLastCommitForPaths returns last commit information
func GetLastCommitForPaths(ctx context.Context, gitRepo *Repository, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) {
// We read backwards from the commit to obtain all of the commits
revs, err := walkGitLog(ctx, gitRepo, commit, treePath, paths...)
if err != nil {
return nil, err
}
commitsMap := map[string]*Commit{}
commitsMap[commit.ID.String()] = commit
commitCommits := map[string]*Commit{}
for path, commitID := range revs {
if len(commitID) == 0 {
continue
}
c, ok := commitsMap[commitID]
if ok {
commitCommits[path] = c
continue
}
c, err := gitRepo.GetCommit(ctx, commitID) // Ensure the commit exists in the repository
if err != nil {
return nil, err
}
commitCommits[path] = c
}
return commitCommits, nil
}
-57
View File
@@ -1,57 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"context"
"path/filepath"
"testing"
"time"
"gitea.dev/modules/git/gitrepo"
"gitea.dev/modules/test"
"gitea.dev/modules/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEntries_GetCommitsInfo_ContextErr(t *testing.T) {
repoPath, _ := filepath.Abs(filepath.Join(testReposDir, "repo1_bare"))
repo, err := OpenRepository(t.Context(), gitrepo.RepositoryManaged("dummy", repoPath))
require.NoError(t, err)
defer repo.Close()
commit, err := repo.GetCommit(t.Context(), "feaf4ba6bc635fec442f46ddd4512416ec43c2c2")
require.NoError(t, err)
entries, err := commit.Tree().ListEntries(t.Context(), repo)
require.NoError(t, err)
countCommitInfosCommit := func(infos []CommitInfo) (nilCommits, nonNilCommits int) {
for _, info := range infos {
nilCommits += util.Iif(info.Commit == nil, 1, 0)
nonNilCommits += util.Iif(info.Commit != nil, 1, 0)
}
return nilCommits, nonNilCommits
}
ctx, cancel := context.WithCancel(t.Context())
defer test.MockVariableValue(&walkGitLogDebugBeforeNext)()
walkGitLogDebugBeforeNext = cancel
commitInfos, _, err := entries.GetCommitsInfo(ctx, time.Second, "/any/repo-link", repo, commit, "")
assert.NoError(t, err)
nilCommits, nonNilCommits := countCommitInfosCommit(commitInfos)
assert.Equal(t, 0, nonNilCommits) // no commit info due to canceled (or deadline-exceeded) context
assert.Equal(t, 3, nilCommits)
walkGitLogDebugBeforeNext = nil
commitInfos, _, err = entries.GetCommitsInfo(t.Context(), time.Second, "/any/repo-link", repo, commit, "")
assert.NoError(t, err)
nilCommits, nonNilCommits = countCommitInfosCommit(commitInfos)
assert.Equal(t, 3, nonNilCommits)
assert.Equal(t, 0, nilCommits)
}
+40
View File
@@ -4,11 +4,14 @@
package git package git
import ( import (
"context"
"path/filepath" "path/filepath"
"testing" "testing"
"time" "time"
"gitea.dev/modules/git/gitrepo" "gitea.dev/modules/git/gitrepo"
"gitea.dev/modules/test"
"gitea.dev/modules/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -164,3 +167,40 @@ func TestEntries_GetCommitsInfo(t *testing.T) {
assert.Nil(t, cisf.SubmoduleWebLinkTree(t.Context())) assert.Nil(t, cisf.SubmoduleWebLinkTree(t.Context()))
}) })
} }
func TestEntries_GetCommitsInfo_ContextErr(t *testing.T) {
repoPath, _ := filepath.Abs(filepath.Join(testReposDir, "repo1_bare"))
repo, err := OpenRepository(t.Context(), gitrepo.RepositoryManaged("dummy", repoPath))
require.NoError(t, err)
defer repo.Close()
commit, err := repo.GetCommit(t.Context(), "feaf4ba6bc635fec442f46ddd4512416ec43c2c2")
require.NoError(t, err)
entries, err := commit.Tree().ListEntries(t.Context(), repo)
require.NoError(t, err)
countCommitInfosCommit := func(infos []CommitInfo) (nilCommits, nonNilCommits int) {
for _, info := range infos {
nilCommits += util.Iif(info.Commit == nil, 1, 0)
nonNilCommits += util.Iif(info.Commit != nil, 1, 0)
}
return nilCommits, nonNilCommits
}
ctx, cancel := context.WithCancel(t.Context())
defer test.MockVariableValue(&walkGitLogDebugBeforeNext)()
walkGitLogDebugBeforeNext = cancel
commitInfos, _, err := entries.GetCommitsInfo(ctx, time.Second, "/any/repo-link", repo, commit, "")
assert.NoError(t, err)
nilCommits, nonNilCommits := countCommitInfosCommit(commitInfos)
assert.Equal(t, 0, nonNilCommits) // no commit info due to canceled (or deadline-exceeded) context
assert.Equal(t, 3, nilCommits)
walkGitLogDebugBeforeNext = nil
commitInfos, _, err = entries.GetCommitsInfo(t.Context(), time.Second, "/any/repo-link", repo, commit, "")
assert.NoError(t, err)
nilCommits, nonNilCommits = countCommitInfosCommit(commitInfos)
assert.Equal(t, 3, nonNilCommits)
assert.Equal(t, 0, nilCommits)
}
-2
View File
@@ -1,8 +1,6 @@
// Copyright 2023 The Gitea Authors. All rights reserved. // Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//go:build !gogit
package git package git
import ( import (
+2 -3
View File
@@ -29,7 +29,6 @@ const RequiredVersion = "2.25.0" // the minimum Git version required
type Features struct { type Features struct {
gitVersion *version.Version gitVersion *version.Version
UsingGogit bool
SupportProcReceive bool // >= 2.29 SupportProcReceive bool // >= 2.29
SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’ SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’
SupportedObjectFormats []ObjectFormat // sha1, sha256 SupportedObjectFormats []ObjectFormat // sha1, sha256
@@ -79,9 +78,9 @@ func loadGitVersionFeatures() (*Features, error) {
return nil, err return nil, err
} }
features := &Features{gitVersion: ver, UsingGogit: isGogit} features := &Features{gitVersion: ver}
features.SupportProcReceive = features.CheckVersionAtLeast("2.29") features.SupportProcReceive = features.CheckVersionAtLeast("2.29")
features.SupportHashSha256 = features.CheckVersionAtLeast("2.42") && !isGogit features.SupportHashSha256 = features.CheckVersionAtLeast("2.42")
features.SupportedObjectFormats = []ObjectFormat{Sha1ObjectFormat} features.SupportedObjectFormats = []ObjectFormat{Sha1ObjectFormat}
if features.SupportHashSha256 { if features.SupportHashSha256 {
features.SupportedObjectFormats = append(features.SupportedObjectFormats, Sha256ObjectFormat) features.SupportedObjectFormats = append(features.SupportedObjectFormats, Sha256ObjectFormat)
+10 -5
View File
@@ -49,8 +49,8 @@ type Command struct {
cmd *process.Cmd cmd *process.Cmd
cmdCtx context.Context cmdCtx context.Context
cmdCancel process.CancelCauseFunc cmdCtxCancel process.CancelCauseFunc
cmdFinished process.FinishedFunc cmdFinished func()
cmdStartTime time.Time cmdStartTime time.Time
pipelineFunc func(Context) error pipelineFunc func(Context) error
@@ -428,19 +428,24 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
if c.callerInfo == "" { if c.callerInfo == "" {
c.WithParentCallerInfo() c.WithParentCallerInfo()
} }
// these logs are for debugging purposes only, so no guarantee of correctness or stability // these logs are for debugging purposes only, so no guarantee of correctness or stability
desc := fmt.Sprintf("git.Run(by:%s, repo:%s): %s", c.callerInfo, logArgSanitize(c.gitDir), cmdLogString) desc := fmt.Sprintf("git.Run(by:%s, repo:%s): %s", c.callerInfo, logArgSanitize(c.gitDir), cmdLogString)
log.Debug("git.Command: %s", desc) log.Debug("git.Command: %s", desc)
_, span := gtprof.GetTracer().Start(ctx, gtprof.TraceSpanGitRun) _, span := gtprof.GetTracer().Start(ctx, gtprof.TraceSpanGitRun)
defer span.End()
span.SetAttributeString(gtprof.TraceAttrFuncCaller, c.callerInfo) span.SetAttributeString(gtprof.TraceAttrFuncCaller, c.callerInfo)
span.SetAttributeString(gtprof.TraceAttrGitCommand, cmdLogString) span.SetAttributeString(gtprof.TraceAttrGitCommand, cmdLogString)
var cmdCtxFinished func()
if c.cmdTimeout <= 0 { if c.cmdTimeout <= 0 {
c.cmdCtx, c.cmdCancel, c.cmdFinished = process.GetManager().AddContext(ctx, desc) c.cmdCtx, c.cmdCtxCancel, cmdCtxFinished = process.GetManager().AddContext(ctx, desc)
} else { } else {
c.cmdCtx, c.cmdCancel, c.cmdFinished = process.GetManager().AddContextTimeout(ctx, c.cmdTimeout, desc) c.cmdCtx, c.cmdCtxCancel, cmdCtxFinished = process.GetManager().AddContextTimeout(ctx, c.cmdTimeout, desc)
}
c.cmdFinished = func() {
cmdCtxFinished()
span.End()
} }
c.cmdStartTime = time.Now() c.cmdStartTime = time.Now()
+1 -1
View File
@@ -27,6 +27,6 @@ func (c *cmdContext) CancelPipeline(err error) error {
// * context canceled by pipeline caller with/without error (normal cancellation) // * context canceled by pipeline caller with/without error (normal cancellation)
// * context canceled by parent context (still context.Canceled error) // * context canceled by parent context (still context.Canceled error)
// * other causes // * other causes
c.cmd.cmdCancel(pipelineError{err}) c.cmd.cmdCtxCancel(pipelineError{err})
return err return err
} }
+48 -2
View File
@@ -25,14 +25,20 @@ type LastCommitCache struct {
// Put puts the last commit id with commit and entry path // Put puts the last commit id with commit and entry path
func (c *LastCommitCache) Put(ref, entryPath, commitID string) error { func (c *LastCommitCache) Put(ref, entryPath, commitID string) error {
if c.ttlFn() < 0 {
return nil
}
log.Debug("LastCommitCache save: [%s:%s:%s]", ref, entryPath, commitID) log.Debug("LastCommitCache save: [%s:%s:%s]", ref, entryPath, commitID)
return c.cache.Put(getCacheKey(c.repo, ref, entryPath), commitID, c.ttlFn()) return c.cache.Put(getCacheKey(c.repo, ref, entryPath), commitID, c.ttlFn())
} }
// Get gets the last commit information by commit id and entry path // Get gets the last commit information by commit id and entry path
func (c *LastCommitCache) Get(ctx context.Context, ref, entryPath string) (*Commit, error) { func (c *LastCommitCache) Get(ctx context.Context, ref, entryPath string) (*Commit, error) {
lastCommitID, ok := c.cache.Get(getCacheKey(c.repo, ref, entryPath)) var lastCommitID string
if !ok || lastCommitID == "" { if c.ttlFn() >= 0 {
lastCommitID, _ = c.cache.Get(getCacheKey(c.repo, ref, entryPath))
}
if lastCommitID == "" {
return nil, nil //nolint:nilnil // return nil when cache miss return nil, nil //nolint:nilnil // return nil when cache miss
} }
@@ -72,3 +78,43 @@ func (c *LastCommitCache) GetCommitByPath(ctx context.Context, entryCommitID Obj
return lastCommit, nil return lastCommit, nil
} }
// CacheCommit will cache the commit from the gitRepository
func (c *Commit) CacheCommit(ctx context.Context, gitRepo *Repository) error {
return c.recursiveCache(ctx, gitRepo, c.Tree(), "", 1)
}
func (c *Commit) recursiveCache(ctx context.Context, gitRepo *Repository, tree *Tree, treePath string, level int) error {
if level == 0 {
return nil
}
entries, err := tree.ListEntries(ctx, gitRepo)
if err != nil {
return err
}
entryPaths := make([]string, len(entries))
for i, entry := range entries {
entryPaths[i] = entry.Name()
}
_, err = walkGitLog(ctx, gitRepo, c, treePath, entryPaths...)
if err != nil {
return err
}
for _, treeEntry := range entries {
// entryMap won't contain "" therefore skip this.
if treeEntry.IsDir() {
subTree, err := tree.SubTree(ctx, gitRepo, treeEntry.Name())
if err != nil {
return err
}
if err := c.recursiveCache(ctx, gitRepo, subTree, treeEntry.Name(), level-1); err != nil {
return err
}
}
}
return nil
}
-63
View File
@@ -1,63 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"context"
"github.com/go-git/go-git/v5/plumbing"
cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph"
)
// CacheCommit will cache the commit from the gitRepository
func (c *Commit) CacheCommit(ctx context.Context, gitRepo *Repository) error {
commitNodeIndex, closer := gitRepo.CommitNodeIndex()
defer closer()
index, err := commitNodeIndex.Get(plumbing.Hash(c.ID.RawValue()))
if err != nil {
return err
}
return c.recursiveCache(ctx, gitRepo, index, c.Tree(), "", 1)
}
func (c *Commit) recursiveCache(ctx context.Context, gitRepo *Repository, index cgobject.CommitNode, tree *Tree, treePath string, level int) error {
if level == 0 {
return nil
}
entries, err := tree.ListEntries(ctx, gitRepo)
if err != nil {
return err
}
entryPaths := make([]string, len(entries))
entryMap := make(map[string]*TreeEntry)
for i, entry := range entries {
entryPaths[i] = entry.Name()
entryMap[entry.Name()] = entry
}
commits, err := getLastCommitForPathsByCommitNode(ctx, gitRepo, index, treePath, entryPaths)
if err != nil {
return err
}
for entry := range commits {
if entryMap[entry].IsDir() {
subTree, err := tree.SubTree(ctx, gitRepo, entry)
if err != nil {
return err
}
if err := c.recursiveCache(ctx, gitRepo, index, subTree, entry, level-1); err != nil {
return err
}
}
}
return nil
}
-50
View File
@@ -1,50 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"context"
)
// CacheCommit will cache the commit from the gitRepository
func (c *Commit) CacheCommit(ctx context.Context, gitRepo *Repository) error {
return c.recursiveCache(ctx, gitRepo, c.Tree(), "", 1)
}
func (c *Commit) recursiveCache(ctx context.Context, gitRepo *Repository, tree *Tree, treePath string, level int) error {
if level == 0 {
return nil
}
entries, err := tree.ListEntries(ctx, gitRepo)
if err != nil {
return err
}
entryPaths := make([]string, len(entries))
for i, entry := range entries {
entryPaths[i] = entry.Name()
}
_, err = walkGitLog(ctx, gitRepo, c, treePath, entryPaths...)
if err != nil {
return err
}
for _, treeEntry := range entries {
// entryMap won't contain "" therefore skip this.
if treeEntry.IsDir() {
subTree, err := tree.SubTree(ctx, gitRepo, treeEntry.Name())
if err != nil {
return err
}
if err := c.recursiveCache(ctx, gitRepo, subTree, treeEntry.Name(), level-1); err != nil {
return err
}
}
}
return nil
}
@@ -1,8 +1,6 @@
// Copyright 2021 The Gitea Authors. All rights reserved. // Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//go:build !gogit
package git package git
import ( import (
@@ -23,27 +21,18 @@ import (
// logNameStatusRepo opens git log --raw in the provided repo and returns a parser // logNameStatusRepo opens git log --raw in the provided repo and returns a parser
func logNameStatusRepo(ctx context.Context, repo RepositoryFacade, head, treepath string, paths ...string) *logNameStatusRepoParser { func logNameStatusRepo(ctx context.Context, repo RepositoryFacade, head, treepath string, paths ...string) *logNameStatusRepoParser {
cmd := gitcmd.NewCommand() cmd := gitcmd.NewCommand()
cmd.AddArguments("log", "--name-status", "-c", "--format=commit%x00%H %P%x00", "--parents", "--no-renames", "-t", "-z").AddDynamicArguments(head) cmd.AddArguments("log", "--name-status", "-c", "--format=commit%x00%H %P%x00", "--parents", "--no-renames", "--no-follow", "-t", "-z").AddDynamicArguments(head)
var files []string var files []string
if len(paths) < 70 {
if treepath != "" { if treepath != "" {
files = append(files, treepath) files = append(files, treepath) // matches all entries, adding them would not narrow the log
for _, pth := range paths { } else if len(paths) < 70 {
if pth != "" {
files = append(files, path.Join(treepath, pth))
}
}
} else {
for _, pth := range paths { for _, pth := range paths {
if pth != "" { if pth != "" {
files = append(files, pth) files = append(files, pth)
} }
} }
} }
} else if treepath != "" {
files = append(files, treepath)
}
// Use the :(literal) pathspec magic to handle edge cases with files named like ":file.txt" or "*.jpg" // Use the :(literal) pathspec magic to handle edge cases with files named like ":file.txt" or "*.jpg"
for i, file := range files { for i, file := range files {
files[i] = ":(literal)" + file files[i] = ":(literal)" + file
-30
View File
@@ -1,30 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/hash"
)
func ParseGogitHash(h plumbing.Hash) ObjectID {
switch hash.Size {
case 20:
return Sha1ObjectFormat.MustID(h[:])
case 32:
return Sha256ObjectFormat.MustID(h[:])
}
return nil
}
func ParseGogitHashArray(objectIDs []plumbing.Hash) []ObjectID {
ret := make([]ObjectID, len(objectIDs))
for i, h := range objectIDs {
ret[i] = ParseGogitHash(h)
}
return ret
}
-2
View File
@@ -43,8 +43,6 @@ func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
return entries, nil return entries, nil
} }
var _ = catBatchParseTreeEntries // bypass "unused" lint because it is only used by "nogogit"
func catBatchParseTreeEntries(objectFormat ObjectFormat, ptree *Tree, rd BufferedReader, sz int64) ([]*TreeEntry, error) { func catBatchParseTreeEntries(objectFormat ObjectFormat, ptree *Tree, rd BufferedReader, sz int64) ([]*TreeEntry, error) {
entries := make([]*TreeEntry, 0, 10) entries := make([]*TreeEntry, 0, 10)
-1
View File
@@ -16,7 +16,6 @@ import (
) )
func fillResultNameRev(ctx context.Context, repo git.RepositoryFacade, results []*LFSResult) error { func fillResultNameRev(ctx context.Context, repo git.RepositoryFacade, results []*LFSResult) error {
// Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
wg := errgroup.Group{} wg := errgroup.Group{}
cmd := gitcmd.NewCommand("name-rev", "--stdin", "--name-only", "--always").WithRepo(repo) cmd := gitcmd.NewCommand("name-rev", "--stdin", "--name-only", "--always").WithRepo(repo)
stdin, stdinClose := cmd.MakeStdinPipe() stdin, stdinClose := cmd.MakeStdinPipe()
@@ -2,12 +2,8 @@
// Copyright 2017 The Gitea Authors. All rights reserved. // Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//go:build !gogit
package git package git
const isGogit = false
type Repository struct { type Repository struct {
RepositoryBase RepositoryBase
} }
-97
View File
@@ -1,97 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"errors"
"path/filepath"
"slices"
"gitea.dev/modules/git/gitrepo"
"gitea.dev/modules/setting"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/osfs"
gogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/cache"
"github.com/go-git/go-git/v5/storage/filesystem"
)
const isGogit = true
type Repository struct {
RepositoryBase
gogitRepo *gogit.Repository
gogitStorage *reindexingStorage
}
// reindexingStorage picks up packs that git wrote after go-git loaded its index
// https://github.com/go-git/go-git/issues/2439
type reindexingStorage struct {
*filesystem.Storage
packs []plumbing.Hash
}
func (s *reindexingStorage) EncodedObject(t plumbing.ObjectType, h plumbing.Hash) (plumbing.EncodedObject, error) {
obj, err := s.Storage.EncodedObject(t, h)
if !errors.Is(err, plumbing.ErrObjectNotFound) {
return obj, err
}
packs, _ := s.ObjectPacks()
if slices.Equal(packs, s.packs) {
return obj, err
}
s.packs = packs
s.Reindex()
return s.Storage.EncodedObject(t, h)
}
func openRepositoryInternal(gitRepo *Repository) error {
repoPath := gitrepo.RepoLocalPath(gitRepo)
fs := osfs.New(repoPath)
_, err := fs.Stat(".git")
if err == nil {
fs, err = fs.Chroot(".git")
if err != nil {
return err
}
}
// the "clone --shared" repo doesn't work well with go-git AlternativeFS, https://github.com/go-git/go-git/issues/1006
// so use "/" for AlternatesFS, I guess it is the same behavior as current nogogit (no limitation or check for the "objects/info/alternates" paths), trust the "clone" command executed by the server.
var altFs billy.Filesystem
if setting.IsWindows {
altFs = osfs.New(filepath.VolumeName(setting.RepoRootPath) + "\\") // TODO: does it really work for Windows? Need some time to check.
} else {
altFs = osfs.New("/")
}
gitRepo.objectFormatCache = ParseGogitHash(plumbing.ZeroHash).Type()
storage := filesystem.NewStorageWithOptions(fs, cache.NewObjectLRUDefault(), filesystem.Options{KeepDescriptors: true, LargeObjectThreshold: setting.Git.LargeObjectThreshold, AlternatesFS: altFs})
packs, _ := storage.ObjectPacks()
gitRepo.gogitStorage = &reindexingStorage{Storage: storage, packs: packs}
gitRepo.gogitRepo, err = gogit.Open(gitRepo.gogitStorage, fs)
if err != nil {
_ = gitRepo.gogitStorage.Close()
return err
}
return nil
}
func (repo *Repository) closeInternal() error {
if repo.gogitStorage == nil {
return nil
}
err := repo.gogitStorage.Close()
repo.gogitStorage = nil
return err
}
// GoGitRepo gets the go-git repo representation
func (repo *Repository) GoGitRepo() *gogit.Repository {
return repo.gogitRepo
}
@@ -1,8 +1,6 @@
// Copyright 2026 The Gitea Authors. All rights reserved. // Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//go:build !gogit
package git package git
import ( import (
+171
View File
@@ -5,9 +5,13 @@
package git package git
import ( import (
"bufio"
"context" "context"
"io"
"strings"
"gitea.dev/modules/git/gitcmd" "gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/log"
) )
// BranchPrefix base dir of the branch information file store on git // BranchPrefix base dir of the branch information file store on git
@@ -24,3 +28,170 @@ func (repo *Repository) AddRemote(ctx context.Context, name, url string, fetch b
RunStdString(ctx) RunStdString(ctx)
return err return err
} }
// IsObjectExist returns true if the given object exists in the repository.
// FIXME: this function doesn't seem right, it is only used by GarbageCollectLFSMetaObjectsForRepo
func (repo *Repository) IsObjectExist(ctx context.Context, name string) bool {
if name == "" {
return false
}
batch, cancel, err := repo.CatFileBatch()
if err != nil {
log.Debug("Error opening CatFileBatch %v", err)
return false
}
defer cancel()
info, err := batch.QueryInfo(name)
if err != nil {
log.Debug("Error checking object info %v", err)
return false
}
return strings.HasPrefix(info.ID, name) // FIXME: this logic doesn't seem right, why "HasPrefix"
}
// IsReferenceExist returns true if given reference exists in the repository.
func (repo *Repository) IsReferenceExist(ctx context.Context, name string) bool {
if name == "" {
return false
}
batch, cancel, err := repo.CatFileBatch()
if err != nil {
log.Error("Error opening CatFileBatch %v", err)
return false
}
defer cancel()
_, err = batch.QueryInfo(name)
return err == nil
}
// IsBranchExist returns true if given branch exists in current repository.
func (repo *Repository) IsBranchExist(ctx context.Context, name string) bool {
if repo == nil || name == "" {
return false
}
return repo.IsReferenceExist(ctx, BranchPrefix+name)
}
// GetBranchNames returns branches from the repository, skipping "skip" initial branches and
// returning at most "limit" branches, or all branches if "limit" is 0.
func (repo *Repository) GetBranchNames(ctx context.Context, skip, limit int) ([]string, int, error) {
return callShowRef(ctx, repo, BranchPrefix, gitcmd.TrustedCmdArgs{BranchPrefix, "--sort=-committerdate"}, skip, limit)
}
// WalkReferences walks all the references from the repository
// refType should be empty, ObjectTag or ObjectBranch. All other values are equivalent to empty.
func (repo *Repository) WalkReferences(ctx context.Context, refType ObjectType, skip, limit int, walkfn func(sha1, refname string) error) (int, error) {
var args gitcmd.TrustedCmdArgs
switch refType {
case ObjectTag:
args = gitcmd.TrustedCmdArgs{TagPrefix, "--sort=-taggerdate"}
case ObjectBranch:
args = gitcmd.TrustedCmdArgs{BranchPrefix, "--sort=-committerdate"}
}
return WalkShowRef(ctx, repo, args, skip, limit, walkfn)
}
// callShowRef return refs, if limit = 0 it will not limit
func callShowRef(ctx context.Context, repo RepositoryFacade, trimPrefix string, extraArgs gitcmd.TrustedCmdArgs, skip, limit int) (branchNames []string, countAll int, err error) {
countAll, err = WalkShowRef(ctx, repo, extraArgs, skip, limit, func(_, branchName string) error {
branchName = strings.TrimPrefix(branchName, trimPrefix)
branchNames = append(branchNames, branchName)
return nil
})
return branchNames, countAll, err
}
func WalkShowRef(ctx context.Context, repo RepositoryFacade, extraArgs gitcmd.TrustedCmdArgs, skip, limit int, walkfn func(sha1, refname string) error) (countAll int, err error) {
i := 0
args := gitcmd.TrustedCmdArgs{"for-each-ref", "--format=%(objectname) %(refname)"}
args = append(args, extraArgs...)
cmd := gitcmd.NewCommand(args...)
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
defer stdoutReaderClose()
cmd.WithRepo(repo).
WithPipelineFunc(func(gitcmd.Context) error {
bufReader := bufio.NewReader(stdoutReader)
for i < skip {
_, isPrefix, err := bufReader.ReadLine()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
if !isPrefix {
i++
}
}
for limit == 0 || i < skip+limit {
// The output of show-ref is simply a list:
// <sha> SP <ref> LF
sha, err := bufReader.ReadString(' ')
if err == io.EOF {
return nil
}
if err != nil {
return err
}
branchName, err := bufReader.ReadString('\n')
if err == io.EOF {
// This shouldn't happen... but we'll tolerate it for the sake of peace
return nil
}
if err != nil {
return err
}
if len(branchName) > 0 {
branchName = branchName[:len(branchName)-1]
}
if len(sha) > 0 {
sha = sha[:len(sha)-1]
}
err = walkfn(sha, branchName)
if err != nil {
return err
}
i++
}
// count all refs
for limit != 0 {
_, isPrefix, err := bufReader.ReadLine()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
if !isPrefix {
i++
}
}
return nil
})
err = cmd.RunWithStderr(ctx)
if errPipeline, ok := gitcmd.UnwrapPipelineError(err); ok {
return i, errPipeline // keep the old behavior: return pipeline error directly
}
return i, err
}
// GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash
func (repo *Repository) GetRefsBySha(ctx context.Context, sha, prefix string) ([]string, error) {
var revList []string
_, err := WalkShowRef(ctx, repo, nil, 0, 0, func(walkSha, refname string) error {
if walkSha == sha && strings.HasPrefix(refname, prefix) {
revList = append(revList, refname)
}
return nil
})
return revList, err
}
-153
View File
@@ -1,153 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"context"
"sort"
"strings"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/storer"
)
// IsObjectExist returns true if the given object exists in the repository.
// FIXME: Inconsistent behavior with nogogit edition
// Unlike the implementation of IsObjectExist in nogogit edition, it does not support short hashes here.
// For example, IsObjectExist("153f451") will return false, but it will return true in nogogit edition.
// To fix this, the solution could be adding support for short hashes in gogit edition if it's really needed.
func (repo *Repository) IsObjectExist(_ context.Context, name string) bool {
if name == "" {
return false
}
_, err := repo.gogitRepo.Object(plumbing.AnyObject, plumbing.NewHash(name))
return err == nil
}
// IsReferenceExist returns true if given reference exists in the repository.
// FIXME: Inconsistent behavior with nogogit edition
// Unlike the implementation of IsObjectExist in nogogit edition, it does not support blob hashes here.
// For example, IsObjectExist([existing_blob_hash]) will return false, but it will return true in nogogit edition.
// To fix this, the solution could be refusing to support blob hashes in nogogit edition since a blob hash is not a reference.
func (repo *Repository) IsReferenceExist(_ context.Context, name string) bool {
if name == "" {
return false
}
_, err := repo.gogitRepo.ResolveRevision(plumbing.Revision(name))
return err == nil
}
// IsBranchExist returns true if given branch exists in current repository.
func (repo *Repository) IsBranchExist(_ context.Context, name string) bool {
if name == "" {
return false
}
reference, err := repo.gogitRepo.Reference(plumbing.ReferenceName(BranchPrefix+name), true)
if err != nil {
return false
}
return reference.Type() != plumbing.InvalidReference
}
// GetBranches returns branches from the repository, skipping "skip" initial branches and
// returning at most "limit" branches, or all branches if "limit" is 0.
// Branches are returned with sort of `-committerdate` as the nogogit
// implementation. This requires full fetch, sort and then the
// skip/limit applies later as gogit returns in undefined order.
func (repo *Repository) GetBranchNames(_ context.Context, skip, limit int) ([]string, int, error) {
type BranchData struct {
name string
committerDate int64
}
var branchData []BranchData
branchIter, err := repo.gogitRepo.Branches()
if err != nil {
return nil, 0, err
}
_ = branchIter.ForEach(func(branch *plumbing.Reference) error {
obj, err := repo.gogitRepo.CommitObject(branch.Hash())
if err != nil {
// skip branch if can't find commit
return nil
}
branchData = append(branchData, BranchData{strings.TrimPrefix(branch.Name().String(), BranchPrefix), obj.Committer.When.Unix()})
return nil
})
sort.Slice(branchData, func(i, j int) bool {
return !(branchData[i].committerDate < branchData[j].committerDate)
})
var branchNames []string
maxPos := len(branchData)
if limit > 0 {
maxPos = min(skip+limit, maxPos)
}
for i := skip; i < maxPos; i++ {
branchNames = append(branchNames, branchData[i].name)
}
return branchNames, len(branchData), nil
}
// WalkReferences walks all the references from the repository
func (repo *Repository) WalkReferences(ctx context.Context, arg ObjectType, skip, limit int, walkfn func(sha1, refname string) error) (int, error) {
i := 0
var iter storer.ReferenceIter
var err error
switch arg {
case ObjectTag:
iter, err = repo.gogitRepo.Tags()
case ObjectBranch:
iter, err = repo.gogitRepo.Branches()
default:
iter, err = repo.gogitRepo.References()
}
if err != nil {
return i, err
}
defer iter.Close()
err = iter.ForEach(func(ref *plumbing.Reference) error {
if i < skip {
i++
return nil
}
err := walkfn(ref.Hash().String(), string(ref.Name()))
i++
if err != nil {
return err
}
if limit != 0 && i >= skip+limit {
return storer.ErrStop
}
return ctx.Err()
})
return i, err
}
// GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash
func (repo *Repository) GetRefsBySha(ctx context.Context, sha, prefix string) ([]string, error) {
var revList []string
iter, err := repo.gogitRepo.References()
if err != nil {
return nil, err
}
err = iter.ForEach(func(ref *plumbing.Reference) error {
if ref.Hash().String() == sha && strings.HasPrefix(string(ref.Name()), prefix) {
revList = append(revList, string(ref.Name()))
}
return ctx.Err()
})
return revList, err
}
-184
View File
@@ -1,184 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"bufio"
"context"
"io"
"strings"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/log"
)
// IsObjectExist returns true if the given object exists in the repository.
// FIXME: this function doesn't seem right, it is only used by GarbageCollectLFSMetaObjectsForRepo
func (repo *Repository) IsObjectExist(ctx context.Context, name string) bool {
if name == "" {
return false
}
batch, cancel, err := repo.CatFileBatch()
if err != nil {
log.Debug("Error opening CatFileBatch %v", err)
return false
}
defer cancel()
info, err := batch.QueryInfo(name)
if err != nil {
log.Debug("Error checking object info %v", err)
return false
}
return strings.HasPrefix(info.ID, name) // FIXME: this logic doesn't seem right, why "HasPrefix"
}
// IsReferenceExist returns true if given reference exists in the repository.
func (repo *Repository) IsReferenceExist(ctx context.Context, name string) bool {
if name == "" {
return false
}
batch, cancel, err := repo.CatFileBatch()
if err != nil {
log.Error("Error opening CatFileBatch %v", err)
return false
}
defer cancel()
_, err = batch.QueryInfo(name)
return err == nil
}
// IsBranchExist returns true if given branch exists in current repository.
func (repo *Repository) IsBranchExist(ctx context.Context, name string) bool {
if repo == nil || name == "" {
return false
}
return repo.IsReferenceExist(ctx, BranchPrefix+name)
}
// GetBranchNames returns branches from the repository, skipping "skip" initial branches and
// returning at most "limit" branches, or all branches if "limit" is 0.
func (repo *Repository) GetBranchNames(ctx context.Context, skip, limit int) ([]string, int, error) {
return callShowRef(ctx, repo, BranchPrefix, gitcmd.TrustedCmdArgs{BranchPrefix, "--sort=-committerdate"}, skip, limit)
}
// WalkReferences walks all the references from the repository
// refType should be empty, ObjectTag or ObjectBranch. All other values are equivalent to empty.
func (repo *Repository) WalkReferences(ctx context.Context, refType ObjectType, skip, limit int, walkfn func(sha1, refname string) error) (int, error) {
var args gitcmd.TrustedCmdArgs
switch refType {
case ObjectTag:
args = gitcmd.TrustedCmdArgs{TagPrefix, "--sort=-taggerdate"}
case ObjectBranch:
args = gitcmd.TrustedCmdArgs{BranchPrefix, "--sort=-committerdate"}
}
return WalkShowRef(ctx, repo, args, skip, limit, walkfn)
}
// callShowRef return refs, if limit = 0 it will not limit
func callShowRef(ctx context.Context, repo RepositoryFacade, trimPrefix string, extraArgs gitcmd.TrustedCmdArgs, skip, limit int) (branchNames []string, countAll int, err error) {
countAll, err = WalkShowRef(ctx, repo, extraArgs, skip, limit, func(_, branchName string) error {
branchName = strings.TrimPrefix(branchName, trimPrefix)
branchNames = append(branchNames, branchName)
return nil
})
return branchNames, countAll, err
}
func WalkShowRef(ctx context.Context, repo RepositoryFacade, extraArgs gitcmd.TrustedCmdArgs, skip, limit int, walkfn func(sha1, refname string) error) (countAll int, err error) {
i := 0
args := gitcmd.TrustedCmdArgs{"for-each-ref", "--format=%(objectname) %(refname)"}
args = append(args, extraArgs...)
cmd := gitcmd.NewCommand(args...)
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
defer stdoutReaderClose()
cmd.WithRepo(repo).
WithPipelineFunc(func(gitcmd.Context) error {
bufReader := bufio.NewReader(stdoutReader)
for i < skip {
_, isPrefix, err := bufReader.ReadLine()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
if !isPrefix {
i++
}
}
for limit == 0 || i < skip+limit {
// The output of show-ref is simply a list:
// <sha> SP <ref> LF
sha, err := bufReader.ReadString(' ')
if err == io.EOF {
return nil
}
if err != nil {
return err
}
branchName, err := bufReader.ReadString('\n')
if err == io.EOF {
// This shouldn't happen... but we'll tolerate it for the sake of peace
return nil
}
if err != nil {
return err
}
if len(branchName) > 0 {
branchName = branchName[:len(branchName)-1]
}
if len(sha) > 0 {
sha = sha[:len(sha)-1]
}
err = walkfn(sha, branchName)
if err != nil {
return err
}
i++
}
// count all refs
for limit != 0 {
_, isPrefix, err := bufReader.ReadLine()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
if !isPrefix {
i++
}
}
return nil
})
err = cmd.RunWithStderr(ctx)
if errPipeline, ok := gitcmd.UnwrapPipelineError(err); ok {
return i, errPipeline // keep the old behavior: return pipeline error directly
}
return i, err
}
// GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash
func (repo *Repository) GetRefsBySha(ctx context.Context, sha, prefix string) ([]string, error) {
var revList []string
_, err := WalkShowRef(ctx, repo, nil, 0, 0, func(walkSha, refname string) error {
if walkSha == sha && strings.HasPrefix(refname, prefix) {
revList = append(revList, refname)
}
return nil
})
return revList, err
}
+4 -12
View File
@@ -102,10 +102,6 @@ func TestRepository_IsObjectExist(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
defer repo.Close() defer repo.Close()
// FIXME: Inconsistent behavior between gogit and nogogit editions
// See the comment of IsObjectExist in gogit edition for more details.
supportShortHash := !isGogit
tests := []struct { tests := []struct {
name string name string
arg string arg string
@@ -129,7 +125,7 @@ func TestRepository_IsObjectExist(t *testing.T) {
{ {
name: "short commit hash", name: "short commit hash",
arg: "ce06481", arg: "ce06481",
want: supportShortHash, want: true,
}, },
{ {
name: "blob hash", name: "blob hash",
@@ -139,7 +135,7 @@ func TestRepository_IsObjectExist(t *testing.T) {
{ {
name: "short blob hash", name: "short blob hash",
arg: "153f451", arg: "153f451",
want: supportShortHash, want: true,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
@@ -155,10 +151,6 @@ func TestRepository_IsReferenceExist(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
defer repo.Close() defer repo.Close()
// FIXME: Inconsistent behavior between gogit and nogogit editions
// See the comment of IsReferenceExist in gogit edition for more details.
supportBlobHash := !isGogit
tests := []struct { tests := []struct {
name string name string
arg string arg string
@@ -187,12 +179,12 @@ func TestRepository_IsReferenceExist(t *testing.T) {
{ {
name: "blob hash", name: "blob hash",
arg: "153f451b9ee7fa1da317ab17a127e9fd9d384310", arg: "153f451b9ee7fa1da317ab17a127e9fd9d384310",
want: supportBlobHash, want: true,
}, },
{ {
name: "short blob hash", name: "short blob hash",
arg: "153f451", arg: "153f451",
want: supportBlobHash, want: true,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
+122
View File
@@ -7,6 +7,7 @@ package git
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"io" "io"
"strconv" "strconv"
"strings" "strings"
@@ -458,3 +459,124 @@ func (repo *Repository) GetCommitBranchStart(ctx context.Context, env []string,
return "", nil return "", nil
} }
// GetRefCommitID returns the last commit ID string of given reference (branch or tag).
func (repo *Repository) GetRefCommitID(ctx context.Context, name string) (string, error) {
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return "", err
}
defer cancel()
info, err := batch.QueryInfo(name)
if IsErrNotExist(err) {
return "", ErrNotExist{name, ""}
} else if err != nil {
return "", err
}
return info.ID, nil
}
func (repo *Repository) getCommit(_ context.Context, id ObjectID) (*Commit, error) {
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return nil, err
}
defer cancel()
return repo.getCommitWithBatch(batch, id)
}
func limitDiscardReader(rd BufferedReader, full, limit int64) (io.Reader, func() error) {
return io.LimitReader(rd, min(full, limit)), func() error {
if full > limit {
return DiscardFull(rd, full-limit)
}
return nil
}
}
func (repo *Repository) getCommitWithBatch(batch CatFileBatch, id ObjectID) (*Commit, error) {
info, rd, err := batch.QueryContent(id.String())
if err != nil {
if errors.Is(err, io.EOF) || IsErrNotExist(err) {
return nil, ErrNotExist{ID: id.String()}
}
return nil, err
}
switch info.Type {
case "missing":
return nil, ErrNotExist{ID: id.String()}
case "tag":
limitReader, limitDiscard := limitDiscardReader(rd, info.Size, MaxGitObjectSize)
data, err := io.ReadAll(limitReader)
if err != nil {
return nil, err
}
if err = limitDiscard(); err != nil {
return nil, err
}
_, err = rd.Discard(1)
if err != nil {
return nil, err
}
tag, err := parseTagData(id.Type(), data)
if err != nil {
return nil, err
}
return repo.getCommitWithBatch(batch, tag.Object)
case "commit":
limitReader, limitDiscard := limitDiscardReader(rd, info.Size, MaxGitObjectSize)
commit, err := CommitFromReader(id, limitReader)
if err != nil {
return nil, err
}
if err = limitDiscard(); err != nil {
return nil, err
}
_, err = rd.Discard(1)
if err != nil {
return nil, err
}
return commit, nil
default:
if info.Type != "blob" && info.Type != "tree" {
setting.PanicInDevOrTesting("Unknown cat-file object type %s for object %s in repo %s", info.Type, id.String(), repo.LogString())
}
if err := DiscardFull(rd, info.Size+1); err != nil {
return nil, err
}
return nil, ErrNotExist{
ID: id.String(),
}
}
}
// ConvertToGitID returns a git object ID from the git ref, it doesn't guarantee the returned ID really exists
func (repo *Repository) ConvertToGitID(ctx context.Context, ref string) (ObjectID, error) {
objectFormat, err := repo.GetObjectFormat(ctx)
if err != nil {
return nil, err
}
if IsStringValidObjectID(objectFormat, ref) {
id, err := NewIDFromString(ref)
if err == nil {
return id, nil
}
}
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return nil, err
}
defer cancel()
info, err := batch.QueryInfo(ref)
if err != nil {
if IsErrNotExist(err) {
return nil, ErrNotExist{ref, ""}
}
return nil, err
}
return MustIDFromString(info.ID), nil
}
-105
View File
@@ -1,105 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"context"
"strings"
"gitea.dev/modules/git/gitcmd"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
)
// GetRefCommitID returns the last commit ID string of given reference.
func (repo *Repository) GetRefCommitID(_ context.Context, name string) (string, error) {
if plumbing.IsHash(name) {
return name, nil
}
refName := plumbing.ReferenceName(name)
if err := refName.Validate(); err != nil {
// Match the nogogit behavior: an unresolvable/invalid ref name
// is reported as not-existing rather than a generic validation error,
// so callers can rely on IsErrNotExist regardless of build tag.
return "", ErrNotExist{ID: name}
}
ref, err := repo.gogitRepo.Reference(refName, true)
if err != nil {
if err == plumbing.ErrReferenceNotFound {
return "", ErrNotExist{
ID: name,
}
}
return "", err
}
return ref.Hash().String(), nil
}
// ConvertToHash returns a Hash object from a potential ID string
func (repo *Repository) ConvertToGitID(ctx context.Context, commitID string) (ObjectID, error) {
objectFormat, err := repo.GetObjectFormat(ctx)
if err != nil {
return nil, err
}
if IsStringValidObjectID(objectFormat, commitID) {
ID, err := NewIDFromString(commitID)
if err == nil {
return ID, nil
}
}
actualCommitID, _, err := gitcmd.NewCommand("rev-parse", "--verify").
AddDynamicArguments(commitID).
WithRepo(repo).
RunStdString(ctx)
actualCommitID = strings.TrimSpace(actualCommitID)
if err != nil {
if strings.Contains(err.Error(), "unknown revision or path") ||
strings.Contains(err.Error(), "fatal: Needed a single revision") {
return objectFormat.EmptyObjectID(), ErrNotExist{commitID, ""}
}
return objectFormat.EmptyObjectID(), err
}
return NewIDFromString(actualCommitID)
}
func (repo *Repository) getCommit(_ context.Context, id ObjectID) (*Commit, error) {
var tagObject *object.Tag
commitID := plumbing.Hash(id.RawValue())
gogitCommit, err := repo.gogitRepo.CommitObject(commitID)
if err == plumbing.ErrObjectNotFound {
tagObject, err = repo.gogitRepo.TagObject(commitID)
if err == plumbing.ErrObjectNotFound {
return nil, ErrNotExist{
ID: id.String(),
}
}
if err == nil {
gogitCommit, err = repo.gogitRepo.CommitObject(tagObject.Target)
}
// if we get a plumbing.ErrObjectNotFound here then the repository is broken and it should be 500
}
if err != nil {
return nil, err
}
commit := convertCommit(gogitCommit)
tree, err := gogitCommit.Tree()
if err != nil {
return nil, err
}
commit.TreeID = ParseGogitHash(tree.Hash)
commit.Tree().resolvedGogitTreeObject = tree
return commit, nil
}
-135
View File
@@ -1,135 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"context"
"errors"
"io"
"gitea.dev/modules/setting"
)
// GetRefCommitID returns the last commit ID string of given reference (branch or tag).
func (repo *Repository) GetRefCommitID(ctx context.Context, name string) (string, error) {
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return "", err
}
defer cancel()
info, err := batch.QueryInfo(name)
if IsErrNotExist(err) {
return "", ErrNotExist{name, ""}
} else if err != nil {
return "", err
}
return info.ID, nil
}
func (repo *Repository) getCommit(ctx context.Context, id ObjectID) (*Commit, error) {
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return nil, err
}
defer cancel()
return repo.getCommitWithBatch(batch, id)
}
func limitDiscardReader(rd BufferedReader, full, limit int64) (io.Reader, func() error) {
return io.LimitReader(rd, min(full, limit)), func() error {
if full > limit {
return DiscardFull(rd, full-limit)
}
return nil
}
}
func (repo *Repository) getCommitWithBatch(batch CatFileBatch, id ObjectID) (*Commit, error) {
info, rd, err := batch.QueryContent(id.String())
if err != nil {
if errors.Is(err, io.EOF) || IsErrNotExist(err) {
return nil, ErrNotExist{ID: id.String()}
}
return nil, err
}
switch info.Type {
case "missing":
return nil, ErrNotExist{ID: id.String()}
case "tag":
limitReader, limitDiscard := limitDiscardReader(rd, info.Size, MaxGitObjectSize)
data, err := io.ReadAll(limitReader)
if err != nil {
return nil, err
}
if err = limitDiscard(); err != nil {
return nil, err
}
_, err = rd.Discard(1)
if err != nil {
return nil, err
}
tag, err := parseTagData(id.Type(), data)
if err != nil {
return nil, err
}
return repo.getCommitWithBatch(batch, tag.Object)
case "commit":
limitReader, limitDiscard := limitDiscardReader(rd, info.Size, MaxGitObjectSize)
commit, err := CommitFromReader(id, limitReader)
if err != nil {
return nil, err
}
if err = limitDiscard(); err != nil {
return nil, err
}
_, err = rd.Discard(1)
if err != nil {
return nil, err
}
return commit, nil
default:
if info.Type != "blob" && info.Type != "tree" {
setting.PanicInDevOrTesting("Unknown cat-file object type %s for object %s in repo %s", info.Type, id.String(), repo.LogString())
}
if err := DiscardFull(rd, info.Size+1); err != nil {
return nil, err
}
return nil, ErrNotExist{
ID: id.String(),
}
}
}
// ConvertToGitID returns a git object ID from the git ref, it doesn't guarantee the returned ID really exists
func (repo *Repository) ConvertToGitID(ctx context.Context, ref string) (ObjectID, error) {
objectFormat, err := repo.GetObjectFormat(ctx)
if err != nil {
return nil, err
}
if IsStringValidObjectID(objectFormat, ref) {
id, err := NewIDFromString(ref)
if err == nil {
return id, nil
}
}
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return nil, err
}
defer cancel()
info, err := batch.QueryInfo(ref)
if err != nil {
if IsErrNotExist(err) {
return nil, ErrNotExist{ref, ""}
}
return nil, err
}
return MustIDFromString(info.ID), nil
}
-38
View File
@@ -1,38 +0,0 @@
// Copyright 2019 The Gitea Authors.
// All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"os"
"path/filepath"
"gitea.dev/modules/git/gitrepo"
"gitea.dev/modules/log"
commitgraph "github.com/go-git/go-git/v5/plumbing/format/commitgraph/v2"
cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph"
)
// CommitNodeIndex returns the index for walking commit graph
func (repo *Repository) CommitNodeIndex() (_ cgobject.CommitNodeIndex, closer func()) {
indexPath := filepath.Join(gitrepo.RepoLocalPath(repo), "objects", "info", "commit-graph")
file, err := os.Open(indexPath)
if err == nil {
var index commitgraph.Index
index, err = commitgraph.OpenFileIndex(file)
if err == nil {
return cgobject.NewGraphCommitNodeIndex(index, repo.gogitRepo.Storer), func() { _ = file.Close() }
}
_ = file.Close()
}
if !os.IsNotExist(err) {
log.Warn("Unable to read commit-graph for %s: %v", repo.LogString(), err)
}
return cgobject.NewObjectCommitNodeIndex(repo.gogitRepo.Storer), func() {}
}
+63
View File
@@ -4,7 +4,9 @@
package git package git
import ( import (
"bufio"
"context" "context"
"io"
"strings" "strings"
"gitea.dev/modules/git/gitcmd" "gitea.dev/modules/git/gitcmd"
@@ -96,3 +98,64 @@ func (repo *Repository) UnstableGuessRefByShortName(ctx context.Context, shortNa
} }
return "" return ""
} }
// GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with.
func (repo *Repository) GetRefsFiltered(ctx context.Context, pattern string) ([]*Reference, error) {
refs := make([]*Reference, 0)
cmd := gitcmd.NewCommand("for-each-ref")
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
defer stdoutReaderClose()
err := cmd.WithRepo(repo).
WithPipelineFunc(func(context gitcmd.Context) error {
bufReader := bufio.NewReader(stdoutReader)
for {
// The output of for-each-ref is simply a list:
// <sha> SP <type> TAB <ref> LF
sha, err := bufReader.ReadString(' ')
if err == io.EOF {
break
}
if err != nil {
return err
}
sha = sha[:len(sha)-1]
typ, err := bufReader.ReadString('\t')
if err == io.EOF {
// This should not happen, but we'll tolerate it
break
}
if err != nil {
return err
}
typ = typ[:len(typ)-1]
refName, err := bufReader.ReadString('\n')
if err == io.EOF {
// This should not happen, but we'll tolerate it
break
}
if err != nil {
return err
}
refName = refName[:len(refName)-1]
// refName cannot be HEAD but can be remotes or stash
if strings.HasPrefix(refName, RemotePrefix) || refName == "/refs/stash" {
continue
}
if pattern == "" || strings.HasPrefix(refName, pattern) {
r := &Reference{
Name: refName,
Object: MustIDFromString(sha),
Type: typ,
repo: repo,
}
refs = append(refs, r)
}
}
return nil
}).RunWithStderr(ctx)
return refs, err
}
-48
View File
@@ -1,48 +0,0 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"context"
"strings"
"github.com/go-git/go-git/v5/plumbing"
)
// GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with.
func (repo *Repository) GetRefsFiltered(ctx context.Context, pattern string) ([]*Reference, error) {
r := repo.gogitRepo
refsIter, err := r.References()
if err != nil {
return nil, err
}
refs := make([]*Reference, 0)
if err = refsIter.ForEach(func(ref *plumbing.Reference) error {
if ref.Name() != plumbing.HEAD && !ref.Name().IsRemote() &&
(pattern == "" || strings.HasPrefix(ref.Name().String(), pattern)) {
refType := string(ObjectCommit)
if ref.Name().IsTag() {
// tags can be of type `commit` (lightweight) or `tag` (annotated)
if tagType, _ := repo.GetTagType(ctx, ParseGogitHash(ref.Hash())); err == nil {
refType = tagType
}
}
r := &Reference{
Name: ref.Name().String(),
Object: ParseGogitHash(ref.Hash()),
Type: refType,
repo: repo,
}
refs = append(refs, r)
}
return nil
}); err != nil {
return nil, err
}
return refs, nil
}
-76
View File
@@ -1,76 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"bufio"
"context"
"io"
"strings"
"gitea.dev/modules/git/gitcmd"
)
// GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with.
func (repo *Repository) GetRefsFiltered(ctx context.Context, pattern string) ([]*Reference, error) {
refs := make([]*Reference, 0)
cmd := gitcmd.NewCommand("for-each-ref")
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
defer stdoutReaderClose()
err := cmd.WithRepo(repo).
WithPipelineFunc(func(context gitcmd.Context) error {
bufReader := bufio.NewReader(stdoutReader)
for {
// The output of for-each-ref is simply a list:
// <sha> SP <type> TAB <ref> LF
sha, err := bufReader.ReadString(' ')
if err == io.EOF {
break
}
if err != nil {
return err
}
sha = sha[:len(sha)-1]
typ, err := bufReader.ReadString('\t')
if err == io.EOF {
// This should not happen, but we'll tolerate it
break
}
if err != nil {
return err
}
typ = typ[:len(typ)-1]
refName, err := bufReader.ReadString('\n')
if err == io.EOF {
// This should not happen, but we'll tolerate it
break
}
if err != nil {
return err
}
refName = refName[:len(refName)-1]
// refName cannot be HEAD but can be remotes or stash
if strings.HasPrefix(refName, RemotePrefix) || refName == "/refs/stash" {
continue
}
if pattern == "" || strings.HasPrefix(refName, pattern) {
r := &Reference{
Name: refName,
Object: MustIDFromString(sha),
Type: typ,
repo: repo,
}
refs = append(refs, r)
}
}
return nil
}).RunWithStderr(ctx)
return refs, err
}
+23 -6
View File
@@ -61,10 +61,28 @@ func (repo *Repository) GetCodeActivityStats(ctx context.Context, fromTime time.
} else { } else {
gitCmd.AddArguments("--first-parent").AddDynamicArguments(branch) gitCmd.AddArguments("--first-parent").AddDynamicArguments(branch)
} }
if err := parseCodeActivityLog(ctx, repo, gitCmd, stats); err != nil {
return nil, err
}
return stats, nil
}
// GetCodeActivityAuthors returns the authors of non-merge commits in all branches since fromTime, sorted by commit count
func GetCodeActivityAuthors(ctx context.Context, repo RepositoryFacade, fromTime time.Time) ([]*CodeActivityAuthor, error) {
stats := &CodeActivityStats{}
gitCmd := gitcmd.NewCommand("log", "--no-merges", "--pretty=format:---%n%H%n%aN%n%aE%n", "--branches=*").
AddOptionFormat("--since=%s", fromTime.Format(time.RFC3339))
if err := parseCodeActivityLog(ctx, repo, gitCmd, stats); err != nil {
return nil, err
}
return stats.Authors, nil
}
func parseCodeActivityLog(ctx context.Context, repo RepositoryFacade, gitCmd *gitcmd.Command, stats *CodeActivityStats) error {
stdoutReader, stdoutReaderClose := gitCmd.MakeStdoutPipe() stdoutReader, stdoutReaderClose := gitCmd.MakeStdoutPipe()
defer stdoutReaderClose() defer stdoutReaderClose()
err = gitCmd. err := gitCmd.
WithRepo(repo). WithRepo(repo).
WithPipelineFunc(func(ctx gitcmd.Context) error { WithPipelineFunc(func(ctx gitcmd.Context) error {
scanner := bufio.NewScanner(stdoutReader) scanner := bufio.NewScanner(stdoutReader)
@@ -116,8 +134,8 @@ func (repo *Repository) GetCodeActivityStats(ctx context.Context, fromTime time.
} }
} }
} }
if err = scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
return fmt.Errorf("GetCodeActivityStats scan: %w", err) return fmt.Errorf("parseCodeActivityLog scan: %w", err)
} }
a := make([]*CodeActivityAuthor, 0, len(authors)) a := make([]*CodeActivityAuthor, 0, len(authors))
for _, v := range authors { for _, v := range authors {
@@ -134,8 +152,7 @@ func (repo *Repository) GetCodeActivityStats(ctx context.Context, fromTime time.
}). }).
RunWithStderr(ctx) RunWithStderr(ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("GetCodeActivityStats: %w", err) return fmt.Errorf("parseCodeActivityLog: %w", err)
} }
return nil
return stats, nil
} }
+4
View File
@@ -33,4 +33,8 @@ func TestRepository_GetCodeActivityStats(t *testing.T) {
assert.Equal(t, "tris.git@shoddynet.org", code.Authors[1].Email) assert.Equal(t, "tris.git@shoddynet.org", code.Authors[1].Email)
assert.EqualValues(t, 3, code.Authors[1].Commits) assert.EqualValues(t, 3, code.Authors[1].Commits)
assert.EqualValues(t, 5, code.Authors[0].Commits) assert.EqualValues(t, 5, code.Authors[0].Commits)
authors, err := GetCodeActivityAuthors(t.Context(), bareRepo1, timeFrom)
assert.NoError(t, err)
assert.Equal(t, code.Authors, authors)
} }
+122
View File
@@ -6,13 +6,16 @@ package git
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"gitea.dev/modules/git/foreachref" "gitea.dev/modules/git/foreachref"
"gitea.dev/modules/git/gitcmd" "gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/log"
"gitea.dev/modules/util" "gitea.dev/modules/util"
) )
@@ -232,3 +235,122 @@ func (repo *Repository) GetAnnotatedTag(ctx context.Context, sha string) (*Tag,
} }
return tag, nil return tag, nil
} }
// IsTagExist returns true if given tag exists in the repository.
func (repo *Repository) IsTagExist(ctx context.Context, name string) bool {
if repo == nil || name == "" {
return false
}
return repo.IsReferenceExist(ctx, TagPrefix+name)
}
// GetTagType gets the type of the tag, either commit (simple) or tag (annotated)
func (repo *Repository) GetTagType(ctx context.Context, id ObjectID) (string, error) {
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return "", err
}
defer cancel()
info, err := batch.QueryInfo(id.String())
if err != nil {
if IsErrNotExist(err) {
return "", ErrNotExist{ID: id.String()}
}
return "", err
}
return info.Type, nil
}
func (repo *Repository) getTag(ctx context.Context, tagID ObjectID, name string) (*Tag, error) {
t, ok := repo.tagCache.Get(tagID.String())
if ok {
log.Debug("Hit cache: %s", tagID)
tagClone := *t
tagClone.Name = name // This is necessary because lightweight tags may have same id
return &tagClone, nil
}
tp, err := repo.GetTagType(ctx, tagID)
if err != nil {
return nil, err
}
// Get the commit ID and tag ID (may be different for annotated tag) for the returned tag object
commitIDStr, err := repo.GetTagCommitID(ctx, name)
if err != nil {
// every tag should have a commit ID so return all errors
return nil, err
}
commitID, err := NewIDFromString(commitIDStr)
if err != nil {
return nil, err
}
// If type is "commit, the tag is a lightweight tag
if ObjectType(tp) == ObjectCommit {
commit, err := repo.GetCommit(ctx, commitIDStr)
if err != nil {
return nil, err
}
tag := &Tag{
Name: name,
ID: tagID,
Object: commitID,
Type: tp,
Tagger: commit.Committer,
CommitMessage: commit.CommitMessage,
}
repo.tagCache.Set(tagID.String(), tag)
return tag, nil
}
// The tag is an annotated tag with a message.
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return nil, err
}
defer cancel()
info, rd, err := batch.QueryContent(tagID.String())
if err != nil {
if errors.Is(err, io.EOF) || IsErrNotExist(err) {
return nil, ErrNotExist{ID: tagID.String()}
}
return nil, err
}
typ, size := info.Type, info.Size
if typ != "tag" {
if err := DiscardFull(rd, size+1); err != nil {
return nil, err
}
return nil, ErrNotExist{ID: tagID.String()}
}
// then we need to parse the tag and load the commit
limitReader, limitDiscard := limitDiscardReader(rd, info.Size, MaxGitObjectSize)
data, err := io.ReadAll(limitReader)
if err != nil {
return nil, err
}
if err = limitDiscard(); err != nil {
return nil, err
}
_, err = rd.Discard(1)
if err != nil {
return nil, err
}
tag, err := parseTagData(tagID.Type(), data)
if err != nil {
return nil, err
}
tag.Name = name
tag.ID = tagID
tag.Type = tp
repo.tagCache.Set(tagID.String(), tag)
return tag, nil
}
-101
View File
@@ -1,101 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"context"
"gitea.dev/modules/log"
"github.com/go-git/go-git/v5/plumbing"
)
// IsTagExist returns true if given tag exists in the repository.
func (repo *Repository) IsTagExist(_ context.Context, name string) bool {
_, err := repo.gogitRepo.Reference(plumbing.ReferenceName(TagPrefix+name), true)
return err == nil
}
// GetTagType gets the type of the tag, either commit (simple) or tag (annotated)
func (repo *Repository) GetTagType(_ context.Context, id ObjectID) (string, error) {
// Get tag type
obj, err := repo.gogitRepo.Object(plumbing.AnyObject, plumbing.Hash(id.RawValue()))
if err != nil {
if err == plumbing.ErrReferenceNotFound {
return "", &ErrNotExist{ID: id.String()}
}
return "", err
}
return obj.Type().String(), nil
}
func (repo *Repository) getTag(ctx context.Context, tagID ObjectID, name string) (*Tag, error) {
t, ok := repo.tagCache.Get(tagID.String())
if ok {
log.Debug("Hit cache: %s", tagID)
tagClone := *t
tagClone.Name = name // This is necessary because lightweight tags may have same id
return &tagClone, nil
}
tp, err := repo.GetTagType(ctx, tagID)
if err != nil {
return nil, err
}
// Get the commit ID and tag ID (may be different for annotated tag) for the returned tag object
commitIDStr, err := repo.GetTagCommitID(ctx, name)
if err != nil {
// every tag should have a commit ID so return all errors
return nil, err
}
commitID, err := NewIDFromString(commitIDStr)
if err != nil {
return nil, err
}
// If type is "commit, the tag is a lightweight tag
if ObjectType(tp) == ObjectCommit {
commit, err := repo.GetCommit(ctx, commitIDStr)
if err != nil {
return nil, err
}
tag := &Tag{
Name: name,
ID: tagID,
Object: commitID,
Type: tp,
Tagger: commit.Committer,
CommitMessage: CommitMessage{MessageRaw: commit.CommitMessage.MessageRaw},
}
repo.tagCache.Set(tagID.String(), tag)
return tag, nil
}
gogitTag, err := repo.gogitRepo.TagObject(plumbing.Hash(tagID.RawValue()))
if err != nil {
if err == plumbing.ErrReferenceNotFound {
return nil, &ErrNotExist{ID: tagID.String()}
}
return nil, err
}
tag := &Tag{
Name: name,
ID: tagID,
Object: commitID.Type().MustID(gogitTag.Target[:]),
Type: tp,
Tagger: &gogitTag.Tagger,
CommitMessage: CommitMessage{MessageRaw: gogitTag.Message},
}
repo.tagCache.Set(tagID.String(), tag)
return tag, nil
}
-134
View File
@@ -1,134 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"context"
"errors"
"io"
"gitea.dev/modules/log"
)
// IsTagExist returns true if given tag exists in the repository.
func (repo *Repository) IsTagExist(ctx context.Context, name string) bool {
if repo == nil || name == "" {
return false
}
return repo.IsReferenceExist(ctx, TagPrefix+name)
}
// GetTagType gets the type of the tag, either commit (simple) or tag (annotated)
func (repo *Repository) GetTagType(ctx context.Context, id ObjectID) (string, error) {
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return "", err
}
defer cancel()
info, err := batch.QueryInfo(id.String())
if err != nil {
if IsErrNotExist(err) {
return "", ErrNotExist{ID: id.String()}
}
return "", err
}
return info.Type, nil
}
func (repo *Repository) getTag(ctx context.Context, tagID ObjectID, name string) (*Tag, error) {
t, ok := repo.tagCache.Get(tagID.String())
if ok {
log.Debug("Hit cache: %s", tagID)
tagClone := *t
tagClone.Name = name // This is necessary because lightweight tags may have same id
return &tagClone, nil
}
tp, err := repo.GetTagType(ctx, tagID)
if err != nil {
return nil, err
}
// Get the commit ID and tag ID (may be different for annotated tag) for the returned tag object
commitIDStr, err := repo.GetTagCommitID(ctx, name)
if err != nil {
// every tag should have a commit ID so return all errors
return nil, err
}
commitID, err := NewIDFromString(commitIDStr)
if err != nil {
return nil, err
}
// If type is "commit, the tag is a lightweight tag
if ObjectType(tp) == ObjectCommit {
commit, err := repo.GetCommit(ctx, commitIDStr)
if err != nil {
return nil, err
}
tag := &Tag{
Name: name,
ID: tagID,
Object: commitID,
Type: tp,
Tagger: commit.Committer,
CommitMessage: commit.CommitMessage,
}
repo.tagCache.Set(tagID.String(), tag)
return tag, nil
}
// The tag is an annotated tag with a message.
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return nil, err
}
defer cancel()
info, rd, err := batch.QueryContent(tagID.String())
if err != nil {
if errors.Is(err, io.EOF) || IsErrNotExist(err) {
return nil, ErrNotExist{ID: tagID.String()}
}
return nil, err
}
typ, size := info.Type, info.Size
if typ != "tag" {
if err := DiscardFull(rd, size+1); err != nil {
return nil, err
}
return nil, ErrNotExist{ID: tagID.String()}
}
// then we need to parse the tag and load the commit
limitReader, limitDiscard := limitDiscardReader(rd, info.Size, MaxGitObjectSize)
data, err := io.ReadAll(limitReader)
if err != nil {
return nil, err
}
if err = limitDiscard(); err != nil {
return nil, err
}
_, err = rd.Discard(1)
if err != nil {
return nil, err
}
tag, err := parseTagData(tagID.Type(), data)
if err != nil {
return nil, err
}
tag.Name = name
tag.ID = tagID
tag.Type = tp
repo.tagCache.Set(tagID.String(), tag)
return tag, nil
}
+85
View File
@@ -7,6 +7,7 @@ package git
import ( import (
"bytes" "bytes"
"context" "context"
"io"
"os" "os"
"strings" "strings"
"time" "time"
@@ -68,3 +69,87 @@ func (repo *Repository) CommitTree(ctx context.Context, author, committer *Signa
} }
return NewIDFromString(strings.TrimSpace(stdout)) return NewIDFromString(strings.TrimSpace(stdout))
} }
func (repo *Repository) getTree(ctx context.Context, id ObjectID) (*Tree, error) {
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return nil, err
}
defer cancel()
info, rd, err := batch.QueryContent(id.String())
if err != nil {
return nil, err
}
switch info.Type {
case "tag":
data, err := io.ReadAll(io.LimitReader(rd, info.Size))
if err != nil {
return nil, err
}
tag, err := parseTagData(id.Type(), data)
if err != nil {
return nil, err
}
commit, err := repo.getCommitWithBatch(batch, tag.Object)
if err != nil {
return nil, err
}
tree := commit.Tree()
return tree, nil
case "commit":
commit, err := CommitFromReader(id, io.LimitReader(rd, info.Size))
if err != nil {
return nil, err
}
if _, err := rd.Discard(1); err != nil {
return nil, err
}
tree := commit.Tree()
return tree, nil
case "tree":
tree := newTree(id)
objectFormat, err := repo.GetObjectFormat(ctx)
if err != nil {
return nil, err
}
tree.entries, err = catBatchParseTreeEntries(objectFormat, tree, rd, info.Size)
if err != nil {
return nil, err
}
tree.entriesParsed = true
return tree, nil
default:
if err := DiscardFull(rd, info.Size+1); err != nil {
return nil, err
}
return nil, ErrNotExist{
ID: id.String(),
}
}
}
// GetTree find the tree object in the repository.
func (repo *Repository) GetTree(ctx context.Context, idStr string) (*Tree, error) {
objectFormat, err := repo.GetObjectFormat(ctx)
if err != nil {
return nil, err
}
if len(idStr) != objectFormat.FullLength() {
res, err := repo.GetRefCommitID(ctx, idStr)
if err != nil {
return nil, err
}
if len(res) > 0 {
idStr = res
}
}
id, err := NewIDFromString(idStr)
if err != nil {
return nil, err
}
return repo.getTree(ctx, id)
}
-66
View File
@@ -1,66 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"context"
"errors"
"gitea.dev/modules/git/gitcmd"
"github.com/go-git/go-git/v5/plumbing"
)
func (repo *Repository) getTree(_ context.Context, id ObjectID) (*Tree, error) {
gogitTree, err := repo.gogitRepo.TreeObject(plumbing.Hash(id.RawValue()))
if err != nil {
if errors.Is(err, plumbing.ErrObjectNotFound) {
return nil, ErrNotExist{
ID: id.String(),
}
}
return nil, err
}
tree := newTree(id)
tree.resolvedGogitTreeObject = gogitTree
return tree, nil
}
// GetTree find the tree object in the repository.
func (repo *Repository) GetTree(ctx context.Context, idStr string) (*Tree, error) {
objectFormat, err := repo.GetObjectFormat(ctx)
if err != nil {
return nil, err
}
if len(idStr) != objectFormat.FullLength() {
res, _, err := gitcmd.NewCommand("rev-parse", "--verify").
AddDynamicArguments(idStr).
WithRepo(repo).
RunStdString(ctx)
if err != nil {
return nil, err
}
if len(res) > 0 {
idStr = res[:len(res)-1]
}
}
id, err := NewIDFromString(idStr)
if err != nil {
return nil, err
}
commitObject, err := repo.gogitRepo.CommitObject(plumbing.Hash(id.RawValue()))
if err == nil {
id = ParseGogitHash(commitObject.TreeHash)
}
treeObject, err := repo.getTree(ctx, id)
if err != nil {
return nil, err
}
return treeObject, nil
}
-95
View File
@@ -1,95 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"context"
"io"
)
func (repo *Repository) getTree(ctx context.Context, id ObjectID) (*Tree, error) {
batch, cancel, err := repo.CatFileBatch()
if err != nil {
return nil, err
}
defer cancel()
info, rd, err := batch.QueryContent(id.String())
if err != nil {
return nil, err
}
switch info.Type {
case "tag":
data, err := io.ReadAll(io.LimitReader(rd, info.Size))
if err != nil {
return nil, err
}
tag, err := parseTagData(id.Type(), data)
if err != nil {
return nil, err
}
commit, err := repo.getCommitWithBatch(batch, tag.Object)
if err != nil {
return nil, err
}
tree := commit.Tree()
return tree, nil
case "commit":
commit, err := CommitFromReader(id, io.LimitReader(rd, info.Size))
if err != nil {
return nil, err
}
if _, err := rd.Discard(1); err != nil {
return nil, err
}
tree := commit.Tree()
return tree, nil
case "tree":
tree := newTree(id)
objectFormat, err := repo.GetObjectFormat(ctx)
if err != nil {
return nil, err
}
tree.entries, err = catBatchParseTreeEntries(objectFormat, tree, rd, info.Size)
if err != nil {
return nil, err
}
tree.entriesParsed = true
return tree, nil
default:
if err := DiscardFull(rd, info.Size+1); err != nil {
return nil, err
}
return nil, ErrNotExist{
ID: id.String(),
}
}
}
// GetTree find the tree object in the repository.
func (repo *Repository) GetTree(ctx context.Context, idStr string) (*Tree, error) {
objectFormat, err := repo.GetObjectFormat(ctx)
if err != nil {
return nil, err
}
if len(idStr) != objectFormat.FullLength() {
res, err := repo.GetRefCommitID(ctx, idStr)
if err != nil {
return nil, err
}
if len(res) > 0 {
idStr = res
}
}
id, err := NewIDFromString(idStr)
if err != nil {
return nil, err
}
return repo.getTree(ctx, id)
}
+25
View File
@@ -5,11 +5,14 @@
package git package git
import ( import (
"fmt"
"io"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"gitea.dev/modules/log" "gitea.dev/modules/log"
"gitea.dev/modules/util"
) )
// Helper to get a signature from the commit line, which looks like: // Helper to get a signature from the commit line, which looks like:
@@ -47,3 +50,25 @@ func parseSignatureFromCommitLine(line string) *Signature {
} }
return sig return sig
} }
// Signature represents the Author, Committer or Tagger information.
type Signature struct {
Name string // the committer name, it can be anything
Email string // the committer email, it can be anything
When time.Time // the timestamp of the signature
}
func (s *Signature) String() string {
return fmt.Sprintf("%s <%s>", s.Name, s.Email)
}
// Encode writes the signature for git commit object
func (s *Signature) Encode(w io.Writer) error {
_, err := fmt.Fprintf(w, "%s <%s> %d %s", s.Name, s.Email, max(0, s.When.Unix()), s.When.Format("-0700"))
return err
}
// Decode parses the signature for git commit object
func (s *Signature) Decode(b []byte) {
*s = *parseSignatureFromCommitLine(util.UnsafeBytesToString(b))
}
-14
View File
@@ -1,14 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"github.com/go-git/go-git/v5/plumbing/object"
)
// Signature represents the Author or Committer information.
type Signature = object.Signature
-37
View File
@@ -1,37 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"fmt"
"io"
"time"
"gitea.dev/modules/util"
)
// Signature represents the Author, Committer or Tagger information.
type Signature struct {
Name string // the committer name, it can be anything
Email string // the committer email, it can be anything
When time.Time // the timestamp of the signature
}
func (s *Signature) String() string {
return fmt.Sprintf("%s <%s>", s.Name, s.Email)
}
// Encode writes the signature for git commit object (same as gogit's object.Signature Encode method)
func (s *Signature) Encode(w io.Writer) error {
_, err := fmt.Fprintf(w, "%s <%s> %d %s", s.Name, s.Email, max(0, s.When.Unix()), s.When.Format("-0700"))
return err
}
// Decode parses the signature for git commit object (same as gogit's object.Signature Decode method)
func (s *Signature) Decode(b []byte) {
*s = *parseSignatureFromCommitLine(util.UnsafeBytesToString(b))
}
+95
View File
@@ -7,6 +7,7 @@ package git
import ( import (
"bytes" "bytes"
"context" "context"
"io"
"strings" "strings"
"gitea.dev/modules/git/gitcmd" "gitea.dev/modules/git/gitcmd"
@@ -76,3 +77,97 @@ func (repo *Repository) GetTreePathLatestCommit(ctx context.Context, refName, tr
} }
return repo.GetCommit(ctx, strings.TrimSpace(stdout)) return repo.GetCommit(ctx, strings.TrimSpace(stdout))
} }
// Tree represents a flat directory listing.
type Tree struct {
TreeCommon
entries Entries
entriesParsed bool
}
// ListEntries returns all entries of current tree.
func (t *Tree) ListEntries(ctx context.Context, gitRepo *Repository) (Entries, error) {
if t.entriesParsed {
return t.entries, nil
}
batch, cancel, err := gitRepo.CatFileBatch()
if err != nil {
return nil, err
}
defer cancel()
info, rd, err := batch.QueryContent(t.ID.String())
if err != nil {
return nil, err
}
if info.Type == "commit" {
treeID, err := ReadTreeID(rd, info.Size)
if err != nil && err != io.EOF {
return nil, err
}
info, rd, err = batch.QueryContent(treeID)
if err != nil {
return nil, err
}
}
if info.Type == "tree" {
t.entries, err = catBatchParseTreeEntries(t.ID.Type(), t, rd, info.Size)
if err != nil {
return nil, err
}
t.entriesParsed = true
return t.entries, nil
}
// Not a tree just use ls-tree instead
if err := DiscardFull(rd, info.Size+1); err != nil {
return nil, err
}
stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-l").AddDynamicArguments(t.ID.String()).WithRepo(gitRepo).RunStdBytes(ctx)
if runErr != nil {
if gitcmd.IsStderr(runErr, gitcmd.StderrNotValidObjectName) || gitcmd.IsStderr(runErr, gitcmd.StderrNotTreeObject) {
return nil, ErrNotExist{
ID: t.ID.String(),
}
}
return nil, runErr
}
t.entries, err = parseTreeEntries(stdout, t)
if err == nil {
t.entriesParsed = true
}
return t.entries, err
}
// listEntriesRecursive returns all entries of current tree recursively including all subtrees
// extraArgs could be "-l" to get the size, which is slower
func (t *Tree) listEntriesRecursive(ctx context.Context, gitRepo *Repository, extraArgs gitcmd.TrustedCmdArgs) (Entries, error) {
stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-t", "-r").
AddArguments(extraArgs...).
AddDynamicArguments(t.ID.String()).
WithRepo(gitRepo).
RunStdBytes(ctx)
if runErr != nil {
return nil, runErr
}
// FIXME: the "name" field is abused, here it is a full path
// FIXME: this ptree is not right, fortunately it isn't really used
return parseTreeEntries(stdout, t)
}
// ListEntriesRecursiveFast returns all entries of current tree recursively including all subtrees, no size
func (t *Tree) ListEntriesRecursiveFast(ctx context.Context, gitRepo *Repository) (Entries, error) {
return t.listEntriesRecursive(ctx, gitRepo, nil)
}
// ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees, with size
func (t *Tree) ListEntriesRecursiveWithSize(ctx context.Context, gitRepo *Repository) (Entries, error) {
return t.listEntriesRecursive(ctx, gitRepo, gitcmd.TrustedCmdArgs{"--long"})
}
+40 -1
View File
@@ -4,7 +4,11 @@
package git package git
import "context" import (
"context"
"path"
"strings"
)
// GetBlobByPath get the blob object according the path // GetBlobByPath get the blob object according the path
func (t *Tree) GetBlobByPath(ctx context.Context, gitRepo *Repository, relpath string) (*Blob, error) { func (t *Tree) GetBlobByPath(ctx context.Context, gitRepo *Repository, relpath string) (*Blob, error) {
@@ -19,3 +23,38 @@ func (t *Tree) GetBlobByPath(ctx context.Context, gitRepo *Repository, relpath s
return nil, ErrNotExist{"", relpath} return nil, ErrNotExist{"", relpath}
} }
// GetTreeEntryByPath get the tree entries according the sub dir
func (t *Tree) GetTreeEntryByPath(ctx context.Context, gitRepo *Repository, relpath string) (_ *TreeEntry, err error) {
if len(relpath) == 0 {
return &TreeEntry{
ptree: t,
ID: t.ID,
name: "",
entryMode: EntryModeTree,
}, nil
}
relpath = path.Clean(relpath)
parts := strings.Split(relpath, "/")
tree := t
for _, name := range parts[:len(parts)-1] {
tree, err = tree.SubTree(ctx, gitRepo, name)
if err != nil {
return nil, err
}
}
name := parts[len(parts)-1]
entries, err := tree.ListEntries(ctx, gitRepo)
if err != nil {
return nil, err
}
for _, v := range entries {
if v.Name() == name {
return v, nil
}
}
return nil, ErrNotExist{"", relpath}
}
-61
View File
@@ -1,61 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"context"
"path"
"strings"
"github.com/go-git/go-git/v5/plumbing"
)
// GetTreeEntryByPath get the tree entries according the sub dir
func (t *Tree) GetTreeEntryByPath(ctx context.Context, gitRepo *Repository, relpath string) (*TreeEntry, error) {
if len(relpath) == 0 {
return &TreeEntry{
ID: t.ID,
ptree: t,
name: "",
entryMode: EntryModeTree,
}, nil
}
relpath = path.Clean(relpath)
parts := strings.Split(relpath, "/")
var err error
tree := t
for i, name := range parts {
if i == len(parts)-1 {
entries, err := tree.ListEntries(ctx, gitRepo)
if err != nil {
if err == plumbing.ErrObjectNotFound {
return nil, ErrNotExist{
RelPath: relpath,
}
}
return nil, err
}
for _, v := range entries {
if v.Name() == name {
return v, nil
}
}
} else {
tree, err = tree.SubTree(ctx, gitRepo, name)
if err != nil {
if err == plumbing.ErrObjectNotFound {
return nil, ErrNotExist{
RelPath: relpath,
}
}
return nil, err
}
}
}
return nil, ErrNotExist{"", relpath}
}
-47
View File
@@ -1,47 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"context"
"path"
"strings"
)
// GetTreeEntryByPath get the tree entries according the sub dir
func (t *Tree) GetTreeEntryByPath(ctx context.Context, gitRepo *Repository, relpath string) (_ *TreeEntry, err error) {
if len(relpath) == 0 {
return &TreeEntry{
ptree: t,
ID: t.ID,
name: "",
entryMode: EntryModeTree,
}, nil
}
relpath = path.Clean(relpath)
parts := strings.Split(relpath, "/")
tree := t
for _, name := range parts[:len(parts)-1] {
tree, err = tree.SubTree(ctx, gitRepo, name)
if err != nil {
return nil, err
}
}
name := parts[len(parts)-1]
entries, err := tree.ListEntries(ctx, gitRepo)
if err != nil {
return nil, err
}
for _, v := range entries {
if v.Name() == name {
return v, nil
}
}
return nil, ErrNotExist{"", relpath}
}

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