1
0
Fork 0
mirror of https://codeberg.org/forgejo/forgejo.git synced 2025-01-20 16:50:28 -05:00

Compare commits

...

30 commits

Author SHA1 Message Date
michael-sparrow
359c5a5259 Merge branch 'forgejo' into pull-through-cache-proxy 2025-01-20 10:56:21 +00:00
Otto
243fdb60d0 enh(ui): Remove DiffFileList component (#6619)
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6619
Reviewed-by: Otto <otto@codeberg.org>
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
2025-01-19 19:18:49 +00:00
Beowulf
86c8949d9c
Remove DiffFileList component
The benefit / functionality provided by DiffFileList is already (better)
integrated in the header of the files.
If you want an overview, you can collapse all files via the same
overflow menu (where the stats were available).
To reduce the maintenance effort, the DiffFileList component is
therefore removed.
2025-01-19 18:56:18 +01:00
Otto
ce4730f9d7 fix(ui): add triangle down octicon to code search options dropdown (#6620)
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6620
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Reviewed-by: Otto <otto@codeberg.org>
2025-01-19 15:06:15 +00:00
Beowulf
5f2d445d00
fix(ui): add triangle down octicon to code search options dropdown
This adds the triangle down oction to the code search options dropdown
to match the other search option dropdowns (issue, pull).
2025-01-19 01:48:37 +01:00
Beowulf
d68e0d3e39 fix(ui): hide git note add button for commit if commit already has a note (#6613)
Regression from f5c0570533

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6613
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
Co-authored-by: Beowulf <beowulf@beocode.eu>
Co-committed-by: Beowulf <beowulf@beocode.eu>
2025-01-18 19:39:42 +00:00
Renovate Bot
e35afe475a Update renovate Docker tag to v39.115.4 (forgejo) (#6606)
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6606
Co-authored-by: Renovate Bot <forgejo-renovate-action@forgejo.org>
Co-committed-by: Renovate Bot <forgejo-renovate-action@forgejo.org>
2025-01-18 14:56:11 +00:00
Michael Kriese
d2bb86407f chore(renovate): fix self-update config [skip ci] (#6610)
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6610
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
2025-01-18 14:54:06 +00:00
Michael Kriese
f2e63495e7
chore(renovate): fix self-update config [skip-ci] 2025-01-18 15:19:53 +01:00
0ko
244ee64c30 fix(i18n): flatten next locales (#6607)
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6607
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
2025-01-18 07:35:28 +00:00
Renovate Bot
f1a92de4e6 Update postcss (forgejo) (#6562)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [postcss](https://postcss.org/) ([source](https://github.com/postcss/postcss)) | dependencies | minor | [`8.4.49` -> `8.5.1`](https://renovatebot.com/diffs/npm/postcss/8.4.49/8.5.1) |
| [postcss-html](https://github.com/ota-meshi/postcss-html) | devDependencies | minor | [`1.7.0` -> `1.8.0`](https://renovatebot.com/diffs/npm/postcss-html/1.7.0/1.8.0) |

---

### Release Notes

<details>
<summary>postcss/postcss (postcss)</summary>

### [`v8.5.1`](https://github.com/postcss/postcss/blob/HEAD/CHANGELOG.md#851)

[Compare Source](https://github.com/postcss/postcss/compare/8.5.0...8.5.1)

-   Fixed backwards compatibility for complex cases (by [@&#8203;romainmenke](https://github.com/romainmenke)).

### [`v8.5.0`](https://github.com/postcss/postcss/releases/tag/8.5.0): 8.5 “Duke Alloces”

[Compare Source](https://github.com/postcss/postcss/compare/8.4.49...8.5.0)

<img src="https://github.com/user-attachments/assets/6ef654a0-d675-4ba0-a670-e28ef27062f5" align="right" width="200" height="200" alt="President Alloces seal">

PostCSS 8.5 brought API to work better with non-CSS sources like HTML, Vue.js/Svelte sources or CSS-in-JS.

[@&#8203;romainmenke](https://github.com/romainmenke) during [his work](https://github.com/postcss/postcss/issues/1995) on [Stylelint](https://stylelint.io) added `Input#document` in additional to `Input#css`.

```js
root.source.input.document //=> "<p>Hello</p>
                           //    <style>
                           //    p {
                           //      color: green;
                           //    }
                           //    </style>"
root.source.input.css      //=> "p {
                           //      color: green;
                           //    }"

```

#### Thanks to Sponsors

This release was possible thanks to our community.

If your company wants to support the sustainability of front-end infrastructure or wants to give some love to PostCSS, you can join our supporters by:

-   [**Tidelift**](https://tidelift.com/) with a Spotify-like subscription model supporting all projects from your lock file.
-   Direct donations at [**GitHub Sponsors**](https://github.com/sponsors/ai) or [**Open Collective**](https://opencollective.com/postcss#section-contributors).

</details>

<details>
<summary>ota-meshi/postcss-html (postcss-html)</summary>

### [`v1.8.0`](https://github.com/ota-meshi/postcss-html/releases/tag/v1.8.0)

[Compare Source](https://github.com/ota-meshi/postcss-html/compare/v1.7.0...v1.8.0)

#### What's Changed

-   update to latest PostCSS by [@&#8203;romainmenke](https://github.com/romainmenke) in https://github.com/ota-meshi/postcss-html/pull/134

**Full Changelog**: https://github.com/ota-meshi/postcss-html/compare/v1.7.0...v1.8.0

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "* 0-3 * * *" (UTC), Automerge - "* 0-3 * * *" (UTC).

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

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

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://github.com/renovatebot/renovate/discussions) if that's undesired.

---

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

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDYuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNi4wIiwidGFyZ2V0QnJhbmNoIjoiZm9yZ2VqbyIsImxhYmVscyI6WyJkZXBlbmRlbmN5LXVwZ3JhZGUiLCJ0ZXN0L25vdC1uZWVkZWQiXX0=-->

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6562
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Renovate Bot <forgejo-renovate-action@forgejo.org>
Co-committed-by: Renovate Bot <forgejo-renovate-action@forgejo.org>
2025-01-18 06:39:17 +00:00
0ko
4c746ec653 Initial support for localization and pluralization with go-i18n-JSON-v2 format (#6203)
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6203
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
2025-01-18 05:51:19 +00:00
Renovate Bot
401906b88e Update module google.golang.org/protobuf to v1.36.3 (forgejo) (#6581)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [google.golang.org/protobuf](https://github.com/protocolbuffers/protobuf-go) | require | patch | `v1.36.2` -> `v1.36.3` |

---

### Release Notes

<details>
<summary>protocolbuffers/protobuf-go (google.golang.org/protobuf)</summary>

### [`v1.36.3`](https://github.com/protocolbuffers/protobuf-go/releases/tag/v1.36.3)

[Compare Source](https://github.com/protocolbuffers/protobuf-go/compare/v1.36.2...v1.36.3)

**Full Changelog**: https://github.com/protocolbuffers/protobuf-go/compare/v1.36.2...v1.36.3

Bug fixes:
[CL/642575](https://go-review.googlesource.com/c/protobuf/+/642575): reflect/protodesc: fix panic when working with dynamicpb
[CL/641036](https://go-review.googlesource.com/c/protobuf/+/641036): cmd/protoc-gen-go: remove json struct tags from unexported fields

User-visible changes:
[CL/641876](https://go-review.googlesource.com/c/protobuf/+/641876): proto: add example for GetExtension, SetExtension
[CL/642015](https://go-review.googlesource.com/c/protobuf/+/642015): runtime/protolazy: replace internal doc link with external link

Maintenance:
[CL/641635](https://go-review.googlesource.com/c/protobuf/+/641635): all: split flags.ProtoLegacyWeak out of flags.ProtoLegacy
[CL/641019](https://go-review.googlesource.com/c/protobuf/+/641019): internal/impl: remove unused exporter parameter
[CL/641018](https://go-review.googlesource.com/c/protobuf/+/641018): internal/impl: switch to reflect.Value.IsZero
[CL/641035](https://go-review.googlesource.com/c/protobuf/+/641035): internal/impl: clean up unneeded Go<1.12 MapRange() alternative
[CL/641017](https://go-review.googlesource.com/c/protobuf/+/641017): types/dynamicpb: switch atomicExtFiles to atomic.Uint64 type

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "* 0-3 * * *" (UTC), Automerge - "* 0-3 * * *" (UTC).

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

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

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

---

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

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDYuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNi4wIiwidGFyZ2V0QnJhbmNoIjoiZm9yZ2VqbyIsImxhYmVscyI6WyJkZXBlbmRlbmN5LXVwZ3JhZGUiLCJ0ZXN0L25vdC1uZWVkZWQiXX0=-->

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6581
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Renovate Bot <forgejo-renovate-action@forgejo.org>
Co-committed-by: Renovate Bot <forgejo-renovate-action@forgejo.org>
2025-01-18 05:33:34 +00:00
Renovate Bot
dbec2ed350 Update module github.com/caddyserver/certmagic to v0.21.7 (forgejo) (#6604)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [github.com/caddyserver/certmagic](https://github.com/caddyserver/certmagic) | require | patch | `v0.21.6` -> `v0.21.7` |

---

### Release Notes

<details>
<summary>caddyserver/certmagic (github.com/caddyserver/certmagic)</summary>

### [`v0.21.7`](https://github.com/caddyserver/certmagic/compare/v0.21.6...v0.21.7)

[Compare Source](https://github.com/caddyserver/certmagic/compare/v0.21.6...v0.21.7)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "* 0-3 * * *" (UTC), Automerge - "* 0-3 * * *" (UTC).

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

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

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

---

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

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDYuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNi4wIiwidGFyZ2V0QnJhbmNoIjoiZm9yZ2VqbyIsImxhYmVscyI6WyJkZXBlbmRlbmN5LXVwZ3JhZGUiLCJ0ZXN0L25vdC1uZWVkZWQiXX0=-->

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6604
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Renovate Bot <forgejo-renovate-action@forgejo.org>
Co-committed-by: Renovate Bot <forgejo-renovate-action@forgejo.org>
2025-01-18 05:31:17 +00:00
Renovate Bot
5c8db43447 Update dependency katex to v0.16.21 (forgejo) (#6603)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [katex](https://katex.org) ([source](https://github.com/KaTeX/KaTeX)) | dependencies | patch | [`0.16.20` -> `0.16.21`](https://renovatebot.com/diffs/npm/katex/0.16.20/0.16.21) |

---

### Release Notes

<details>
<summary>KaTeX/KaTeX (katex)</summary>

### [`v0.16.21`](https://github.com/KaTeX/KaTeX/blob/HEAD/CHANGELOG.md#01621-2025-01-17)

[Compare Source](https://github.com/KaTeX/KaTeX/compare/v0.16.20...v0.16.21)

##### Bug Fixes

-   escape \htmlData attribute name ([57914ad](57914ad91e))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "* 0-3 * * *" (UTC), Automerge - "* 0-3 * * *" (UTC).

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

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

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

---

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

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDYuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNi4wIiwidGFyZ2V0QnJhbmNoIjoiZm9yZ2VqbyIsImxhYmVscyI6WyJkZXBlbmRlbmN5LXVwZ3JhZGUiLCJ0ZXN0L25vdC1uZWVkZWQiXX0=-->

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6603
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Renovate Bot <forgejo-renovate-action@forgejo.org>
Co-committed-by: Renovate Bot <forgejo-renovate-action@forgejo.org>
2025-01-18 05:30:16 +00:00
Beowulf
34e1100ae2 fix(ui): reset content of text field for comments when cancelling (#6595)
Currently, the content of the text field is not reset when you cancel editing. This change resets the content of the text field when editing is canceled.

If this is not done and you click on cancel and then on edit again, you can no longer return to the initial content without completely reloading the page.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6595
Reviewed-by: Otto <otto@codeberg.org>
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Beowulf <beowulf@beocode.eu>
Co-committed-by: Beowulf <beowulf@beocode.eu>
2025-01-17 20:14:28 +00:00
Benedikt Straub
a2787bb09e
Initial support for localization and pluralization with go-i18n-JSON-v2 format 2025-01-17 11:21:28 +01:00
Earl Warren
376a2e19ea fix: reduce noise for the v303 migration (#6591)
Using SELECT `%s` FROM `%s` WHERE 0 = 1 to assert the existence of a column is simple but noisy: it shows errors in the migrations that are confusing for Forgejo admins because they are not actual errors.

Use introspection instead, which is more complicated but leads to the same result.

Add a test that ensures it works as expected, for all database types. Although the migration is run for all database types, it does not account for various scenarios and is never tested in the case a column does not exist.

Refs: https://codeberg.org/forgejo/forgejo/issues/6583

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6591
Reviewed-by: Otto <otto@codeberg.org>
Co-authored-by: Earl Warren <contact@earl-warren.org>
Co-committed-by: Earl Warren <contact@earl-warren.org>
2025-01-17 07:42:20 +00:00
Gusted
b2a3a0411c [PORT] Remove SHA1 for support for ssh rsa signing (#31857) (#5303)
https://github.com/go-fed/httpsig seems to be unmaintained.

Switch to github.com/42wim/httpsig which has removed deprecated crypto
and default sha256 signing for ssh rsa.

No impact for those that use ed25519 ssh certificates.

This is a breaking change for:
- gitea.com/gitea/tea (go-sdk) - I'll be sending a PR there too
- activitypub using deprecated crypto (is this actually used?)

(cherry picked from commit 01dec7577a051d9bb30e91f6cf6653dc51a37d06)

---
Conflict resolution: trivial

Co-authored-by: Wim <wim@42.be>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5303
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
2025-01-17 03:17:10 +00:00
Earl Warren
387f590d9c [gitea] week 2025-03 cherry pick (gitea/main -> forgejo) (#6539)
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6539
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
2025-01-17 01:58:00 +00:00
Robert Wolff
cefd786685 Remove source branch from pr list, fix #5009, #6080 (#6522)
### What?
It removes the source branch that is not necessary in the PR list (see #5009). It adds a little chevron to the right in front of the target branch. That could be replaced by words (“into”), or removed, if preferred. But, I think it looks decent like that.
### Screenshots
#### Before
![image](/attachments/be3c9817-2207-4610-b6bd-70304436f81d)
#### After
![image](/attachments/a7c84d2f-1592-4a82-aecc-d038f9495ef7)
### Testing
Run the development version of forgejo from the PR. For any existing repository with PRs, open the pulls list.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6522
Reviewed-by: Otto <otto@codeberg.org>
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Robert Wolff <mahlzahn@posteo.de>
Co-committed-by: Robert Wolff <mahlzahn@posteo.de>
2025-01-16 23:59:21 +00:00
Renovate Bot
907ab8bdef Update dependency @github/relative-time-element to v4.4.5 (forgejo) (#6559)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [@github/relative-time-element](https://github.com/github/relative-time-element) | dependencies | patch | [`4.4.4` -> `4.4.5`](https://renovatebot.com/diffs/npm/@github%2frelative-time-element/4.4.4/4.4.5) |

---

### Release Notes

<details>
<summary>github/relative-time-element (@&#8203;github/relative-time-element)</summary>

### [`v4.4.5`](https://github.com/github/relative-time-element/releases/tag/v4.4.5)

[Compare Source](https://github.com/github/relative-time-element/compare/v4.4.4...v4.4.5)

#### What's Changed

-   fix: wrap Intl.<>() calls in try/catch by [@&#8203;francinelucca](https://github.com/francinelucca) in https://github.com/github/relative-time-element/pull/297
-   get main branch green by [@&#8203;keithamus](https://github.com/keithamus) in https://github.com/github/relative-time-element/pull/302
-   Make `applyDuration` reversible by [@&#8203;leduyquang753](https://github.com/leduyquang753) in https://github.com/github/relative-time-element/pull/298
-   Use node v22 by [@&#8203;camertron](https://github.com/camertron) in https://github.com/github/relative-time-element/pull/303

#### New Contributors

-   [@&#8203;francinelucca](https://github.com/francinelucca) made their first contribution in https://github.com/github/relative-time-element/pull/297
-   [@&#8203;leduyquang753](https://github.com/leduyquang753) made their first contribution in https://github.com/github/relative-time-element/pull/298
-   [@&#8203;camertron](https://github.com/camertron) made their first contribution in https://github.com/github/relative-time-element/pull/303

**Full Changelog**: https://github.com/github/relative-time-element/compare/v4.4.4...v4.4.5

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "* 0-3 * * *" (UTC), Automerge - "* 0-3 * * *" (UTC).

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

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

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

---

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

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDYuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNi4wIiwidGFyZ2V0QnJhbmNoIjoiZm9yZ2VqbyIsImxhYmVscyI6WyJkZXBlbmRlbmN5LXVwZ3JhZGUiLCJ0ZXN0L25vdC1uZWVkZWQiXX0=-->

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6559
Reviewed-by: Otto <otto@codeberg.org>
Co-authored-by: Renovate Bot <forgejo-renovate-action@forgejo.org>
Co-committed-by: Renovate Bot <forgejo-renovate-action@forgejo.org>
2025-01-16 22:51:49 +00:00
0ko
4fd56a11c8 tests(e2e): Various fixes to visual testing (#6569)
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6569
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
2025-01-16 18:06:47 +00:00
Renovate Bot
909738e6f6 Update renovate Docker tag to v39.111.0 (forgejo) (#6570)
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6570
Reviewed-by: Michael Kriese <michael.kriese@gmx.de>
Co-authored-by: Renovate Bot <forgejo-renovate-action@forgejo.org>
Co-committed-by: Renovate Bot <forgejo-renovate-action@forgejo.org>
2025-01-16 15:20:57 +00:00
Otto Richter
e7299eb0fb tests(e2e): Mask user heatmap in screenshots
It depends on both relative activity and date the test is run on
2025-01-15 19:16:28 +01:00
Otto Richter
f7ba8f0e41 tests(e2e): Do not fail early when comparing screenshots
Also set a unique outputDir so that artifacts are preserved and not overwritten by the following tests.
2025-01-15 19:16:28 +01:00
Otto Richter
a975b6ab94 tests(e2e): Explicitly generate screenshots
As per https://codeberg.org/forgejo/forgejo/pulls/6400, the after hook runs for every test, resulting in duplicated screenshots.

Not all tests are supposed to generate screenshots, especially because they could be flaky (also see 206d4cfb7a ).
Additionally, the implicit behaviour might have caused confusion, so we now create screenshots explicitly, adding the statements from the tests that already generated screenshots.
2025-01-15 15:09:24 +01:00
Lunny Xiao
f2feb34927
chore(performance): loadCommentsByType sets Issues
Keep the setting of comment.Issues from the refactor. It is cheap and
potentially saves loading the issue again.

Former title: Some small refactors (#33144)

(cherry picked from commit d3083d21981f9445cf7570956a1fdedfc8578b56)

Conflicts:
	models/issues/comment_list.go
	models/issues/issue_list.go
	routers/web/repo/issue_view.go
2025-01-12 17:52:38 +01:00
Lunny Xiao
d09b8ba9cf
Fix fuzz test (#33156)
(cherry picked from commit ba5e3a5161a497aa8c73827774870fb94676e68b)
2025-01-12 09:02:47 +01:00
yp05327
f809052193
Support the new exit code for git remote subcommands for git version >=2.30.0 (#33129)
Fix #32889

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
(cherry picked from commit 0d7d2ed39d0c0435cdc6403ee7764850154dca5a)

Conflicts:
	modules/git/remote.go
  trivial context conflict
2025-01-12 08:52:51 +01:00
104 changed files with 1343 additions and 255 deletions

View file

@ -246,6 +246,7 @@ code.gitea.io/gitea/modules/translation
MockLocale.TrString
MockLocale.Tr
MockLocale.TrN
MockLocale.TrPluralString
MockLocale.TrSize
MockLocale.PrettyNumber

View file

@ -49,7 +49,7 @@ GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 # renovate: datasour
DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.29.0 # renovate: datasource=go
GOMOCK_PACKAGE ?= go.uber.org/mock/mockgen@v0.4.0 # renovate: datasource=go
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.17.1 # renovate: datasource=go
RENOVATE_NPM_PACKAGE ?= renovate@39.106.0 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate
RENOVATE_NPM_PACKAGE ?= renovate@39.115.4 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate
# https://github.com/disposable-email-domains/disposable-email-domains/commits/main/
DISPOSABLE_EMAILS_SHA ?= 0c27e671231d27cf66370034d7f6818037416989 # renovate: ...

10
go.mod
View file

@ -17,6 +17,7 @@ require (
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
connectrpc.com/connect v1.17.0
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
github.com/42wim/httpsig v1.2.2
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
github.com/ProtonMail/go-crypto v1.1.4
@ -26,7 +27,7 @@ require (
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
github.com/blevesearch/bleve/v2 v2.4.4
github.com/buildkite/terminal-to-html/v3 v3.16.4
github.com/caddyserver/certmagic v0.21.6
github.com/caddyserver/certmagic v0.21.7
github.com/chi-middleware/proxy v1.1.1
github.com/djherbis/buffer v1.2.0
github.com/djherbis/nio/v3 v3.0.1
@ -43,7 +44,6 @@ require (
github.com/go-chi/cors v1.2.1
github.com/go-co-op/gocron v1.37.0
github.com/go-enry/go-enry/v2 v2.9.1
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e
github.com/go-git/go-git/v5 v5.13.1
github.com/go-ldap/ldap/v3 v3.4.6
github.com/go-openapi/spec v0.20.14
@ -109,7 +109,7 @@ require (
golang.org/x/sys v0.29.0
golang.org/x/text v0.21.0
google.golang.org/grpc v1.69.2
google.golang.org/protobuf v1.36.2
google.golang.org/protobuf v1.36.3
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/ini.v1 v1.67.0
gopkg.in/yaml.v3 v3.0.1
@ -131,7 +131,6 @@ require (
dario.cat/mergo v1.0.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
github.com/42wim/httpsig v1.2.2 // indirect
github.com/DataDog/zstd v1.5.5 // indirect
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.2 // indirect
@ -184,6 +183,7 @@ require (
github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
github.com/go-enry/go-oniguruma v1.2.1 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.1 // indirect
github.com/go-ini/ini v1.67.0 // indirect
@ -221,7 +221,7 @@ require (
github.com/markbates/going v1.0.3 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mholt/acmez/v3 v3.0.0 // indirect
github.com/mholt/acmez/v3 v3.0.1 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect

16
go.sum
View file

@ -770,8 +770,8 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/buildkite/terminal-to-html/v3 v3.16.4 h1:QFYO8IGvRnp7tGgiQb8g9uFU8kY9wOzxsFFx17+yy6Q=
github.com/buildkite/terminal-to-html/v3 v3.16.4/go.mod h1:r/J7cC9c3EzBzP3/wDz0RJLPwv5PUAMp+KF2w+ntMc0=
github.com/caddyserver/certmagic v0.21.6 h1:1th6GfprVfsAtFNOu4StNMF5IxK5XiaI0yZhAHlZFPE=
github.com/caddyserver/certmagic v0.21.6/go.mod h1:n1sCo7zV1Ez2j+89wrzDxo4N/T1Ws/Vx8u5NvuBFabw=
github.com/caddyserver/certmagic v0.21.7 h1:66KJioPFJwttL43KYSWk7ErSmE6LfaJgCQuhm8Sg6fg=
github.com/caddyserver/certmagic v0.21.7/go.mod h1:LCPG3WLxcnjVKl/xpjzM0gqh0knrKKKiO5WVttX2eEI=
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@ -919,8 +919,8 @@ github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e h1:oRq/fiirun5HqlEWMLIcDmLpIELlG4iGbd0s8iqgPi8=
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
@ -1253,8 +1253,8 @@ github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBW
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/meilisearch/meilisearch-go v0.29.0 h1:HZ9NEKN59USINQ/DXJge/aaXq8IrsKbXGTdAoBaaDz4=
github.com/meilisearch/meilisearch-go v0.29.0/go.mod h1:2cRCAn4ddySUsFfNDLVPod/plRibQsJkXF/4gLhxbOk=
github.com/mholt/acmez/v3 v3.0.0 h1:r1NcjuWR0VaKP2BTjDK9LRFBw/WvURx3jlaEUl9Ht8E=
github.com/mholt/acmez/v3 v3.0.0/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/mholt/acmez/v3 v3.0.1 h1:4PcjKjaySlgXK857aTfDuRbmnM5gb3Ruz3tvoSJAUp8=
github.com/mholt/acmez/v3 v3.0.1/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
@ -2179,8 +2179,8 @@ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View file

@ -268,6 +268,9 @@ func (issue *Issue) loadCommentsByType(ctx context.Context, tp CommentType) (err
IssueID: issue.ID,
Type: tp,
})
for _, comment := range issue.Comments {
comment.Issue = issue
}
return err
}

View file

@ -1,23 +1,27 @@
// Copyright 2024 The Forgejo Authors.
// SPDX-License-Identifier: MIT
// Copyright 2025 The Forgejo Authors.
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_23 //nolint
import (
"fmt"
"code.gitea.io/gitea/models/migrations/base"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
)
func GiteaLastDrop(x *xorm.Engine) error {
tables, err := x.DBMetas()
if err != nil {
return err
}
sess := x.NewSession()
defer sess.Close()
for _, drop := range []struct {
table string
field string
table string
column string
}{
{"badge", "slug"},
{"oauth2_application", "skip_secondary_authorization"},
@ -29,10 +33,25 @@ func GiteaLastDrop(x *xorm.Engine) error {
{"protected_branch", "force_push_allowlist_team_i_ds"},
{"protected_branch", "force_push_allowlist_deploy_keys"},
} {
if _, err := sess.Exec(fmt.Sprintf("SELECT `%s` FROM `%s` WHERE 0 = 1", drop.field, drop.table)); err != nil {
var table *schemas.Table
found := false
for _, table = range tables {
if table.Name == drop.table {
found = true
break
}
}
if !found {
continue
}
if err := base.DropTableColumns(sess, drop.table, drop.field); err != nil {
if table.GetColumn(drop.column) == nil {
continue
}
if err := base.DropTableColumns(sess, drop.table, drop.column); err != nil {
return err
}
}

View file

@ -0,0 +1,41 @@
// Copyright 2025 The Forgejo Authors.
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_23 //nolint
import (
"testing"
migration_tests "code.gitea.io/gitea/models/migrations/test"
"github.com/stretchr/testify/require"
"xorm.io/xorm/schemas"
)
func Test_GiteaLastDrop(t *testing.T) {
type Badge struct {
ID int64 `xorm:"pk autoincr"`
Slug string
}
x, deferable := migration_tests.PrepareTestEnv(t, 0, new(Badge))
defer deferable()
if x == nil || t.Failed() {
return
}
getColumn := func() *schemas.Column {
tables, err := x.DBMetas()
require.NoError(t, err)
require.Len(t, tables, 1)
table := tables[0]
require.Equal(t, "badge", table.Name)
return table.GetColumn("slug")
}
require.NotNil(t, getColumn(), "slug column exists")
require.NoError(t, GiteaLastDrop(x))
require.Nil(t, getColumn(), "slug column was deleted")
// idempotent
require.NoError(t, GiteaLastDrop(x))
}

View file

@ -22,7 +22,7 @@ import (
"code.gitea.io/gitea/modules/proxy"
"code.gitea.io/gitea/modules/setting"
"github.com/go-fed/httpsig"
"github.com/42wim/httpsig"
)
const (

View file

@ -5,6 +5,7 @@ package git
import (
"context"
"strings"
giturl "code.gitea.io/gitea/modules/git/url"
)
@ -37,3 +38,12 @@ func GetRemoteURL(ctx context.Context, repoPath, remoteName string) (*giturl.Git
}
return giturl.Parse(addr)
}
// IsRemoteNotExistError checks the prefix of the error message to see whether a remote does not exist.
func IsRemoteNotExistError(err error) bool {
// see: https://github.com/go-gitea/gitea/issues/32889#issuecomment-2571848216
// Should not add space in the end, sometimes git will add a `:`
prefix1 := "exit status 128 - fatal: No such remote" // git < 2.30
prefix2 := "exit status 2 - error: No such remote" // git >= 2.30
return strings.HasPrefix(err.Error(), prefix1) || strings.HasPrefix(err.Error(), prefix2)
}

View file

@ -6,7 +6,7 @@ package setting
import (
"code.gitea.io/gitea/modules/log"
"github.com/go-fed/httpsig"
"github.com/42wim/httpsig"
)
// Federation settings

View file

@ -22,20 +22,7 @@ func (k *KeyLocale) HasKey(trKey string) bool {
// TrHTML implements Locale.
func (k *KeyLocale) TrHTML(trKey string, trArgs ...any) template.HTML {
args := slices.Clone(trArgs)
for i, v := range args {
switch v := v.(type) {
case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML:
// for most basic types (including template.HTML which is safe), just do nothing and use it
case string:
args[i] = template.HTMLEscapeString(v)
case fmt.Stringer:
args[i] = template.HTMLEscapeString(v.String())
default:
args[i] = template.HTMLEscapeString(fmt.Sprint(v))
}
}
return template.HTML(k.TrString(trKey, args...))
return template.HTML(k.TrString(trKey, PrepareArgsForHTML(trArgs...)...))
}
// TrString implements Locale.
@ -43,6 +30,11 @@ func (k *KeyLocale) TrString(trKey string, trArgs ...any) string {
return FormatDummy(trKey, trArgs...)
}
// TrPluralString implements Locale.
func (k *KeyLocale) TrPluralString(count any, trKey string, trArgs ...any) template.HTML {
return template.HTML(FormatDummy(trKey, PrepareArgsForHTML(trArgs...)...))
}
func FormatDummy(trKey string, args ...any) string {
if len(args) == 0 {
return fmt.Sprintf("(%s)", trKey)

View file

@ -8,6 +8,8 @@ import (
)
var (
ErrLocaleAlreadyExist = util.SilentWrap{Message: "lang already exists", Err: util.ErrAlreadyExist}
ErrUncertainArguments = util.SilentWrap{Message: "arguments to i18n should not contain uncertain slices", Err: util.ErrInvalidArgument}
ErrLocaleAlreadyExist = util.SilentWrap{Message: "lang already exists", Err: util.ErrAlreadyExist}
ErrLocaleDoesNotExist = util.SilentWrap{Message: "lang does not exist", Err: util.ErrNotExist}
ErrTranslationDoesNotExist = util.SilentWrap{Message: "translation does not exist", Err: util.ErrNotExist}
ErrUncertainArguments = util.SilentWrap{Message: "arguments to i18n should not contain uncertain slices", Err: util.ErrInvalidArgument}
)

View file

@ -8,11 +8,28 @@ import (
"io"
)
type (
PluralFormIndex uint8
PluralFormRule func(int64) PluralFormIndex
)
const (
PluralFormZero PluralFormIndex = iota
PluralFormOne
PluralFormTwo
PluralFormFew
PluralFormMany
PluralFormOther
)
var DefaultLocales = NewLocaleStore()
type Locale interface {
// TrString translates a given key and arguments for a language
TrString(trKey string, trArgs ...any) string
// TrPluralString translates a given pluralized key and arguments for a language.
// This function returns an error if new-style support for the given key is not available.
TrPluralString(count any, trKey string, trArgs ...any) template.HTML
// TrHTML translates a given key and arguments for a language, string arguments are escaped to HTML
TrHTML(trKey string, trArgs ...any) template.HTML
// HasKey reports if a locale has a translation for a given key
@ -31,8 +48,10 @@ type LocaleStore interface {
Locale(langName string) (Locale, bool)
// HasLang returns whether a given language is present in the store
HasLang(langName string) bool
// AddLocaleByIni adds a new language to the store
AddLocaleByIni(langName, langDesc string, source, moreSource []byte) error
// AddLocaleByIni adds a new old-style language to the store
AddLocaleByIni(langName, langDesc string, pluralRule PluralFormRule, source, moreSource []byte) error
// AddLocaleByJSON adds new-style content to an existing language to the store
AddToLocaleFromJSON(langName string, source []byte) error
}
// ResetDefaultLocales resets the current default locales

View file

@ -12,6 +12,26 @@ import (
"github.com/stretchr/testify/require"
)
var MockPluralRule PluralFormRule = func(n int64) PluralFormIndex {
if n == 0 {
return PluralFormZero
}
if n == 1 {
return PluralFormOne
}
if n >= 2 && n <= 4 {
return PluralFormFew
}
return PluralFormOther
}
var MockPluralRuleEnglish PluralFormRule = func(n int64) PluralFormIndex {
if n == 1 {
return PluralFormOne
}
return PluralFormOther
}
func TestLocaleStore(t *testing.T) {
testData1 := []byte(`
.dot.name = Dot Name
@ -27,11 +47,48 @@ fmt = %[2]s %[1]s
[section]
sub = Changed Sub String
commits = fallback value for commits
`)
testDataJSON2 := []byte(`
{
"section.json": "the JSON is %s",
"section.commits": {
"one": "one %d commit",
"few": "some %d commits",
"other": "lots of %d commits"
},
"section.incomplete": {
"few": "some %d objects (translated)"
},
"nested": {
"outer": {
"inner": {
"json": "Hello World",
"issue": {
"one": "one %d issue",
"few": "some %d issues",
"other": "lots of %d issues"
}
}
}
}
}
`)
testDataJSON1 := []byte(`
{
"section.incomplete": {
"one": "[untranslated] some %d object",
"other": "[untranslated] some %d objects"
}
}
`)
ls := NewLocaleStore()
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1, nil))
require.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", testData2, nil))
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRuleEnglish, testData1, nil))
require.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", MockPluralRule, testData2, nil))
require.NoError(t, ls.AddToLocaleFromJSON("lang1", testDataJSON1))
require.NoError(t, ls.AddToLocaleFromJSON("lang2", testDataJSON2))
ls.SetDefaultLang("lang1")
lang1, _ := ls.Locale("lang1")
@ -56,6 +113,45 @@ sub = Changed Sub String
result2 := lang2.TrHTML("section.mixed", "a&b")
assert.EqualValues(t, `test value; <span style="color: red; background: none;">a&amp;b</span>`, result2)
result = lang2.TrString("section.json", "valid")
assert.Equal(t, "the JSON is valid", result)
result = lang2.TrString("nested.outer.inner.json")
assert.Equal(t, "Hello World", result)
result = lang2.TrString("section.commits")
assert.Equal(t, "lots of %d commits", result)
result2 = lang2.TrPluralString(1, "section.commits", 1)
assert.EqualValues(t, "one 1 commit", result2)
result2 = lang2.TrPluralString(3, "section.commits", 3)
assert.EqualValues(t, "some 3 commits", result2)
result2 = lang2.TrPluralString(8, "section.commits", 8)
assert.EqualValues(t, "lots of 8 commits", result2)
result2 = lang2.TrPluralString(0, "section.commits")
assert.EqualValues(t, "section.commits", result2)
result2 = lang2.TrPluralString(1, "nested.outer.inner.issue", 1)
assert.EqualValues(t, "one 1 issue", result2)
result2 = lang2.TrPluralString(3, "nested.outer.inner.issue", 3)
assert.EqualValues(t, "some 3 issues", result2)
result2 = lang2.TrPluralString(9, "nested.outer.inner.issue", 9)
assert.EqualValues(t, "lots of 9 issues", result2)
result2 = lang2.TrPluralString(3, "section.incomplete", 3)
assert.EqualValues(t, "some 3 objects (translated)", result2)
result2 = lang2.TrPluralString(1, "section.incomplete", 1)
assert.EqualValues(t, "[untranslated] some 1 object", result2)
result2 = lang2.TrPluralString(7, "section.incomplete", 7)
assert.EqualValues(t, "[untranslated] some 7 objects", result2)
langs, descs := ls.ListLangNameDesc()
assert.ElementsMatch(t, []string{"lang1", "lang2"}, langs)
assert.ElementsMatch(t, []string{"Lang1", "Lang2"}, descs)
@ -77,7 +173,7 @@ c=22
`)
ls := NewLocaleStore()
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1, testData2))
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, testData1, testData2))
lang1, _ := ls.Locale("lang1")
assert.Equal(t, "11", lang1.TrString("a"))
assert.Equal(t, "21", lang1.TrString("b"))
@ -118,7 +214,7 @@ func (e *errorPointerReceiver) Error() string {
func TestLocaleWithTemplate(t *testing.T) {
ls := NewLocaleStore()
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", []byte(`key=<a>%s</a>`), nil))
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, []byte(`key=<a>%s</a>`), nil))
lang1, _ := ls.Locale("lang1")
tmpl := template.New("test").Funcs(template.FuncMap{"tr": lang1.TrHTML})
@ -181,7 +277,7 @@ func TestLocaleStoreQuirks(t *testing.T) {
for _, testData := range testDataList {
ls := NewLocaleStore()
err := ls.AddLocaleByIni("lang1", "Lang1", []byte("a="+testData.in), nil)
err := ls.AddLocaleByIni("lang1", "Lang1", nil, []byte("a="+testData.in), nil)
lang1, _ := ls.Locale("lang1")
require.NoError(t, err, testData.hint)
assert.Equal(t, testData.out, lang1.TrString("a"), testData.hint)

View file

@ -8,8 +8,10 @@ import (
"html/template"
"slices"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
// This file implements the static LocaleStore that will not watch for changes
@ -18,6 +20,9 @@ type locale struct {
store *localeStore
langName string
idxToMsgMap map[int]string // the map idx is generated by store's trKeyToIdxMap
newStyleMessages map[string]string
pluralRule PluralFormRule
}
var _ Locale = (*locale)(nil)
@ -38,8 +43,19 @@ func NewLocaleStore() LocaleStore {
return &localeStore{localeMap: make(map[string]*locale), trKeyToIdxMap: make(map[string]int)}
}
const (
PluralFormSeparator string = "\036"
)
// A note about pluralization rules.
// go-i18n supports plural rules in theory.
// In practice, it relies on another library that hardcodes a list of common languages
// and their plural rules, and does not support languages not hardcoded there.
// So we pretend that all languages are English and use our own function to extract
// the correct plural form for a given count and language.
// AddLocaleByIni adds locale by ini into the store
func (store *localeStore) AddLocaleByIni(langName, langDesc string, source, moreSource []byte) error {
func (store *localeStore) AddLocaleByIni(langName, langDesc string, pluralRule PluralFormRule, source, moreSource []byte) error {
if _, ok := store.localeMap[langName]; ok {
return ErrLocaleAlreadyExist
}
@ -47,7 +63,7 @@ func (store *localeStore) AddLocaleByIni(langName, langDesc string, source, more
store.langNames = append(store.langNames, langName)
store.langDescs = append(store.langDescs, langDesc)
l := &locale{store: store, langName: langName, idxToMsgMap: make(map[int]string)}
l := &locale{store: store, langName: langName, idxToMsgMap: make(map[int]string), pluralRule: pluralRule, newStyleMessages: make(map[string]string)}
store.localeMap[l.langName] = l
iniFile, err := setting.NewConfigProviderForLocale(source, moreSource)
@ -78,6 +94,98 @@ func (store *localeStore) AddLocaleByIni(langName, langDesc string, source, more
return nil
}
func RecursivelyAddTranslationsFromJSON(locale *locale, object map[string]any, prefix string) error {
for key, value := range object {
var fullkey string
if prefix != "" {
fullkey = prefix + "." + key
} else {
fullkey = key
}
switch v := value.(type) {
case string:
// Check whether we are adding a plural form to the parent object, or a new nested JSON object.
if key == "zero" || key == "one" || key == "two" || key == "few" || key == "many" {
locale.newStyleMessages[prefix+PluralFormSeparator+key] = v
} else if key == "other" {
locale.newStyleMessages[prefix] = v
} else {
locale.newStyleMessages[fullkey] = v
}
case map[string]any:
err := RecursivelyAddTranslationsFromJSON(locale, v, fullkey)
if err != nil {
return err
}
case nil:
default:
return fmt.Errorf("Unrecognized JSON value '%s'", value)
}
}
return nil
}
func (store *localeStore) AddToLocaleFromJSON(langName string, source []byte) error {
locale, ok := store.localeMap[langName]
if !ok {
return ErrLocaleDoesNotExist
}
var result map[string]any
if err := json.Unmarshal(source, &result); err != nil {
return err
}
return RecursivelyAddTranslationsFromJSON(locale, result, "")
}
func (l *locale) LookupNewStyleMessage(trKey string) string {
if msg, ok := l.newStyleMessages[trKey]; ok {
return msg
}
return ""
}
func (l *locale) LookupPlural(trKey string, count any) string {
n, err := util.ToInt64(count)
if err != nil {
log.Error("Invalid plural count '%s'", count)
return ""
}
pluralForm := l.pluralRule(n)
suffix := ""
switch pluralForm {
case PluralFormZero:
suffix = PluralFormSeparator + "zero"
case PluralFormOne:
suffix = PluralFormSeparator + "one"
case PluralFormTwo:
suffix = PluralFormSeparator + "two"
case PluralFormFew:
suffix = PluralFormSeparator + "few"
case PluralFormMany:
suffix = PluralFormSeparator + "many"
case PluralFormOther:
// No suffix for the "other" string.
default:
log.Error("Invalid plural form index %d for count %d", pluralForm, count)
return ""
}
if result, ok := l.newStyleMessages[trKey+suffix]; ok {
return result
}
log.Error("Missing translation for plural form index %d for count %d", pluralForm, count)
return ""
}
func (store *localeStore) HasLang(langName string) bool {
_, ok := store.localeMap[langName]
return ok
@ -113,22 +221,37 @@ func (store *localeStore) Close() error {
func (l *locale) TrString(trKey string, trArgs ...any) string {
format := trKey
idx, ok := l.store.trKeyToIdxMap[trKey]
found := false
if ok {
if msg, ok := l.idxToMsgMap[idx]; ok {
format = msg // use the found translation
found = true
} else if def, ok := l.store.localeMap[l.store.defaultLang]; ok {
// try to use default locale's translation
if msg, ok := def.idxToMsgMap[idx]; ok {
format = msg
if msg := l.LookupNewStyleMessage(trKey); msg != "" {
format = msg
} else {
// First fallback: old-style translation
idx, ok := l.store.trKeyToIdxMap[trKey]
found := false
if ok {
if msg, ok := l.idxToMsgMap[idx]; ok {
format = msg // use the found translation
found = true
}
}
}
if !found {
log.Error("Missing translation %q", trKey)
if !found {
// Second fallback: new-style default language
if defaultLang, ok := l.store.localeMap[l.store.defaultLang]; ok {
if msg := defaultLang.LookupNewStyleMessage(trKey); msg != "" {
format = msg
} else {
// Third fallback: old-style default language
if msg, ok := defaultLang.idxToMsgMap[idx]; ok {
format = msg
found = true
}
}
}
if !found {
log.Error("Missing translation %q", trKey)
}
}
}
msg, err := Format(format, trArgs...)
@ -138,7 +261,7 @@ func (l *locale) TrString(trKey string, trArgs ...any) string {
return msg
}
func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML {
func PrepareArgsForHTML(trArgs ...any) []any {
args := slices.Clone(trArgs)
for i, v := range args {
switch v := v.(type) {
@ -152,7 +275,30 @@ func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML {
args[i] = template.HTMLEscapeString(fmt.Sprint(v))
}
}
return template.HTML(l.TrString(trKey, args...))
return args
}
func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML {
return template.HTML(l.TrString(trKey, PrepareArgsForHTML(trArgs...)...))
}
func (l *locale) TrPluralString(count any, trKey string, trArgs ...any) template.HTML {
message := l.LookupPlural(trKey, count)
if message == "" {
if defaultLang, ok := l.store.localeMap[l.store.defaultLang]; ok {
message = defaultLang.LookupPlural(trKey, count)
}
if message == "" {
message = trKey
}
}
message, err := Format(message, PrepareArgsForHTML(trArgs...)...)
if err != nil {
log.Error("Error whilst formatting %q in %s: %v", trKey, l.langName, err)
}
return template.HTML(message)
}
// HasKey returns whether a key is present in this locale or not

View file

@ -31,6 +31,10 @@ func (l MockLocale) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
return template.HTML(key1)
}
func (l MockLocale) TrPluralString(count any, trKey string, trArgs ...any) template.HTML {
return template.HTML(trKey)
}
func (l MockLocale) TrSize(s int64) ReadableSize {
return ReadableSize{fmt.Sprint(s), ""}
}

View file

@ -0,0 +1,253 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Some useful links:
// https://www.unicode.org/cldr/charts/46/supplemental/language_plural_rules.html
// https://translate.codeberg.org/languages/$LANGUAGE_CODE/#information
// https://github.com/WeblateOrg/language-data/blob/main/languages.csv
// Note that in some cases there is ambiguity about the correct form for a given language. In this case, ask the locale's translators.
package translation
import (
"strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/translation/i18n"
)
// The constants refer to indices below in `PluralRules` and also in i18n.js, keep them in sync!
const (
PluralRuleDefault = 0
PluralRuleBengali = 1
PluralRuleIcelandic = 2
PluralRuleFilipino = 3
PluralRuleOneForm = 4
PluralRuleCzech = 5
PluralRuleRussian = 6
PluralRulePolish = 7
PluralRuleLatvian = 8
PluralRuleLithuanian = 9
PluralRuleFrench = 10
PluralRuleCatalan = 11
PluralRuleSlovenian = 12
PluralRuleArabic = 13
)
func GetPluralRuleImpl(langName string) int {
// First, check for languages with country-specific plural rules.
switch langName {
case "pt-BR":
return PluralRuleFrench
case "pt-PT":
return PluralRuleCatalan
default:
break
}
// Remove the country portion of the locale name.
langName = strings.Split(strings.Split(langName, "_")[0], "-")[0]
// When adding a new language not in the list, add its plural rule definition here.
switch langName {
case "en", "aa", "ab", "abr", "ada", "ae", "aeb", "af", "afh", "aii", "ain", "akk", "ale", "aln", "alt", "ami", "an", "ang", "anp", "apc", "arc", "arp", "arq", "arw", "arz", "asa", "ast", "av", "avk", "awa", "ayc", "az", "azb", "ba", "bal", "ban", "bar", "bas", "bbc", "bci", "bej", "bem", "ber", "bew", "bez", "bg", "bgc", "bgn", "bhb", "bhi", "bi", "bik", "bin", "bjj", "bjn", "bla", "bnt", "bqi", "bra", "brb", "brh", "brx", "bua", "bug", "bum", "byn", "cad", "cak", "car", "ce", "cgg", "ch", "chb", "chg", "chk", "chm", "chn", "cho", "chp", "chr", "chy", "ckb", "co", "cop", "cpe", "cpf", "cr", "crp", "cu", "cv", "da", "dak", "dar", "dcc", "de", "del", "den", "dgr", "din", "dje", "dnj", "dnk", "dru", "dry", "dua", "dum", "dv", "dyu", "ee", "efi", "egl", "egy", "eka", "el", "elx", "enm", "eo", "et", "eu", "ewo", "ext", "fan", "fat", "fbl", "ffm", "fi", "fj", "fo", "fon", "frk", "frm", "fro", "frr", "frs", "fuq", "fur", "fuv", "fvr", "fy", "gaa", "gay", "gba", "gbm", "gez", "gil", "gl", "glk", "gmh", "gn", "goh", "gom", "gon", "gor", "got", "grb", "gsw", "guc", "gum", "gur", "guz", "gwi", "ha", "hai", "haw", "haz", "hil", "hit", "hmn", "hnd", "hne", "hno", "ho", "hoc", "hoj", "hrx", "ht", "hu", "hup", "hus", "hz", "ia", "iba", "ibb", "ie", "ik", "ilo", "inh", "io", "jam", "jgo", "jmc", "jpr", "jrb", "ka", "kaa", "kac", "kaj", "kam", "kaw", "kbd", "kcg", "kfr", "kfy", "kg", "kha", "khn", "kho", "ki", "kj", "kk", "kkj", "kl", "kln", "kmb", "kmr", "kok", "kpe", "kr", "krc", "kri", "krl", "kru", "ks", "ksb", "ku", "kum", "kut", "kv", "kxm", "ky", "la", "lad", "laj", "lam", "lb", "lez", "lfn", "lg", "li", "lij", "ljp", "lki", "lmn", "lmo", "lol", "loz", "lrc", "lu", "lua", "lui", "lun", "luo", "lus", "luy", "luz", "mad", "mag", "mai", "mak", "man", "mas", "mdf", "mdh", "mdr", "men", "mer", "mfa", "mga", "mgh", "mgo", "mh", "mhr", "mic", "min", "mjw", "ml", "mn", "mnc", "mni", "mnw", "moe", "moh", "mos", "mr", "mrh", "mtr", "mus", "mwk", "mwl", "mwr", "mxc", "myv", "myx", "mzn", "na", "nah", "nap", "nb", "nd", "ndc", "nds", "ne", "new", "ng", "ngl", "nia", "nij", "niu", "nl", "nn", "nnh", "nod", "noe", "nog", "non", "nr", "nuk", "nv", "nwc", "ny", "nym", "nyn", "nyo", "nzi", "oj", "om", "or", "os", "ota", "otk", "ovd", "pag", "pal", "pam", "pap", "pau", "pbb", "pdt", "peo", "phn", "pi", "pms", "pon", "pro", "ps", "pwn", "qu", "quc", "qug", "qya", "raj", "rap", "rar", "rcf", "rej", "rhg", "rif", "rkt", "rm", "rmt", "rn", "rng", "rof", "rom", "rue", "rup", "rw", "rwk", "sad", "sai", "sam", "saq", "sas", "sc", "sck", "sco", "sd", "sdh", "sef", "seh", "sel", "sga", "sgn", "sgs", "shn", "sid", "sjd", "skr", "sm", "sml", "sn", "snk", "so", "sog", "sou", "sq", "srn", "srr", "ss", "ssy", "st", "suk", "sus", "sux", "sv", "sw", "swg", "swv", "sxu", "syc", "syl", "syr", "szy", "ta", "tay", "tcy", "te", "tem", "teo", "ter", "tet", "tig", "tiv", "tk", "tkl", "tli", "tly", "tmh", "tn", "tog", "tr", "trv", "ts", "tsg", "tsi", "tsj", "tts", "tum", "tvl", "tw", "ty", "tyv", "tzj", "tzl", "udm", "ug", "uga", "umb", "und", "unr", "ur", "uz", "vai", "ve", "vls", "vmf", "vmw", "vo", "vot", "vro", "vun", "wae", "wal", "war", "was", "wbq", "wbr", "wep", "wtm", "xal", "xh", "xnr", "xog", "yao", "yap", "yi", "yua", "za", "zap", "zbl", "zen", "zgh", "zun", "zza":
return PluralRuleDefault
case "ach", "ady", "ak", "am", "arn", "as", "bh", "bho", "bn", "csw", "doi", "fa", "ff", "frc", "frp", "gu", "gug", "gun", "guw", "hi", "hy", "kab", "kn", "ln", "mfe", "mg", "mi", "mia", "nso", "oc", "pa", "pcm", "pt", "qdt", "qtp", "si", "tg", "ti", "wa", "zu":
return PluralRuleBengali
case "is":
return PluralRuleIcelandic
case "fil":
return PluralRuleFilipino
case "ace", "ay", "bm", "bo", "cdo", "cpx", "crh", "dz", "gan", "hak", "hnj", "hsn", "id", "ig", "ii", "ja", "jbo", "jv", "kde", "kea", "km", "ko", "kos", "lkt", "lo", "lzh", "ms", "my", "nan", "nqo", "osa", "sah", "ses", "sg", "son", "su", "th", "tlh", "to", "tok", "tpi", "tt", "vi", "wo", "wuu", "yo", "yue", "zh":
return PluralRuleOneForm
case "cpp", "cs", "sk":
return PluralRuleCzech
case "be", "bs", "cnr", "hr", "ru", "sr", "uk", "wen":
return PluralRuleRussian
case "csb", "pl", "szl":
return PluralRulePolish
case "lv", "prg":
return PluralRuleLatvian
case "lt":
return PluralRuleLithuanian
case "fr":
return PluralRuleFrench
case "ca", "es", "it":
return PluralRuleCatalan
case "sl":
return PluralRuleSlovenian
case "ar":
return PluralRuleArabic
default:
break
}
log.Error("No plural rule defined for language %s", langName)
return PluralRuleDefault
}
var PluralRules = []i18n.PluralFormRule{
// [ 0] Common 2-form, e.g. English, German
func(n int64) i18n.PluralFormIndex {
if n != 1 {
return i18n.PluralFormOther
}
return i18n.PluralFormOne
},
// [ 1] Bengali
func(n int64) i18n.PluralFormIndex {
if n > 1 {
return i18n.PluralFormOther
}
return i18n.PluralFormOne
},
// [ 2] Icelandic
func(n int64) i18n.PluralFormIndex {
if n%10 != 1 || n%100 == 11 {
return i18n.PluralFormOther
}
return i18n.PluralFormOne
},
// [ 3] Filipino
func(n int64) i18n.PluralFormIndex {
if n != 1 && n != 2 && n != 3 && (n%10 == 4 || n%10 == 6 || n%10 == 9) {
return i18n.PluralFormOther
}
return i18n.PluralFormOne
},
// [ 4] OneForm
func(n int64) i18n.PluralFormIndex {
return i18n.PluralFormOther
},
// [ 5] Czech
func(n int64) i18n.PluralFormIndex {
if n == 1 {
return i18n.PluralFormOne
}
if n >= 2 && n <= 4 {
return i18n.PluralFormFew
}
return i18n.PluralFormOther
},
// [ 6] Russian
func(n int64) i18n.PluralFormIndex {
if n%10 == 1 && n%100 != 11 {
return i18n.PluralFormOne
}
if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) {
return i18n.PluralFormFew
}
return i18n.PluralFormMany
},
// [ 7] Polish
func(n int64) i18n.PluralFormIndex {
if n == 1 {
return i18n.PluralFormOne
}
if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) {
return i18n.PluralFormFew
}
return i18n.PluralFormMany
},
// [ 8] Latvian
func(n int64) i18n.PluralFormIndex {
if n%10 == 0 || n%100 >= 11 && n%100 <= 19 {
return i18n.PluralFormZero
}
if n%10 == 1 && n%100 != 11 {
return i18n.PluralFormOne
}
return i18n.PluralFormOther
},
// [ 9] Lithuanian
func(n int64) i18n.PluralFormIndex {
if n%10 == 1 && (n%100 < 11 || n%100 > 19) {
return i18n.PluralFormOne
}
if n%10 >= 2 && n%10 <= 9 && (n%100 < 11 || n%100 > 19) {
return i18n.PluralFormFew
}
return i18n.PluralFormMany
},
// [10] French
func(n int64) i18n.PluralFormIndex {
if n == 0 || n == 1 {
return i18n.PluralFormOne
}
if n != 0 && n%1000000 == 0 {
return i18n.PluralFormMany
}
return i18n.PluralFormOther
},
// [11] Catalan
func(n int64) i18n.PluralFormIndex {
if n == 1 {
return i18n.PluralFormOne
}
if n != 0 && n%1000000 == 0 {
return i18n.PluralFormMany
}
return i18n.PluralFormOther
},
// [12] Slovenian
func(n int64) i18n.PluralFormIndex {
if n%100 == 1 {
return i18n.PluralFormOne
}
if n%100 == 2 {
return i18n.PluralFormTwo
}
if n%100 == 3 || n%100 == 4 {
return i18n.PluralFormFew
}
return i18n.PluralFormOther
},
// [13] Arabic
func(n int64) i18n.PluralFormIndex {
if n == 0 {
return i18n.PluralFormZero
}
if n == 1 {
return i18n.PluralFormOne
}
if n == 2 {
return i18n.PluralFormTwo
}
if n%100 >= 3 && n%100 <= 10 {
return i18n.PluralFormFew
}
if n%100 >= 11 {
return i18n.PluralFormMany
}
return i18n.PluralFormOther
},
}

View file

@ -32,6 +32,9 @@ type Locale interface {
TrString(string, ...any) string
Tr(key string, args ...any) template.HTML
// New-style pluralized strings
TrPluralString(count any, trKey string, trArgs ...any) template.HTML
// Old-style pseudo-pluralized strings, deprecated
TrN(cnt any, key1, keyN string, args ...any) template.HTML
TrSize(size int64) ReadableSize
@ -100,8 +103,17 @@ func InitLocales(ctx context.Context) {
}
key := "locale_" + setting.Langs[i] + ".ini"
if err = i18n.DefaultLocales.AddLocaleByIni(setting.Langs[i], setting.Names[i], localeDataBase, localeData[key]); err != nil {
log.Error("Failed to set messages to %s: %v", setting.Langs[i], err)
if err = i18n.DefaultLocales.AddLocaleByIni(setting.Langs[i], setting.Names[i], PluralRules[GetPluralRuleImpl(setting.Langs[i])], localeDataBase, localeData[key]); err != nil {
log.Error("Failed to set old-style messages to %s: %v", setting.Langs[i], err)
}
key = "locale_next/locale_" + setting.Langs[i] + ".json"
if bytes, err := options.AssetFS().ReadFile(key); err == nil {
if err = i18n.DefaultLocales.AddToLocaleFromJSON(setting.Langs[i], bytes); err != nil {
log.Error("Failed to add new-style messages to %s: %v", setting.Langs[i], err)
}
} else {
log.Error("Failed to open new-style messages for %s: %v", setting.Langs[i], err)
}
}
if len(setting.Langs) != 0 {

View file

@ -48,3 +48,111 @@ func TestPrettyNumber(t *testing.T) {
assert.EqualValues(t, "1,000,000", l.PrettyNumber(1000000))
assert.EqualValues(t, "1,000,000.1", l.PrettyNumber(1000000.1))
}
func TestGetPluralRule(t *testing.T) {
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en"))
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en-US"))
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en_UK"))
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("nds"))
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("de-DE"))
assert.Equal(t, PluralRuleOneForm, GetPluralRuleImpl("zh"))
assert.Equal(t, PluralRuleOneForm, GetPluralRuleImpl("ja"))
assert.Equal(t, PluralRuleBengali, GetPluralRuleImpl("bn"))
assert.Equal(t, PluralRuleIcelandic, GetPluralRuleImpl("is"))
assert.Equal(t, PluralRuleFilipino, GetPluralRuleImpl("fil"))
assert.Equal(t, PluralRuleCzech, GetPluralRuleImpl("cs"))
assert.Equal(t, PluralRuleRussian, GetPluralRuleImpl("ru"))
assert.Equal(t, PluralRulePolish, GetPluralRuleImpl("pl"))
assert.Equal(t, PluralRuleLatvian, GetPluralRuleImpl("lv"))
assert.Equal(t, PluralRuleLithuanian, GetPluralRuleImpl("lt"))
assert.Equal(t, PluralRuleFrench, GetPluralRuleImpl("fr"))
assert.Equal(t, PluralRuleCatalan, GetPluralRuleImpl("ca"))
assert.Equal(t, PluralRuleSlovenian, GetPluralRuleImpl("sl"))
assert.Equal(t, PluralRuleArabic, GetPluralRuleImpl("ar"))
assert.Equal(t, PluralRuleCatalan, GetPluralRuleImpl("pt-PT"))
assert.Equal(t, PluralRuleFrench, GetPluralRuleImpl("pt-BR"))
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("invalid"))
}
func TestApplyPluralRule(t *testing.T) {
testCases := []struct {
expect i18n.PluralFormIndex
pluralRule int
values []int64
}{
{i18n.PluralFormOne, PluralRuleDefault, []int64{1}},
{i18n.PluralFormOther, PluralRuleDefault, []int64{0, 2, 10, 256}},
{i18n.PluralFormOther, PluralRuleOneForm, []int64{0, 1, 2}},
{i18n.PluralFormOne, PluralRuleBengali, []int64{0, 1}},
{i18n.PluralFormOther, PluralRuleBengali, []int64{2, 10, 256}},
{i18n.PluralFormOne, PluralRuleIcelandic, []int64{1, 21, 31}},
{i18n.PluralFormOther, PluralRuleIcelandic, []int64{0, 2, 11, 15, 256}},
{i18n.PluralFormOne, PluralRuleFilipino, []int64{0, 1, 2, 3, 5, 7, 8, 10, 11, 12, 257}},
{i18n.PluralFormOther, PluralRuleFilipino, []int64{4, 6, 9, 14, 16, 19, 256}},
{i18n.PluralFormOne, PluralRuleCzech, []int64{1}},
{i18n.PluralFormFew, PluralRuleCzech, []int64{2, 3, 4}},
{i18n.PluralFormOther, PluralRuleCzech, []int64{5, 0, 12, 78, 254}},
{i18n.PluralFormOne, PluralRuleRussian, []int64{1, 21, 31}},
{i18n.PluralFormFew, PluralRuleRussian, []int64{2, 23, 34}},
{i18n.PluralFormMany, PluralRuleRussian, []int64{0, 5, 11, 37, 111, 256}},
{i18n.PluralFormOne, PluralRulePolish, []int64{1}},
{i18n.PluralFormFew, PluralRulePolish, []int64{2, 23, 34}},
{i18n.PluralFormMany, PluralRulePolish, []int64{0, 5, 11, 21, 37, 256}},
{i18n.PluralFormZero, PluralRuleLatvian, []int64{0, 10, 11, 17}},
{i18n.PluralFormOne, PluralRuleLatvian, []int64{1, 21, 71}},
{i18n.PluralFormOther, PluralRuleLatvian, []int64{2, 7, 22, 23, 256}},
{i18n.PluralFormOne, PluralRuleLithuanian, []int64{1, 21, 31}},
{i18n.PluralFormFew, PluralRuleLithuanian, []int64{2, 5, 9, 23, 34, 256}},
{i18n.PluralFormMany, PluralRuleLithuanian, []int64{0, 10, 11, 18}},
{i18n.PluralFormOne, PluralRuleFrench, []int64{0, 1}},
{i18n.PluralFormMany, PluralRuleFrench, []int64{1000000, 2000000}},
{i18n.PluralFormOther, PluralRuleFrench, []int64{2, 4, 10, 256}},
{i18n.PluralFormOne, PluralRuleCatalan, []int64{1}},
{i18n.PluralFormMany, PluralRuleCatalan, []int64{1000000, 2000000}},
{i18n.PluralFormOther, PluralRuleCatalan, []int64{0, 2, 4, 10, 256}},
{i18n.PluralFormOne, PluralRuleSlovenian, []int64{1, 101, 201, 501}},
{i18n.PluralFormTwo, PluralRuleSlovenian, []int64{2, 102, 202, 502}},
{i18n.PluralFormFew, PluralRuleSlovenian, []int64{3, 103, 203, 503, 4, 104, 204, 504}},
{i18n.PluralFormOther, PluralRuleSlovenian, []int64{0, 5, 11, 12, 20, 256}},
{i18n.PluralFormZero, PluralRuleArabic, []int64{0}},
{i18n.PluralFormOne, PluralRuleArabic, []int64{1}},
{i18n.PluralFormTwo, PluralRuleArabic, []int64{2}},
{i18n.PluralFormFew, PluralRuleArabic, []int64{3, 4, 9, 10, 103, 104}},
{i18n.PluralFormMany, PluralRuleArabic, []int64{11, 12, 13, 14, 17, 111, 256}},
{i18n.PluralFormOther, PluralRuleArabic, []int64{100, 101, 102}},
}
for _, tc := range testCases {
for _, n := range tc.values {
assert.Equal(t, tc.expect, PluralRules[tc.pluralRule](n), "Testcase for plural rule %d, value %d", tc.pluralRule, n)
}
}
}

View file

@ -190,7 +190,6 @@ commit_kind = Search commits...
runner_kind = Search runners...
no_results = No matching results found.
issue_kind = Search issues...
milestone_kind = Search milestones...
pull_kind = Search pulls...
keyword_search_unavailable = Searching by keyword is currently not available. Please contact the site administrator.
@ -1887,10 +1886,6 @@ pulls.nothing_to_compare_have_tag = The selected branch/tag are equal.
pulls.nothing_to_compare_and_allow_empty_pr = These branches are equal. This PR will be empty.
pulls.has_pull_request = `A pull request between these branches already exists: <a href="%[1]s">%[2]s#%[3]d</a>`
pulls.create = Create pull request
pulls.title_desc_one = wants to merge %[1]d commit from <code>%[2]s</code> into <code id="%[4]s">%[3]s</code>
pulls.title_desc_few = wants to merge %[1]d commits from <code>%[2]s</code> into <code id="%[4]s">%[3]s</code>
pulls.merged_title_desc_one = merged %[1]d commit from <code>%[2]s</code> into <code>%[3]s</code> %[4]s
pulls.merged_title_desc_few = merged %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> %[4]s
pulls.change_target_branch_at = `changed target branch from <b>%s</b> to <b>%s</b> %s`
pulls.tab_conversation = Conversation
pulls.tab_commits = Commits
@ -2648,7 +2643,6 @@ diff.git-notes.remove-header = Remove note
diff.git-notes.remove-body = This note will be removed.
diff.data_not_available = Diff content is not available
diff.options_button = Diff options
diff.show_diff_stats = Show stats
diff.download_patch = Download patch file
diff.download_diff = Download diff file
diff.show_split_view = Split view

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1,10 @@
{
"repo.pulls.merged_title_desc": {
"one": "сля %[1]d подаване от <code>%[2]s</code> в <code>%[3]s</code> %[4]s",
"other": "сля %[1]d подавания от <code>%[2]s</code> в <code>%[3]s</code> %[4]s"
},
"repo.pulls.title_desc": {
"one": "иска да слее %[1]d подаване от <code>%[2]s</code> в <code id=\"%[4]s\">%[3]s</code>",
"other": "иска да слее %[1]d подавания от <code>%[2]s</code> в <code id=\"%[4]s\">%[3]s</code>"
}
}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1,3 @@
{
"search.milestone_kind": "Cerca fites..."
}

View file

@ -0,0 +1,11 @@
{
"repo.pulls.merged_title_desc": {
"one": "sloučil %[1]d commit z <code>%[2]s</code> do <code>%[3]s</code> %[4]s",
"other": "sloučil %[1]d commity z větve <code>%[2]s</code> do větve <code>%[3]s</code> před %[4]s"
},
"repo.pulls.title_desc": {
"one": "žádá o sloučení %[1]d commitu z <code>%[2]s</code> do <code id=\"%[4]s\">%[3]s</code>",
"other": "chce sloučit %[1]d commity z větve <code>%[2]s</code> do <code id=\"%[4]s\">%[3]s</code>"
},
"search.milestone_kind": "Hledat milníky..."
}

View file

@ -0,0 +1,3 @@
{
"search.milestone_kind": "Søg milepæle..."
}

View file

@ -0,0 +1,11 @@
{
"repo.pulls.merged_title_desc": {
"one": "hat %[1]d Commit von <code>%[2]s</code> nach <code>%[3]s</code> %[4]s zusammengeführt",
"other": "hat %[1]d Commits von <code>%[2]s</code> nach <code>%[3]s</code> %[4]s zusammengeführt"
},
"repo.pulls.title_desc": {
"one": "möchte %[1]d Commit von <code>%[2]s</code> nach <code id=\"%[4]s\">%[3]s</code> zusammenführen",
"other": "möchte %[1]d Commits von <code>%[2]s</code> nach <code id=\"%[4]s\">%[3]s</code> zusammenführen"
},
"search.milestone_kind": "Meilensteine suchen …"
}

View file

@ -0,0 +1,11 @@
{
"repo.pulls.merged_title_desc": {
"one": "συγχώνευσε %[1]d υποβολή από τον κλάδο <code>%[2]s</code> στον κλάδο <code>%[3]s</code> %[4]s",
"other": "συγχώνευσε %[1]d υποβολές από <code>%[2]s</code> σε <code>%[3]s</code> %[4]s"
},
"repo.pulls.title_desc": {
"one": ": θα ήθελε να συγχωνεύσει %[1]d υποβολή από τον κλάδο <code>%[2]s</code> στον κλάδο <code id=\"%[4]s\">%[3]s</code>",
"other": "θέλει να συγχωνεύσει %[1]d υποβολές από <code>%[2]s</code> σε <code id=\"%[4]s\">%[3]s</code>"
},
"search.milestone_kind": "Αναζήτηση ορόσημων..."
}

View file

@ -0,0 +1,11 @@
{
"repo.pulls.merged_title_desc": {
"one": "merged %[1]d commit from <code>%[2]s</code> into <code>%[3]s</code> %[4]s",
"other": "merged %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> %[4]s"
},
"repo.pulls.title_desc": {
"one": "wants to merge %[1]d commit from <code>%[2]s</code> into <code id=\"%[4]s\">%[3]s</code>",
"other": "wants to merge %[1]d commits from <code>%[2]s</code> into <code id=\"%[4]s\">%[3]s</code>"
},
"search.milestone_kind": "Search milestones..."
}

View file

@ -0,0 +1,3 @@
{
"search.milestone_kind": "Serĉi celojn..."
}

View file

@ -0,0 +1,11 @@
{
"repo.pulls.merged_title_desc": {
"one": "fusionó %[1]d commit de <code>%[2]s</code> en <code>%[3]s</code> %[4]s",
"other": "fusionó %[1]d commits de <code>%[2]s</code> en <code>%[3]s</code> %[4]s"
},
"repo.pulls.title_desc": {
"one": "quiere fusionar %[1]d commit de <code>%[2]s</code> en <code id=\"%[4]s\">%[3]s</code>",
"other": "quiere fusionar %[1]d commits de <code>%[2]s</code> en <code id=\"%[4]s\">%[3]s</code>"
},
"search.milestone_kind": "Buscar hitos…"
}

View file

@ -0,0 +1,3 @@
{
"search.milestone_kind": "Otsi verstapostid..."
}

View file

@ -0,0 +1,8 @@
{
"repo.pulls.merged_title_desc": {
"other": "%[1]d کامیت ادغام شده از <code>%[2]s</code> به <code>%[3]s</code> %[4]s"
},
"repo.pulls.title_desc": {
"other": "قصد ادغام %[1]d تغییر را از <code>%[2]s</code> به <code id=\"%[4]s\">%[3]s</code> دارد"
}
}

View file

@ -0,0 +1,9 @@
{
"repo.pulls.merged_title_desc": {
"other": "yhdistetty %[1]d committia lähteestä <code>%[2]s</code> kohteeseen <code>%[3]s</code> %[4]s"
},
"repo.pulls.title_desc": {
"other": "haluaa yhdistää %[1]d committia lähteestä <code>%[2]s</code> kohteeseen <code id=\"%[4]s\">%[3]s</code>"
},
"search.milestone_kind": "Etsi merkkipaaluja..."
}

View file

@ -0,0 +1,11 @@
{
"repo.pulls.merged_title_desc": {
"one": "isinali ang %[1]d commit mula<code>%[2]s</code> patungong <code>%[3]s</code> %[4]s",
"other": "isinali ang %[1]d mga commit mula sa <code>%[2]s</code> patungong <code>%[3]s</code> %[4]s"
},
"repo.pulls.title_desc": {
"one": "hinihiling na isama ang %[1]d commit mula <code>%[2]s</code> patungong <code id=\"%[4]s\">%[3]s</code>",
"other": "hiniling na isama ang %[1]d mga commit mula sa <code>%[2]s</code> patungong <code id=\"%[4]s\">%[3]s</code>"
},
"search.milestone_kind": "Maghanap ng mga milestone…"
}

View file

@ -0,0 +1,11 @@
{
"repo.pulls.merged_title_desc": {
"one": "fusionné %[1]d commit depuis <code>%[2]s</code> vers <code>%[3]s</code> %[4]s",
"other": "a fusionné %[1]d révision(s) à partir de <code>%[2]s</code> vers <code>%[3]s</code> %[4]s"
},
"repo.pulls.title_desc": {
"one": "veut fusionner %[1]d commit depuis <code>%[2]s</code> vers <code id=\"%[4]s\">%[3]s</code>",
"other": "souhaite fusionner %[1]d révision(s) depuis <code>%[2]s</code> vers <code id=\"%[4]s\">%[3]s</code>"
},
"search.milestone_kind": "Recherche dans les jalons..."
}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1,9 @@
{
"repo.pulls.merged_title_desc": {
"other": "egyesítve %[1]d változás(ok) a <code>%[2]s</code>-ból <code>%[3]s</code>-ba %[4]s"
},
"repo.pulls.title_desc": {
"other": "egyesíteni szeretné %[1]d változás(oka)t a(z) <code>%[2]s</code>-ból <code id=\"%[4]s\">%[3]s</code>-ba"
},
"search.milestone_kind": "Mérföldkövek keresése..."
}

View file

@ -0,0 +1,8 @@
{
"repo.pulls.merged_title_desc": {
"other": "commit %[1]d telah digabungkan dari <code>%[2]s</code> menjadi <code>%[3]s</code> %[4]s"
},
"repo.pulls.title_desc": {
"other": "ingin menggabungkan komit %[1]d dari <code>%[2]s</code> menuju <code id=\"%[4]s\">%[3]s</code>"
}
}

View file

@ -0,0 +1,5 @@
{
"repo.pulls.title_desc": {
"other": "vill sameina %[1]d framlög frá <code>%[2]s</code> í <code id=\"%[4]s\">%[3]s</code>"
}
}

View file

@ -0,0 +1,11 @@
{
"repo.pulls.merged_title_desc": {
"one": "ha fuso %[1]d commit da <code>%[2]s</code> in <code>%[3]s</code> %[4]s",
"other": "ha unito %[1]d commit da <code>%[2]s</code> a <code>%[3]s</code> %[4]s"
},
"repo.pulls.title_desc": {
"one": "vuole fondere %[1]d commit da <code>%[2]s</code> in <code id=\"%[4]s\">%[3]s</code>",
"other": "vuole unire %[1]d commit da <code>%[2]s</code> a <code id=\"%[4]s\">%[3]s</code>"
},
"search.milestone_kind": "Ricerca tappe..."
}

View file

@ -0,0 +1,9 @@
{
"repo.pulls.merged_title_desc": {
"other": "が %[1]d 個のコミットを <code>%[2]s</code> から <code>%[3]s</code> へマージ %[4]s"
},
"repo.pulls.title_desc": {
"other": "が <code>%[2]s</code> から <code id=\"%[4]s\">%[3]s</code> への %[1]d コミットのマージを希望しています"
},
"search.milestone_kind": "マイルストーンを検索..."
}

View file

@ -0,0 +1,8 @@
{
"repo.pulls.merged_title_desc": {
"other": "님이 <code>%[2]s</code> 에서 <code>%[3]s</code> 로 %[1]d 커밋을 %[4]s 병합함"
},
"repo.pulls.title_desc": {
"other": "<code>%[2]s</code> 에서 <code id=\"%[4]s\">%[3]s</code> 로 %[1]d개의 커밋들을 병합하려함"
}
}

View file

@ -0,0 +1,3 @@
{
"search.milestone_kind": "Ieškoti gairių..."
}

View file

@ -0,0 +1,11 @@
{
"repo.pulls.merged_title_desc": {
"one": "iekļāva %[1]d iesūtījumu no <code>%[2]s</code> <code>%[3]s</code> %[4]s",
"other": "Iekļāva %[1]d iesūtījumus no <code>%[2]s</code> zarā <code>%[3]s</code> %[4]s"
},
"repo.pulls.title_desc": {
"one": "vēlas iekļaut %[1]d iesūtījumu no <code>%[2]s</code> <code id=\"%[4]s\">%[3]s</code>",
"other": "vēlas iekļaut %[1]d iesūtījumus no <code>%[2]s</code> zarā <code id=\"%[4]s\">%[3]s</code>"
},
"search.milestone_kind": "Meklēt atskaites punktus..."
}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1,11 @@
{
"repo.pulls.merged_title_desc": {
"one": "hett %[1]d Kommitteren vun <code>%[2]s</code> na <code>%[3]s</code> %[4]s tosamenföhrt",
"other": "hett %[1]d Kommitterens vun <code>%[2]s</code> na <code>%[3]s</code> %[4]s tosamenföhrt"
},
"repo.pulls.title_desc": {
"one": "will %[1]d Kommitteren vun <code>%[2]s</code> na <code id=\"%[4]s\">%[3]s</code> tosamenföhren",
"other": "will %[1]d Kommitterens vun <code>%[2]s</code> na <code id=\"%[4]s\">%[3]s</code> tosamenföhren"
},
"search.milestone_kind": "In Markstenen söken …"
}

View file

@ -0,0 +1,11 @@
{
"repo.pulls.merged_title_desc": {
"one": "heeft %[1]d commit van <code>%[2]s</code> samengevoegd in <code>%[3]s</code> %[4]s",
"other": "heeft %[1]d commits samengevoegd van <code>%[2]s</code> naar <code>%[3]s</code> %[4]s"
},
"repo.pulls.title_desc": {
"one": "wilt %[1]d commit van <code>%[2]s</code> samenvoegen in <code id=\"%[4]s\">%[3]s</code>",
"other": "wilt %[1]d commits van <code>%[2]s</code> samenvoegen met <code id=\"%[4]s\">%[3]s</code>"
},
"search.milestone_kind": "Zoek mijlpalen..."
}

View file

@ -0,0 +1,9 @@
{
"repo.pulls.merged_title_desc": {
"many": "scala %[1]d commity/ów z <code>%[2]s</code> do <code>%[3]s</code> %[4]s"
},
"repo.pulls.title_desc": {
"many": "chce scalić %[1]d commity/ów z <code>%[2]s</code> do <code id=\"%[4]s\">%[3]s</code>"
},
"search.milestone_kind": "Wyszukaj kamienie milowe..."
}

View file

@ -0,0 +1,11 @@
{
"repo.pulls.merged_title_desc": {
"one": "mesclou %[1]d commit de <code>%[2]s</code> em <code>%[3]s</code> %[4]s",
"other": "mesclou %[1]d commits de <code>%[2]s</code> em <code>%[3]s</code> %[4]s"
},
"repo.pulls.title_desc": {
"one": "quer mesclar %[1]d commit de <code>%[2]s</code> em <code id=\"%[4]s\">%[3]s</code>",
"other": "quer mesclar %[1]d commits de <code>%[2]s</code> em <code id=\"%[4]s\">%[3]s</code>"
},
"search.milestone_kind": "Pesquisar marcos..."
}

View file

@ -0,0 +1,11 @@
{
"repo.pulls.merged_title_desc": {
"one": "integrou %[1]d cometimento do ramo <code>%[2]s</code> no ramo <code>%[3]s</code> %[4]s",
"other": "integrou %[1]d cometimento(s) do ramo <code>%[2]s</code> no ramo <code>%[3]s</code> %[4]s"
},
"repo.pulls.title_desc": {
"one": "quer integrar %[1]d cometimento do ramo <code>%[2]s</code> no ramo <code id=\"%[4]s\">%[3]s</code>",
"other": "quer integrar %[1]d cometimento(s) do ramo <code>%[2]s</code> no ramo <code id=\"%[4]s\">%[3]s</code>"
},
"search.milestone_kind": "Procurar etapas..."
}

View file

@ -0,0 +1,11 @@
{
"repo.pulls.merged_title_desc": {
"one": "слит %[1]d коммит из <code>%[2]s</code> в <code>%[3]s</code> %[4]s",
"many": "слито %[1]d коммит(ов) из <code>%[2]s</code> в <code>%[3]s</code> %[4]s"
},
"repo.pulls.title_desc": {
"one": "хочет влить %[1]d коммит из <code>%[2]s</code> в <code id=\"%[4]s\">%[3]s</code>",
"many": "хочет влить %[1]d коммит(ов) из <code>%[2]s</code> в <code id=\"%[4]s\">%[3]s</code>"
},
"search.milestone_kind": "Найти этапы..."
}

View file

@ -0,0 +1,8 @@
{
"repo.pulls.merged_title_desc": {
"other": "මර්ජ්%[1]d සිට <code>%[2]s</code> දක්වා <code>%[3]s</code> %[4]s"
},
"repo.pulls.title_desc": {
"other": "%[1]d සිට <code>%[2]s</code> දක්වා <code id=\"%[4]s\">%[3]s</code>"
}
}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1,9 @@
{
"repo.pulls.merged_title_desc": {
"other": "sammanfogade %[1]d incheckningar från <code>%[2]s</code> in i <code>%[3]s</code> %[4]s"
},
"repo.pulls.title_desc": {
"other": "vill sammanfoga %[1]d incheckningar från <code>s[2]s</code> in i <code id=\"%[4]s\">%[3]s</code>"
},
"search.milestone_kind": "Sök milstolpar..."
}

View file

@ -0,0 +1,9 @@
{
"repo.pulls.merged_title_desc": {
"other": "%[4]s <code>%[2]s</code> içindeki %[1]d işlemeyi <code>%[3]s</code> ile birleştirdi"
},
"repo.pulls.title_desc": {
"other": "<code>%[2]s</code> içindeki %[1]d işlemeyi <code id=\"%[4]s\">%[3]s</code> ile birleştirmek istiyor"
},
"search.milestone_kind": "Kilometre taşlarını ara..."
}

View file

@ -0,0 +1,11 @@
{
"repo.pulls.merged_title_desc": {
"one": "об'єднав %[1]d коміт з <code>%[2]s</code> в <code>%[3]s</code> %[4]s",
"many": "об'єднав %[1]d комітів з <code>%[2]s</code> в <code>%[3]s</code> %[4]s"
},
"repo.pulls.title_desc": {
"one": "хоче об'єднати %[1]d коміт з <code>%[2]s</code> в <code id=\"%[4]s\">%[3]s</code>",
"many": "хоче об'єднати %[1]d комітів з <code>%[2]s</code> в <code id=\"%[4]s\">%[3]s</code>"
},
"search.milestone_kind": "Шукати віхи..."
}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1,9 @@
{
"repo.pulls.merged_title_desc": {
"other": "于 %[4]s 将 %[1]d 次代码提交从 <code>%[2]s</code>合并至 <code>%[3]s</code>"
},
"repo.pulls.title_desc": {
"other": "请求将 %[1]d 次代码提交从 <code>%[2]s</code> 合并至 <code id=\"%[4]s\">%[3]s</code>"
},
"search.milestone_kind": "搜索里程碑…"
}

View file

@ -0,0 +1,5 @@
{
"repo.pulls.merged_title_desc": {
"other": "於 %[4]s 將 %[1]d 次代碼提交從 <code>%[2]s</code>合併至 <code>%[3]s</code>"
}
}

View file

@ -0,0 +1,9 @@
{
"repo.pulls.merged_title_desc": {
"other": "將 %[1]d 次提交從 <code>%[2]s</code> 合併至 <code>%[3]s</code> %[4]s"
},
"repo.pulls.title_desc": {
"other": "請求將 %[1]d 次程式碼提交從 <code>%[2]s</code> 合併至 <code id=\"%[4]s\">%[3]s</code>"
},
"search.milestone_kind": "搜尋里程碑..."
}

36
package-lock.json generated
View file

@ -10,7 +10,7 @@
"@citation-js/plugin-software-formats": "0.6.1",
"@github/markdown-toolbar-element": "2.2.3",
"@github/quote-selection": "2.1.0",
"@github/relative-time-element": "4.4.4",
"@github/relative-time-element": "4.4.5",
"@github/text-expander-element": "2.8.0",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.14.0",
@ -30,14 +30,14 @@
"htmx.org": "1.9.12",
"idiomorph": "0.3.0",
"jquery": "3.7.1",
"katex": "0.16.20",
"katex": "0.16.21",
"mermaid": "11.4.1",
"mini-css-extract-plugin": "2.9.2",
"minimatch": "10.0.1",
"monaco-editor": "0.52.2",
"monaco-editor-webpack-plugin": "7.1.0",
"pdfobject": "2.3.0",
"postcss": "8.4.49",
"postcss": "8.5.1",
"postcss-loader": "8.1.1",
"postcss-nesting": "13.0.1",
"pretty-ms": "9.0.0",
@ -89,7 +89,7 @@
"happy-dom": "16.3.0",
"license-checker-rseidelsohn": "4.4.2",
"markdownlint-cli": "0.43.0",
"postcss-html": "1.7.0",
"postcss-html": "1.8.0",
"stylelint": "16.12.0",
"stylelint-declaration-block-no-ignored-properties": "2.8.0",
"stylelint-declaration-strict-value": "1.10.6",
@ -2854,9 +2854,9 @@
"license": "MIT"
},
"node_modules/@github/relative-time-element": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.4.tgz",
"integrity": "sha512-Oi8uOL8O+ZWLD7dHRWCkm2cudcTYtB3VyOYf9BtzCgDGm+OKomyOREtItNMtWl1dxvec62BTKErq36uy+RYxQg==",
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.5.tgz",
"integrity": "sha512-9ejPtayBDIJfEU8x1fg/w2o5mahHkkp1SC6uObDtoKs4Gn+2a1vNK8XIiNDD8rMeEfpvDjydgSZZ+uk+7N0VsQ==",
"license": "MIT"
},
"node_modules/@github/text-expander-element": {
@ -10402,9 +10402,9 @@
"license": "MIT"
},
"node_modules/katex": {
"version": "0.16.20",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.20.tgz",
"integrity": "sha512-jjuLaMGD/7P8jUTpdKhA9IoqnH+yMFB3sdAFtq5QdAqeP2PjiSbnC3EaguKPNtv6dXXanHxp1ckwvF4a86LBig==",
"version": "0.16.21",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz",
"integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==",
"funding": [
"https://opencollective.com/katex",
"https://github.com/sponsors/katex"
@ -11867,9 +11867,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.49",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
"funding": [
{
"type": "opencollective",
@ -11886,7 +11886,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"nanoid": "^3.3.8",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@ -11895,15 +11895,15 @@
}
},
"node_modules/postcss-html": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-1.7.0.tgz",
"integrity": "sha512-MfcMpSUIaR/nNgeVS8AyvyDugXlADjN9AcV7e5rDfrF1wduIAGSkL4q2+wgrZgA3sHVAHLDO9FuauHhZYW2nBw==",
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-1.8.0.tgz",
"integrity": "sha512-5mMeb1TgLWoRKxZ0Xh9RZDfwUUIqRrcxO2uXO+Ezl1N5lqpCiSU5Gk6+1kZediBfBHFtPCdopr2UZ2SgUsKcgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"htmlparser2": "^8.0.0",
"js-tokens": "^9.0.0",
"postcss": "^8.4.0",
"postcss": "^8.5.0",
"postcss-safe-parser": "^6.0.0"
},
"engines": {

View file

@ -9,7 +9,7 @@
"@citation-js/plugin-software-formats": "0.6.1",
"@github/markdown-toolbar-element": "2.2.3",
"@github/quote-selection": "2.1.0",
"@github/relative-time-element": "4.4.4",
"@github/relative-time-element": "4.4.5",
"@github/text-expander-element": "2.8.0",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.14.0",
@ -29,14 +29,14 @@
"htmx.org": "1.9.12",
"idiomorph": "0.3.0",
"jquery": "3.7.1",
"katex": "0.16.20",
"katex": "0.16.21",
"mermaid": "11.4.1",
"mini-css-extract-plugin": "2.9.2",
"minimatch": "10.0.1",
"monaco-editor": "0.52.2",
"monaco-editor-webpack-plugin": "7.1.0",
"pdfobject": "2.3.0",
"postcss": "8.4.49",
"postcss": "8.5.1",
"postcss-loader": "8.1.1",
"postcss-nesting": "13.0.1",
"pretty-ms": "9.0.0",
@ -88,7 +88,7 @@
"happy-dom": "16.3.0",
"license-checker-rseidelsohn": "4.4.2",
"markdownlint-cli": "0.43.0",
"postcss-html": "1.7.0",
"postcss-html": "1.8.0",
"stylelint": "16.12.0",
"stylelint-declaration-block-no-ignored-properties": "2.8.0",
"stylelint-declaration-strict-value": "1.10.6",

View file

@ -110,34 +110,19 @@
"matchUpdateTypes": ["patch"],
"automerge": true
},
{
"description": "Automerge renovate updates",
"matchDatasources": ["docker"],
"matchPackageNames": ["code.forgejo.org/forgejo-contrib/renovate"],
"matchUpdateTypes": ["minor", "patch", "digest"],
"automerge": true
},
{
"description": "Add reviewer and additional labels to renovate PRs",
"matchDatasources": ["docker"],
"matchPackageNames": ["code.forgejo.org/forgejo-contrib/renovate"],
"matchPackageNames": ["data.forgejo.org/renovate/renovate"],
"reviewers": ["viceice"],
"addLabels": ["forgejo/ci", "test/not-needed"]
},
{
"description": "Update renovate with higher prio to come through rate limit",
"matchDatasources": ["docker"],
"matchPackageNames": ["code.forgejo.org/forgejo-contrib/renovate"],
"extends": ["schedule:weekly"],
"prPriority": 10,
"groupName": "renovate"
},
{
"description": "Disable renovate self-updates for release branches",
"matchBaseBranches": ["/^v\\d+\\.\\d+\\/forgejo$/"],
"matchDatasources": ["docker"],
"matchPackageNames": [
"code.forgejo.org/forgejo-contrib/renovate",
"data.forgejo.org/renovate/renovate",
"ghcr.io/visualon/renovate"
],
"enabled": false

View file

@ -18,8 +18,8 @@ import (
"code.gitea.io/gitea/modules/setting"
gitea_context "code.gitea.io/gitea/services/context"
"github.com/42wim/httpsig"
ap "github.com/go-ap/activitypub"
"github.com/go-fed/httpsig"
)
func getPublicKeyFromResponse(b []byte, keyID *url.URL) (p crypto.PublicKey, err error) {

View file

@ -17,7 +17,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/go-fed/httpsig"
"github.com/42wim/httpsig"
"golang.org/x/crypto/ssh"
)
@ -205,7 +205,7 @@ func doVerify(verifier httpsig.Verifier, sshPublicKeys []ssh.PublicKey) error {
case strings.HasPrefix(publicKey.Type(), "ssh-ed25519"):
algos = []httpsig.Algorithm{httpsig.ED25519}
case strings.HasPrefix(publicKey.Type(), "ssh-rsa"):
algos = []httpsig.Algorithm{httpsig.RSA_SHA1, httpsig.RSA_SHA256, httpsig.RSA_SHA512}
algos = []httpsig.Algorithm{httpsig.RSA_SHA256, httpsig.RSA_SHA512}
}
for _, algo := range algos {
if err := verifier.Verify(cryptoPubkey, algo); err == nil {

View file

@ -40,7 +40,7 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error
repoPath := m.GetRepository(ctx).RepoPath()
// Remove old remote
_, _, err = git.NewCommand(ctx, "remote", "rm").AddDynamicArguments(remoteName).RunStdString(&git.RunOpts{Dir: repoPath})
if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
if err != nil && !git.IsRemoteNotExistError(err) {
return err
}
@ -51,7 +51,7 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error
cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=fetch %s [repo_path: %s]", remoteName, addr, repoPath))
}
_, _, err = cmd.RunStdString(&git.RunOpts{Dir: repoPath})
if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
if err != nil && !git.IsRemoteNotExistError(err) {
return err
}
@ -60,7 +60,7 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error
wikiRemotePath := repo_module.WikiRemoteURL(ctx, addr)
// Remove old remote of wiki
_, _, err = git.NewCommand(ctx, "remote", "rm").AddDynamicArguments(remoteName).RunStdString(&git.RunOpts{Dir: wikiPath})
if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
if err != nil && !git.IsRemoteNotExistError(err) {
return err
}
@ -71,7 +71,7 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error
cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=fetch %s [repo_path: %s]", remoteName, wikiRemotePath, wikiPath))
}
_, _, err = cmd.RunStdString(&git.RunOpts{Dir: wikiPath})
if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
if err != nil && !git.IsRemoteNotExistError(err) {
return err
}
}

View file

@ -8,7 +8,6 @@ import (
"errors"
"fmt"
"net/http"
"strings"
"time"
"code.gitea.io/gitea/models/db"
@ -253,10 +252,10 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error {
cmd := git.NewCommand(ctx, "remote", "rm", "origin")
// if the origin does not exist
_, stderr, err := cmd.RunStdString(&git.RunOpts{
_, _, err := cmd.RunStdString(&git.RunOpts{
Dir: repoPath,
})
if err != nil && !strings.HasPrefix(stderr, "fatal: No such remote") {
if err != nil && !git.IsRemoteNotExistError(err) {
return err
}
return nil
@ -275,7 +274,7 @@ func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo
}
_, _, err := git.NewCommand(ctx, "remote", "rm", "origin").RunStdString(&git.RunOpts{Dir: repoPath})
if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
if err != nil && !git.IsRemoteNotExistError(err) {
return repo, fmt.Errorf("CleanUpMigrateInfo: %w", err)
}

View file

@ -128,9 +128,11 @@
</form>
</div>
</div>
<div id="commit-notes-add-button" class="item">
{{ctx.Locale.Tr "repo.diff.git-notes.add"}}
</div>
{{if not .NoteRendered}}
<div id="commit-notes-add-button" class="item">
{{ctx.Locale.Tr "repo.diff.git-notes.add"}}
</div>
{{end}}
</div>
</div>
{{end}}

View file

@ -85,7 +85,6 @@
diffFileInfo.files.push(...diffDataFiles);
window.config.pageData.diffFileInfo = diffFileInfo;
</script>
<div id="diff-file-list"></div>
{{end}}
<div id="diff-container">
{{if $showFileTree}}

View file

@ -1,7 +1,6 @@
<div class="ui dropdown tiny basic button" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.options_button"}}">
{{svg "octicon-kebab-horizontal"}}
<div class="menu">
<a class="item" id="show-file-list-btn">{{ctx.Locale.Tr "repo.diff.show_diff_stats"}}</a>
{{if .Issue.Index}}
<a class="item" href="{{$.RepoLink}}/pulls/{{.Issue.Index}}.patch" download="{{.Issue.Index}}.patch">{{ctx.Locale.Tr "repo.diff.download_patch"}}</a>
<a class="item" href="{{$.RepoLink}}/pulls/{{.Issue.Index}}.diff" download="{{.Issue.Index}}.diff">{{ctx.Locale.Tr "repo.diff.download_diff"}}</a>

View file

@ -63,10 +63,10 @@
{{$mergedStr:= DateUtils.TimeSince .Issue.PullRequest.MergedUnix}}
{{if .Issue.OriginalAuthor}}
{{.Issue.OriginalAuthor}}
<span class="pull-desc">{{ctx.Locale.TrN .NumCommits "repo.pulls.merged_title_desc_one" "repo.pulls.merged_title_desc_few" .NumCommits $headHref $baseHref $mergedStr}}</span>
<span class="pull-desc">{{ctx.Locale.TrPluralString .NumCommits "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr}}</span>
{{else}}
<a {{if gt .Issue.PullRequest.Merger.ID 0}}href="{{.Issue.PullRequest.Merger.HomeLink}}"{{end}}>{{.Issue.PullRequest.Merger.GetDisplayName}}</a>
<span class="pull-desc">{{ctx.Locale.TrN .NumCommits "repo.pulls.merged_title_desc_one" "repo.pulls.merged_title_desc_few" .NumCommits $headHref $baseHref $mergedStr}}</span>
<span class="pull-desc">{{ctx.Locale.TrPluralString .NumCommits "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr}}</span>
{{end}}
{{if .MadeUsingAGit}}
{{/* TODO: Move documentation link to the instructions at the bottom of the PR, show instructions when clicking label */}}
@ -79,11 +79,11 @@
{{end}}
{{else}}
{{if .Issue.OriginalAuthor}}
<span id="pull-desc-display" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.TrN .NumCommits "repo.pulls.title_desc_one" "repo.pulls.title_desc_few" .NumCommits $headHref $baseHref "branch_target"}}</span>
<span id="pull-desc-display" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.TrPluralString .NumCommits "repo.pulls.title_desc" .NumCommits $headHref $baseHref "branch_target"}}</span>
{{else}}
<span id="pull-desc-display" class="pull-desc">
<a {{if gt .Issue.Poster.ID 0}}href="{{.Issue.Poster.HomeLink}}"{{end}}>{{.Issue.Poster.GetDisplayName}}</a>
{{ctx.Locale.TrN .NumCommits "repo.pulls.title_desc_one" "repo.pulls.title_desc_few" .NumCommits $headHref $baseHref "branch_target"}}
{{ctx.Locale.TrPluralString .NumCommits "repo.pulls.title_desc" .NumCommits $headHref $baseHref "branch_target"}}
</span>
{{end}}
{{if .MadeUsingAGit}}

View file

@ -70,21 +70,13 @@
{{end}}
{{if .IsPull}}
<div class="branches flex-text-inline">
{{svg "gitea-double-chevron-right" 12}}
<div class="branch">
<a href="{{.PullRequest.BaseRepo.Link}}/src/branch/{{PathEscapeSegments .PullRequest.BaseBranch}}">
{{/* inline to remove the spaces between spans */}}
{{if ne .RepoID .PullRequest.BaseRepoID}}<span class="truncated-name">{{.PullRequest.BaseRepo.OwnerName}}</span>:{{end}}<span class="truncated-name">{{.PullRequest.BaseBranch}}</span>
</a>
</div>
{{svg "gitea-double-chevron-left" 12}}
{{if .PullRequest.HeadRepo}}
<div class="branch">
<a href="{{.PullRequest.HeadRepo.Link}}/src/branch/{{PathEscapeSegments .PullRequest.HeadBranch}}">
{{/* inline to remove the spaces between spans */}}
{{if ne .RepoID .PullRequest.HeadRepoID}}<span class="truncated-name">{{.PullRequest.HeadRepo.OwnerName}}</span>:{{end}}<span class="truncated-name">{{.PullRequest.HeadBranch}}</span>
</a>
</div>
{{end}}
</div>
{{end}}
{{if and .Milestone (ne $.listType "milestone")}}

View file

@ -7,6 +7,7 @@
<div class="ui small fluid action input">
{{template "shared/search/input" dict "Value" .Value "Disabled" .Disabled "Placeholder" .Placeholder}}
<div class="ui small dropdown selection {{if .Disabled}} disabled{{end}}" data-tooltip-content="{{ctx.Locale.Tr "search.type_tooltip"}}">
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="text">
{{ctx.Locale.Tr (printf "search.%s" .Selected)}}
</div>

View file

@ -71,4 +71,5 @@ test('workflow dispatch box not available for unauthenticated users', async ({pa
await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
await expect(page.locator('body')).not.toContainText(workflow_trigger_notification_text);
await save_visual(page);
});

View file

@ -8,7 +8,7 @@
// @watch end
import {expect} from '@playwright/test';
import {test} from './utils_e2e.ts';
import {save_visual, test} from './utils_e2e.ts';
test('copy src file path to clipboard', async ({page}, workerInfo) => {
test.skip(['Mobile Safari', 'webkit'].includes(workerInfo.project.name), 'Apple clipboard API addon - starting at just $499!');
@ -19,6 +19,7 @@ test('copy src file path to clipboard', async ({page}, workerInfo) => {
await page.click('[data-clipboard-text]');
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardText).toContain('README.md');
await save_visual(page);
});
test('copy diff file path to clipboard', async ({page}, workerInfo) => {
@ -30,4 +31,6 @@ test('copy diff file path to clipboard', async ({page}, workerInfo) => {
await page.click('[data-clipboard-text]');
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardText).toContain('README.md');
await expect(page.getByText('Copied')).toBeVisible();
await save_visual(page);
});

View file

@ -3,7 +3,7 @@
// @watch end
import {expect} from '@playwright/test';
import {save_visual, test} from './utils_e2e.ts';
import {test} from './utils_e2e.ts';
test.use({user: 'user2'});
@ -23,5 +23,6 @@ test('Correct link and tooltip', async ({page}, testInfo) => {
const repoStatus = page.locator('.dashboard-repos .repo-owner-name-list > li:nth-child(1) > a:nth-child(2)');
await expect(repoStatus).toHaveAttribute('href', '/user2/test_workflows/actions', {timeout: 10000});
await expect(repoStatus).toHaveAttribute('data-tooltip-content', /^(Error|Failure)$/);
await save_visual(page);
// ToDo: Ensure stable screenshot of dashboard. Known to be flaky: https://code.forgejo.org/forgejo/visual-browser-testing/commit/206d4cfb7a4af6d8d7043026cdd4d63708798b2a
// await save_visual(page);
});

View file

@ -82,6 +82,7 @@ func TestE2e(t *testing.T) {
runArgs := []string{"npx", "playwright", "test"}
_, testVisual := os.LookupEnv("VISUAL_TEST")
// To update snapshot outputs
if _, set := os.LookupEnv("ACCEPT_VISUAL"); set {
runArgs = append(runArgs, "--update-snapshots")
@ -105,6 +106,10 @@ func TestE2e(t *testing.T) {
onForgejoRun(t, func(*testing.T, *url.URL) {
defer DeclareGitRepos(t)()
thisTest := runArgs
// when all tests are run, use unique artifacts directories per test to preserve artifacts from other tests
if testVisual {
thisTest = append(thisTest, "--output=tests/e2e/test-artifacts/"+testname)
}
thisTest = append(thisTest, path)
cmd := exec.Command(runArgs[0], thisTest...)
cmd.Env = os.Environ()
@ -114,7 +119,7 @@ func TestE2e(t *testing.T) {
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
if err != nil && !testVisual {
log.Fatal("Playwright Failed: %s", err)
}
})

View file

@ -5,7 +5,7 @@
// @watch end
import {expect} from '@playwright/test';
import {test} from './utils_e2e.ts';
import {save_visual, test} from './utils_e2e.ts';
test('Load Homepage', async ({page}) => {
const response = await page.goto('/');
@ -26,6 +26,7 @@ test('Register Form', async ({page}, workerInfo) => {
expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);
await expect(page.locator('.secondary-nav span>img.ui.avatar')).toBeVisible();
await expect(page.locator('.ui.positive.message.flash-success')).toHaveText('Account was successfully created. Welcome!');
await save_visual(page);
});
// eslint-disable-next-line playwright/no-skipped-test

View file

@ -7,7 +7,7 @@
// @watch end
import {expect} from '@playwright/test';
import {test} from './utils_e2e.ts';
import {save_visual, test} from './utils_e2e.ts';
test('Explore view taborder', async ({page}) => {
await page.goto('/explore/repos');
@ -42,4 +42,5 @@ test('Explore view taborder', async ({page}) => {
}
}
expect(res).toBe(exp);
await save_visual(page);
});

View file

@ -8,6 +8,9 @@ test('Change git note', async ({page}) => {
let response = await page.goto('/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d');
expect(response?.status()).toBe(200);
// An add button should not be present, because the commit already has a commit note
await expect(page.locator('#commit-notes-add-button')).toHaveCount(0);
await page.locator('#commit-notes-edit-button').click();
let textarea = page.locator('textarea[name="notes"]');

View file

@ -77,6 +77,27 @@ test('Always focus edit tab first on edit', async ({page}) => {
await save_visual(page);
});
test('Reset content of comment edit field on cancel', async ({page}) => {
const response = await page.goto('/user2/repo1/issues/1');
expect(response?.status()).toBe(200);
const editorTextarea = page.locator('[id="_combo_markdown_editor_1"]');
// Change the content of the edit field
await page.click('#issue-1 .comment-container .context-menu');
await page.click('#issue-1 .comment-container .menu>.edit-content');
await expect(editorTextarea).toHaveValue('content for the first issue');
await editorTextarea.fill('some random string');
await expect(editorTextarea).toHaveValue('some random string');
await page.click('#issue-1 .comment-container .edit .cancel');
// Edit again and assert that the edit field should be reset to the initial content
await page.click('#issue-1 .comment-container .context-menu');
await page.click('#issue-1 .comment-container .menu>.edit-content');
await expect(editorTextarea).toHaveValue('content for the first issue');
await save_visual(page);
});
test('Quote reply', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name !== 'firefox', 'Uses Firefox specific selection quirks');
const response = await page.goto('/user2/repo1/issues/1');

View file

@ -3,7 +3,7 @@
// @watch end
import {expect} from '@playwright/test';
import {test} from './utils_e2e.ts';
import {save_visual, test} from './utils_e2e.ts';
test('markup with #xyz-mode-only', async ({page}) => {
const response = await page.goto('/user2/repo1/issues/1');
@ -13,4 +13,5 @@ test('markup with #xyz-mode-only', async ({page}) => {
await expect(comment).toBeVisible();
await expect(comment.locator('[src$="#gh-light-mode-only"]')).toBeVisible();
await expect(comment.locator('[src$="#gh-dark-mode-only"]')).toBeHidden();
await save_visual(page);
});

View file

@ -49,6 +49,7 @@ test('Line Range Selection', async ({page}) => {
// out-of-bounds end line
await page.goto(`${filePath}#L1-L100`);
await assertSelectedLines(page, ['1', '2', '3']);
await save_visual(page);
});
test('Readable diff', async ({page}, workerInfo) => {
@ -75,6 +76,7 @@ test('Readable diff', async ({page}, workerInfo) => {
await expect(page.getByText(thisDiff.added, {exact: true})).toHaveCSS('background-color', 'rgb(134, 239, 172)');
}
}
await save_visual(page);
});
test.describe('As authenticated user', () => {

View file

@ -5,7 +5,7 @@
// @watch end
import {expect} from '@playwright/test';
import {test} from './utils_e2e.ts';
import {save_visual, test} from './utils_e2e.ts';
test('Commit graph overflow', async ({page}) => {
await page.goto('/user2/diff-test/graph');
@ -28,4 +28,5 @@ test('Switch branch', async ({page}) => {
await expect(page.locator('#loading-indicator')).toBeHidden();
await expect(page.locator('#rel-container')).toBeVisible();
await expect(page.locator('#rev-container')).toBeVisible();
await save_visual(page);
});

View file

@ -21,7 +21,6 @@ test('Migration Progress Page', async ({page, browser}, workerInfo) => {
await form.locator('button.primary').click({timeout: 5000});
await expect(page).toHaveURL('user2/invalidrepo');
await save_visual(page);
// page screenshot of unauthenticatedPage is checked automatically after the test
const ctx = await test_context(browser);
const unauthenticatedPage = await ctx.newPage();
@ -37,4 +36,6 @@ test('Migration Progress Page', async ({page, browser}, workerInfo) => {
await save_visual(page);
await deleteModal.getByRole('button', {name: 'Delete repository'}).click();
await expect(page).toHaveURL('/');
// checked last to preserve the order of screenshots from first run
await save_visual(unauthenticatedPage);
});

View file

@ -4,7 +4,7 @@
// @watch end
import {expect} from '@playwright/test';
import {test} from './utils_e2e.ts';
import {save_visual, test} from './utils_e2e.ts';
for (const searchTerm of ['space', 'consectetur']) {
for (const width of [null, 2560, 4000]) {
@ -23,6 +23,7 @@ for (const searchTerm of ['space', 'consectetur']) {
await page.getByPlaceholder('Search wiki').dispatchEvent('keyup');
// timeout is necessary because HTMX search could be slow
await expect(page.locator('#wiki-search a[href]')).toBeInViewport({ratio: 1});
await save_visual(page);
});
}
}
@ -36,4 +37,5 @@ test(`Search results show titles (and not file names)`, async ({page}, workerInf
// so we manually "type" the last letter
await page.getByPlaceholder('Search wiki').dispatchEvent('keyup');
await expect(page.locator('#wiki-search a[href] b')).toHaveText('Page With Spaced Name');
await save_visual(page);
});

View file

@ -5,7 +5,7 @@
// @watch end
import {expect} from '@playwright/test';
import {test} from './utils_e2e.ts';
import {save_visual, test} from './utils_e2e.ts';
test.describe('desktop viewport as user 2', () => {
test.use({user: 'user2', viewport: {width: 1920, height: 300}});
@ -54,6 +54,7 @@ test.describe('desktop viewport, unauthenticated', () => {
await expect(page.locator('.overflow-menu-items>#settings-btn')).toHaveCount(0);
await expect(page.locator('.overflow-menu-button')).toHaveCount(0);
await save_visual(page);
});
});
@ -78,6 +79,7 @@ test.describe('small viewport', () => {
const items = shownItems.concat(overflowItems);
expect(Array.from(new Set(items))).toHaveLength(items.length);
await save_visual(page);
});
test('Settings button in overflow menu of org header', async ({page}) => {
@ -121,5 +123,6 @@ test.describe('small viewport, unauthenticated', () => {
const items = shownItems.concat(overflowItems);
expect(Array.from(new Set(items))).toHaveLength(items.length);
await save_visual(page);
});
});

View file

@ -26,15 +26,6 @@ export const test = baseTest.extend<TestOptions>({
},
user: null,
authScope: 'shared',
// see https://playwright.dev/docs/test-fixtures#adding-global-beforeeachaftereach-hooks
forEachTest: [async ({page}, use) => {
await use();
// some tests create a new page which is not yet available here
// only operate on tests that make the URL available
if (page.url() !== 'about:blank') {
await save_visual(page);
}
}, {auto: true}],
});
export async function test_context(browser: Browser, options?: BrowserContextOptions) {
@ -128,6 +119,7 @@ export async function save_visual(page: Page) {
// update order of recently created repos is not fully deterministic
page.locator('.flex-item-main').filter({hasText: 'relative time in repo'}),
page.locator('#activity-feed'),
page.locator('#user-heatmap'),
// dynamic IDs in fixed-size inputs
page.locator('input[value*="dyn-id-"]'),
],

View file

@ -27,6 +27,7 @@ var renderContext = markup.RenderContext{
func FuzzMarkdownRenderRaw(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
setting.IsInTesting = true
setting.AppURL = "http://localhost:3000/"
markdown.RenderRaw(&renderContext, bytes.NewReader(data), io.Discard)
})
@ -34,6 +35,7 @@ func FuzzMarkdownRenderRaw(f *testing.F) {
func FuzzMarkupPostProcess(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
setting.IsInTesting = true
setting.AppURL = "http://localhost:3000/"
markup.PostProcess(&renderContext, bytes.NewReader(data), io.Discard)
})

View file

@ -15,7 +15,7 @@ import (
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"
"github.com/go-fed/httpsig"
"github.com/42wim/httpsig"
"golang.org/x/crypto/ssh"
)

145
tools/migrate_locales.sh Executable file
View file

@ -0,0 +1,145 @@
#!/bin/bash
# Copyright 2024 The Forgejo Authors. All rights reserved.
# SPDX-License-Identifier: MIT
if [ -z "$1" ] || [ -z "$2" ]
then
echo "USAGE: $0 section key [key1 [keyN]]"
exit 1
fi
if ! [ -d ../options/locale_next ]
then
echo 'Call this script from the `tools` directory.'
exit 1
fi
destsection="$1"
keyJSON="$destsection.$2"
key1=""
keyN=""
if [ -n "$3" ]
then
key1="$3"
else
key1="$2"
fi
if [ -n "$4" ]
then
keyN="$4"
fi
cd ../options/locale
# Migrate the string in one file.
function process() {
file="$1"
exec 3<$file
val1=""
valN=""
cursection=""
line1=0
lineN=0
lineNumber=0
# Parse the file
while read -u 3 line
do
((++lineNumber))
if [[ $line =~ ^\[[-._a-zA-Z0-9]+\]$ ]]
then
cursection="${line#[}"
cursection="${cursection%]}"
elif [ "$cursection" = "$destsection" ]
then
key="${line%%=*}"
value="${line#*=}"
key="$(echo $key)" # Trim leading/trailing whitespace
value="$(echo $value)"
if [ "$key" = "$key1" ]
then
val1="$value"
line1=$lineNumber
fi
if [ -n "$keyN" ] && [ "$key" = "$keyN" ]
then
valN="$value"
lineN=$lineNumber
fi
if [ -n "$val1" ] && ( [ -n "$valN" ] || [ -z "$keyN" ] )
then
# Found all desired strings
break
fi
fi
done
if [ -n "$val1" ] || [ -n "$valN" ]
then
localename="${file#locale_}"
localename="${localename%.ini}"
localename="${localename%-*}"
if [ "$file" = "locale_en-US.ini" ]
then
# Delete migrated string from source file
if [ $line1 -gt 0 ] && [ $lineN -gt 0 ] && [ $lineN -ne $line1 ]
then
sed -i "${line1}d;${lineN}d" "$file"
elif [ $line1 -gt 0 ]
then
sed -i "${line1}d" "$file"
elif [ $lineN -gt 0 ]
then
sed -i "${lineN}d" "$file"
fi
fi
# Write JSON
jsonfile="../locale_next/${file/.ini/.json}"
pluralform="other"
oneform="one"
case $localename in
"be" | "bs" | "cnr" | "csb" | "hr" | "lt" | "pl" | "ru" | "sr" | "szl" | "uk" | "wen")
# These languages have no "other" form and use "many" instead.
pluralform="many"
;;
"ace" | "ay" | "bm" | "bo" | "cdo" | "cpx" | "crh" | "dz" | "gan" | "hak" | "hnj" | "hsn" | "id" | "ig" | "ii" | "ja" | "jbo" | "jv" | "kde" | "kea" | "km" | "ko" | "kos" | "lkt" | "lo" | "lzh" | "ms" | "my" | "nan" | "nqo" | "osa" | "sah" | "ses" | "sg" | "son" | "su" | "th" | "tlh" | "to" | "tok" | "tpi" | "tt" | "vi" | "wo" | "wuu" | "yo" | "yue" | "zh")
# These languages have no singular form.
oneform=""
;;
*)
;;
esac
content=""
if [ -z "$keyN" ]
then
content="$(jq --arg val "$val1" ".$keyJSON = \$val" < "$jsonfile")"
else
object='{}'
if [ -n "$val1" ] && [ -n "$oneform" ]
then
object=$(jq --arg val "$val1" ".$oneform = \$val" <<< "$object")
fi
if [ -n "$valN" ]
then
object=$(jq --arg val "$valN" ".$pluralform = \$val" <<< "$object")
fi
content="$(jq --argjson val "$object" ".$keyJSON = \$val" < "$jsonfile")"
fi
jq . <<< "$content" > "$jsonfile"
fi
}
for file in *.ini
do
process "$file" &
done
wait

View file

@ -1709,26 +1709,6 @@ td .commit-summary {
max-width: initial; /* remove fomantic over 100% width */
}
.repository .diff-stats {
clear: both;
margin-bottom: 5px;
max-height: 200px;
height: fit-content;
overflow: auto;
padding-left: 0;
}
.repository .diff-stats li {
list-style: none;
padding-bottom: 4px;
margin-bottom: 4px;
padding-left: 6px;
}
.repository .diff-stats li + li {
border-top: 1px solid var(--color-secondary);
}
.repository .repo-search-result {
padding-top: 10px;
padding-bottom: 10px;
@ -1839,10 +1819,6 @@ details.repo-search-result summary::marker {
color: var(--color-success-text);
}
.repository .ui.attached.isSigned.isVerified.message .pull-right {
color: var(--color-text);
}
.repository .ui.attached.isSigned.isVerified.message .ui.text {
color: var(--color-success-text);
}

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