Template
1
0
Fork 0
mirror of https://codeberg.org/forgejo/forgejo synced 2024-11-22 01:44:24 +01:00

Compare commits

..

102 commits

Author SHA1 Message Date
James Hatfield 16d06705b3 feat: add setting to block disposable emails
- Add a new setting `EMAIL_DOMAIN_BLOCK_DISPOSABLE` that will append a list of
  domains that are known for being used by temporary or disposable email
  services.

- Add a utility to automatically download and format the list of domains from
  the disposable-email-domains project on github.

  (https://github.com/disposable-email-domains/disposable-email-domains)
  license: CC0 1.0 Universal (CC0 1.0) [Public Domain]

  from README:
  """
  This repo contains a list of disposable and temporary email address domains often used to register dummy users in order to spam or abuse some services.

  We cannot guarantee all of these can still be considered disposable but we do basic checking so chances are they were disposable at one point in time.
  """
2024-11-20 23:17:37 -06:00
Earl Warren 7015bdfa48 Merge pull request 'chore(ci): remove unused experimental DNS updates' (#6034) from earl-warren/forgejo:wip-dns-update into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6034
Reviewed-by: Michael Kriese <michael.kriese@gmx.de>
2024-11-20 15:45:12 +00:00
Earl Warren a69943085a
chore(ci): remove unused experimental DNS updates 2024-11-20 15:13:53 +00:00
JakobDev 45fa9e5ae9 fix: Allow Organisations to remove the Email Address (#5517)
It is possible to set a Email for a Organization. This Email is optional and only used to be displayed on the profile page. However, once you set an EMail, you can no longer remove it. This PR fixes that.

While working on the tests, I found out, that the API returns a 500 when trying to set an invalid EMail. I fixed that too. It returns a 422 now.

Fixes #4567

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5517
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Reviewed-by: Otto <otto@codeberg.org>
Co-authored-by: JakobDev <jakobdev@gmx.de>
Co-committed-by: JakobDev <jakobdev@gmx.de>
2024-11-20 12:31:34 +00:00
0ko 1316f4d338 Merge pull request 'Fix regression from #4753' (#6029) from JakobDev/forgejo:notefix into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6029
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
2024-11-19 19:23:56 +00:00
JakobDev f4c70a3c43
Fix regression from #4753 2024-11-19 19:17:46 +01:00
Earl Warren 3674e90c93 Merge pull request 'Improve git notes UI' (#6025) from 0ko/forgejo:ui-notes-followup into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6025
Reviewed-by: Michael Kriese <michael.kriese@gmx.de>
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
2024-11-19 07:09:26 +00:00
Earl Warren 0a39ee3bbe Merge pull request 'chore(release-notes-assistant): security fix / features come first' (#6003) from earl-warren/forgejo:wip-release-security into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6003
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
2024-11-19 06:32:48 +00:00
Earl Warren 8636a8b228 Merge pull request 'Update dependency chartjs-plugin-zoom to v2.1.0 (forgejo)' (#6023) from renovate/forgejo-chartjs-plugin-zoom-2.x into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6023
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
2024-11-19 06:05:22 +00:00
0ko f5c0570533 ui: improve git notes 2024-11-19 10:27:57 +05:00
Renovate Bot b7c15f7b70 Update dependency chartjs-plugin-zoom to v2.1.0 2024-11-19 00:03:11 +00:00
Earl Warren 298863c701 Merge pull request 'Don't display email in profile settings when hidden' (#6018) from 0ko/forgejo:ui-settings-email-vis into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6018
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Reviewed-by: Otto <otto@codeberg.org>
2024-11-18 23:06:52 +00:00
JakobDev f90928507a [FEAT]Allow changing git notes (#4753)
Git has a cool feature called git notes. It allows adding a text to a commit without changing the commit itself. Forgejo already displays git notes. With this PR you can also now change git notes.

<details>
<summary>Screenshots</summary>

![grafik](/attachments/53a9546b-c4db-4b07-92ae-eb15b209b21d)
![grafik](/attachments/1bd96f2c-6178-45d2-93d7-d19c7cbe5898)
![grafik](/attachments/9ea73623-25d1-4628-a43f-f5ecbd431788)
![grafik](/attachments/efea0c9e-43c6-4441-bb7e-948177bf9021)

</details>

## Checklist

The [developer guide](https://forgejo.org/docs/next/developer/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Tests

- I added test coverage for Go changes...
  - [ ] in their respective `*_test.go` for unit tests.
  - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I added test coverage for JavaScript changes...
  - [ ] in `web_src/js/*.test.js` if it can be unit tested.
  - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)).

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [ ] I do not want this change to show in the release notes.
- [x] I want the title to show in the release notes with a link to this pull request.
- [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title.

<!--start release-notes-assistant-->

## Release notes
<!--URL:https://codeberg.org/forgejo/forgejo-->
- Features
  - [PR](https://codeberg.org/forgejo/forgejo/pulls/4753): <!--number 4753 --><!--line 0 --><!--description QWxsb3cgY2hhbmdpbmcgZ2l0IG5vdGVz-->Allow changing git notes<!--description-->
<!--end release-notes-assistant-->

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/4753
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: JakobDev <jakobdev@gmx.de>
Co-committed-by: JakobDev <jakobdev@gmx.de>
2024-11-18 22:56:17 +00:00
Earl Warren 71ff98d61d Merge pull request 'Update help links on page with no workflows' (#5697) from kwonunn/fix-empty-actions-page into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5697
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
2024-11-18 21:50:31 +00:00
Marco De Araujo 1f4be5baad Escaping specific markdown in commit messages on Discord-type embeds #3664 (#5811)
Co-authored-by: Marco De Araujo <marco.araujo.junior@gmail.com>
Co-committed-by: Marco De Araujo <marco.araujo.junior@gmail.com>
2024-11-18 21:47:11 +00:00
Earl Warren cff7754735 Merge pull request 'ci: disable postgresql fsync' (#5962) from viceice/ci/pg/fsync into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5962
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
2024-11-18 21:43:15 +00:00
Earl Warren 0fb3e45ca5 Merge pull request 'chore(ci): merge jobs issue label jobs in one workflow' (#6021) from earl-warren/forgejo:wip-ci-labels into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6021
Reviewed-by: Michael Kriese <michael.kriese@gmx.de>
2024-11-18 20:33:38 +00:00
Earl Warren 1806db31d1
chore(ci): merge jobs in issue-labels.yml in one workflow
Fixes: forgejo/forgejo#5999
2024-11-18 19:43:35 +01:00
Earl Warren 7c1f3a7594
chore(ci): cat all issue labels workflows in issue-labels.yml
cat cascade-setup-end-to-end.yml backport.yml merge-requirements.yml release-notes-assistant.yml > issue-labels.yml
rm cascade-setup-end-to-end.yml backport.yml merge-requirements.yml release-notes-assistant.yml
2024-11-18 19:40:15 +01:00
Earl Warren 3c3c9b22a9 Merge pull request 'chore(ci): make release-notes-assistant job copy/pastable (part two)' (#6020) from earl-warren/forgejo:wip-ci-labels into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6020
Reviewed-by: Michael Kriese <michael.kriese@gmx.de>
2024-11-18 18:10:13 +00:00
Earl Warren 73cb6c9204
chore(ci): make release-notes-assistant job copy/pastable (part two)
The event is pull_request_target

Refs: forgejo/forgejo#5999
2024-11-18 18:11:07 +01:00
Earl Warren 25354c03a5 Merge pull request 'chore(ci): make release-notes-assistant job copy/pastable' (#6019) from earl-warren/forgejo:wip-ci-labels into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6019
Reviewed-by: Michael Kriese <michael.kriese@gmx.de>
2024-11-18 17:02:59 +00:00
Earl Warren 18cecf124f
chore(ci): make release-notes-assistant job copy/pastable
Refs: forgejo/forgejo#5999
2024-11-18 17:23:33 +01:00
Earl Warren 2f1b1d8b80 Merge pull request 'feat: use oci mirror for tonistiigi/xx image' (#5965) from viceice/feat/oci-mirror into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5965
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
2024-11-18 14:54:05 +00:00
0ko c3653e0eaa ui: don't display email in profile settings when hidden 2024-11-18 17:06:38 +05:00
Earl Warren 4a3f8f3004 Merge pull request 'fix(test): TestGitAttributeCheckerError must allow broken pipe' (#6013) from earl-warren/forgejo:wip-ci-cancel into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6013
Reviewed-by: oliverpool <oliverpool@noreply.codeberg.org>
2024-11-18 11:28:52 +00:00
Otto 6cfaebf043 Merge pull request 'chore(ci): make backporting job copy/pastable' (#6002) from earl-warren/forgejo:wip-ci-labels-backports into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6002
Reviewed-by: Otto <otto@codeberg.org>
2024-11-18 10:44:38 +00:00
Earl Warren e0b37253c8 Merge pull request 'Lock file maintenance (forgejo)' (#6010) from renovate/forgejo-lock-file-maintenance into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6010
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
2024-11-18 07:24:40 +00:00
Earl Warren b9697f5227
fix(test): TestGitAttributeCheckerError must allow broken pipe
Early cancelation can lead to two kinds of error. Either canceled or
broken pipe, depending on when the goroutine stops.

Fixes: forgejo/forgejo#6012
2024-11-18 08:20:10 +01:00
Renovate Bot 6553148de9 Update renovate to v39.19.1 (forgejo) (#6008)
Co-authored-by: Renovate Bot <forgejo-renovate-action@forgejo.org>
Co-committed-by: Renovate Bot <forgejo-renovate-action@forgejo.org>
2024-11-18 05:52:58 +00:00
Renovate Bot 387ed7e072 Lock file maintenance 2024-11-18 00:05:26 +00:00
Otto 289c22cc4a Merge pull request 'fix: vertical center the date on GPG keys' (#6006) from gusted/forgejo-ui-gpg into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6006
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Reviewed-by: Otto <otto@codeberg.org>
2024-11-17 23:17:19 +00:00
Renovate Bot 4163402f5e Update dependency vue to v3.5.13 (forgejo) (#5981)
Co-authored-by: Renovate Bot <forgejo-renovate-action@forgejo.org>
Co-committed-by: Renovate Bot <forgejo-renovate-action@forgejo.org>
2024-11-17 21:23:23 +00:00
Gusted 662e9c53d9
fix: vertical center the date on GPG keys
- Ensure the 'added on' and the date are centered vertically the same.
- Regression #5796
2024-11-17 21:53:37 +01:00
Gusted e31090cf4b Merge pull request 'fix: check read permissions for code owner review requests' (#5996) from gusted/forgejo-codeowners into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5996
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
2024-11-17 19:13:46 +00:00
Gusted 2cc3278791 Merge pull request 'fix: use better code to group UID and stopwatches' (#5989) from gusted/improve-uid-stopwatches into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5989
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
2024-11-17 19:13:30 +00:00
Gusted 693f7731f9
fix: check read permissions for code owner review requests
- Only send a review request based on the code owner file if the code
owner user has read permissions to the pull requests of that repository.
- This avoids leaking title of PRs from private repository when a
CODEOWNER file is present which contains users that do not have access
to the private repository.
- Found by @oliverpool.
- Integration test added.
2024-11-17 20:12:59 +01:00
Earl Warren 02f4d3bd2d
chore(release-notes-assistant): security fix / features come first 2024-11-17 20:03:11 +01:00
Earl Warren b6869d643e
chore(ci): make backporting job copy/pastable
Refs: forgejo/forgejo#5999
2024-11-17 19:17:11 +01:00
Earl Warren abec2442b7 Merge pull request 'chore(ci): make merge-conditions job copy/pastable' (#6001) from earl-warren/forgejo:wip-ci-labels into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6001
Reviewed-by: Michael Kriese <michael.kriese@gmx.de>
2024-11-17 18:13:16 +00:00
Earl Warren 64a89c8d33
chore(ci): make merge-conditions job copy/pastable
Refs: forgejo/forgejo#5999
2024-11-17 17:57:40 +01:00
Gusted 76f172b080 Merge pull request 'fix: remember fuzzy for open/close state' (#5995) from gusted/forgejo-remember-fuzzy into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5995
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Reviewed-by: Shiny Nematoda <snematoda@noreply.codeberg.org>
2024-11-17 15:27:48 +00:00
Earl Warren a5363a539b Merge pull request 'chore(ci): make end-to-end job copy/pastable' (#6000) from earl-warren/forgejo:wip-ci-labels into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6000
Reviewed-by: Michael Kriese <michael.kriese@gmx.de>
2024-11-17 13:33:43 +00:00
Earl Warren b5161325ef
chore(ci): make end-to-end job copy/pastable
Refs: forgejo/forgejo#5999
2024-11-17 13:33:21 +01:00
Gusted 9701e5e0ff
fix: remember fuzzy for open/close state
- Remember if fuzzy was set or not for the open/close/all states.
- Use `fuzzy=false` for test, as `fuzzy=true` is the default (this is
the opposite of all the other values).
- Remove `ctx.Link` prefix for open/close states, this makes them
suspectible to the existing tests (the other filter links are also in
the format of simply having `?xx=xx&yy=yy`).
- Fix typo in test name.
2024-11-17 02:06:51 +01:00
Gusted da40383cf4 Revert defaulting to EdDSA
- Apparently JWT actually checks when doing a JWT operation if the key type is valid and not on startup, this caused errors unfortunately.
2024-11-17 00:42:31 +00:00
Gusted 8e94947ed9 Merge pull request 'fix: api repo compare with commit hashes' (#5991) from angelnu/forgejo:angelnu/IsCommitExist into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5991
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
2024-11-16 23:35:34 +00:00
Gusted c01a03e93d Merge pull request 'feat: default to generating EdDSA for OAuth JWT signing key' (#5987) from gusted/forgejo-default-eddsa-oauth-jwt into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5987
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
2024-11-16 23:34:58 +00:00
Angel Nunez Mencias ca0cd42d7a
simplify test based on feedback 2024-11-16 22:31:14 +01:00
Angel Nunez Mencias 01c9c19536
fmt 2024-11-16 18:12:40 +01:00
angelnu 1b9d1240eb
add test 2024-11-16 18:12:40 +01:00
angelnu d2dc4fae3a
review changes 2024-11-16 18:12:40 +01:00
angelnu e434ecdaca
check IsCommitExist 2024-11-16 18:12:40 +01:00
Earl Warren 569a67327c Merge pull request 'bug: correctly generate oauth2 jwt signing key' (#5986) from gusted/improve-oauth2-jwt into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5986
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
2024-11-16 17:04:31 +00:00
Gusted 146824badc Merge pull request 'feat: improve GetLatestCommitStatusForPairs' (#5983) from gusted/improve-commit-pairs into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5983
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Reviewed-by: Otto <otto@codeberg.org>
2024-11-16 15:54:40 +00:00
Earl Warren eaa66f85f6 Merge pull request '[gitea] week 2024-46 cherry pick (gitea/main -> forgejo)' (#5988) from earl-warren/wcp/2024-46 into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5988
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
2024-11-16 15:49:01 +00:00
Gusted e4eb82b738
fix: use better code to group UID and stopwatches
- Instead of having code that relied on the result being sorted (which
wasn't specified in the query and therefore not safe to assume so). Use
a map where it doesn't care if the result that we get from the database
is sorted or not.
- Added unit test.
2024-11-16 15:59:02 +01:00
Earl Warren 969a6ab24a
chore(release-notes): notes for the week 2024-46 weekly cherry pick 2024-11-16 15:25:37 +01:00
Gusted 7d59060dc6
bug: correctly generate oauth2 jwt signing key
- When RS256, RS384, ES384, ES512 was specified as the JWT signing
algorithm they would generate RS512 and ES256 respectively.
- Added unit test.
2024-11-16 15:17:19 +01:00
silverwind 308812a82e
Fix mermaid diagram height when initially hidden (#32457)
In a hidden iframe, `document.body.clientHeight` is not reliable. Use
`IntersectionObserver` to detect the visibility change and update the
height there.

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

<img width="885" alt="image"
src="https://github.com/user-attachments/assets/a95ef6aa-27e7-443f-9d06-400ef27919ae">

(cherry picked from commit b55a31eb6a894feb5508e350ff5e9548b2531bd6)
2024-11-16 15:12:25 +01:00
Zettat123 fc26becba4
Fix broken releases when re-pushing tags (#32435)
Fix #32427

(cherry picked from commit 35bcd667b23de29a7b0d0bf1090fb10961d3aca3)

Conflicts:
	- tests/integration/repo_tag_test.go
	  Resolved by manually copying the added test, and also manually
	  adjusting the imported Go modules.
2024-11-16 15:12:25 +01:00
Gusted 02a2dbef69
feat: default to generating EdDSA for OAuth JWT signing key 2024-11-16 15:03:28 +01:00
Lunny Xiao 013cc1dee4
Only query team tables if repository is under org when getting assignees (#32414)
It's unnecessary to query the team table if the repository is not under
organization when getting assignees.

(cherry picked from commit 1887c75c35c1d16372b1dbe2b792e374b558ce1f)
2024-11-16 14:57:11 +01:00
Gusted 6d0f2c1b82 Merge pull request 'Update module google.golang.org/grpc to v1.68.0 (forgejo)' (#5969) from renovate/forgejo-google.golang.org-grpc-1.x into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5969
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
2024-11-16 12:25:41 +00:00
Gusted 2cccc02e76
feat: improve GetLatestCommitStatusForPairs
- Simplify the function into a single SQL query. This may or may not
help with a monster query we are seeing in Codeberg that is using 400MiB
and takes 50MiB to simply log the query. The result is now capped to the
actual latest index,
- Add unit test.
2024-11-16 13:23:40 +01:00
Earl Warren 356aa6521b Merge pull request 'fix: extend forgejo_auth_token table (part two)' (#5984) from earl-warren/forgejo:wip-forgejo-auth-token into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5984
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
2024-11-16 11:56:02 +00:00
Earl Warren cf323a3d55
fix: extend forgejo_auth_token table (part two)
Add the default value of the purpose field to both the table and the
migration. The table in v9 and v7 backport already have the default
value.

ALTER TABLE `forgejo_auth_token` ADD `purpose` TEXT NOT NULL [] - Cannot add a NOT NULL column with default value NULL
2024-11-16 10:53:46 +01:00
Gusted 6bab3c374c Merge pull request 'Update github.com/grafana/go-json digest to f14426c (forgejo)' (#5980) from renovate/forgejo-github.com-grafana-go-json-digest into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5980
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
2024-11-16 03:07:17 +00:00
Gusted 570e8cec9e Merge pull request 'Update dependency tailwindcss to v3.4.15 (forgejo)' (#5966) from renovate/forgejo-tailwindcss-3.x into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5966
Reviewed-by: Michael Kriese <michael.kriese@gmx.de>
2024-11-16 02:23:37 +00:00
Michael Kriese bf810fa8d3 Merge pull request 'ci: upload all e2e artifacts' (#5973) from viceice/ci/e2e-artifacts into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5973
2024-11-16 00:34:34 +00:00
Renovate Bot 66dfb2813c Update github.com/grafana/go-json digest to f14426c 2024-11-16 00:03:23 +00:00
Earl Warren 95a8987844 Merge pull request 'chore(release-notes): fix the v9.0.2 links' (#5978) from earl-warren/forgejo:wip-release-notes into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5978
Reviewed-by: Michael Kriese <michael.kriese@gmx.de>
2024-11-15 22:48:55 +00:00
Earl Warren 9fd2df6e30
chore(release-notes): fix the v9.0.2 links 2024-11-15 22:59:52 +01:00
Michael Kriese 7f707b2a6f
ci: disable postgresql fsync 2024-11-15 15:29:06 +01:00
Michael Kriese 5406310f3e
ci: upload all e2e artifacts 2024-11-15 15:01:39 +01:00
Michael Kriese b21cc70dd7 Merge pull request 'chore: fix e2e' (#5977) from gusted/fix-e2e into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5977
Reviewed-by: Michael Kriese <michael.kriese@gmx.de>
2024-11-15 13:33:50 +00:00
Gusted 4a5d9d4b78
chore: fix e2e
- Regression from #5948
- Use proper permission.
- Remove debug statement
2024-11-15 14:02:16 +01:00
Earl Warren 1e1b162cbe Merge pull request 'fix: 15 November 2024 security fixes batch' (#5974) from earl-warren/forgejo:wip-security-15-11 into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5974
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Reviewed-by: Otto <otto@codeberg.org>
2024-11-15 11:19:50 +00:00
Earl Warren b1bc294955
chore(release-notes): 15 November 2024 security fixes 2024-11-15 11:17:14 +01:00
Michael Kriese 01ab0583f5 Merge pull request 'test: fix e2e tests' (#5968) from viceice/test/e2e-fixes into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5968
2024-11-15 10:16:18 +00:00
Gusted 786dfc7fb8
fix: add ID check for updating push mirror interval
- Ensure that the specified push mirror ID belongs to the requested
repository, otherwise it is possible to modify the intervals of the push
mirrors that do not belong to the requested repository.
- Integration test added.
2024-11-15 10:59:36 +01:00
Gusted 061abe6004
fix: don't show private forks in forks list
- If a repository is forked to a private or limited user/organization,
the fork should not be visible in the list of forks depending on the
doer requesting the list of forks.
- Added integration testing for web and API route.
2024-11-15 10:59:36 +01:00
Gusted 3e3ef76808
fix: require code permissions for branch feed
- The RSS and atom feed for branches exposes details about the code, it
therefore should be guarded by the requirement that the doer has access
to the code of that repository.
- Added integration testing.
2024-11-15 10:59:36 +01:00
Gusted 7067cc7da4
fix: strict matching of allowed content for sanitizer
- _Simply_ add `^$` to regexp that didn't had it yet, this avoids any
content being allowed that simply had the allowed content as a
substring.
- Fix file-preview regex to have `$` instead of `*`.
2024-11-15 10:59:36 +01:00
Gusted e6bbecb02d
fix: disallow basic authorization when security keys are enrolled
- This unifies the security behavior of enrolling security keys with
enrolling TOTP as a 2FA method. When TOTP is enrolled, you cannot use
basic authorization (user:password) to make API request on behalf of the
user, this is now also the case when you enroll security keys.
- The usage of access tokens are the only method to make API requests on
behalf of the user when a 2FA method is enrolled for the user.
- Integration test added.
2024-11-15 10:59:36 +01:00
Gusted b70196653f
fix: anomynous users code search for private/limited user's repository
- Consider private/limited users in the `AccessibleRepositoryCondition`
query, previously this only considered private/limited organization.
This limits the ability for anomynous users to do code search on
private/limited user's repository
- Unit test added.
2024-11-15 10:59:36 +01:00
Gusted 9508aa7713
Improve usage of HMAC output for mailer tokens
- If the incoming mail feature is enabled, tokens are being sent with
outgoing mails. These tokens contains information about what type of
action is allow with such token (such as replying to a certain issue
ID), to verify these tokens the code uses the HMAC-SHA256 construction.
- The output of the HMAC is truncated to 80 bits, because this is
recommended by RFC2104, but RFC2104 actually doesn't recommend this. It
recommends, if truncation should need to take place, it should use
max(80, hash_len/2) of the leftmost bits. For HMAC-SHA256 this works out
to 128 bits instead of the currently used 80 bits.
- Update to token version 2 and disallow any usage of token version 1,
token version 2 are generated with 128 bits of HMAC output.
- Add test to verify the deprecation of token version 1 and a general
MAC check test.
2024-11-15 10:59:36 +01:00
Gusted 1ce33aa38d
fix: extend forgejo_auth_token table
- Add a `purpose` column, this allows the `forgejo_auth_token` table to
be used by other parts of Forgejo, while still enjoying the
no-compromise architecture.
- Remove the 'roll your own crypto' time limited code functions and
migrate them to the `forgejo_auth_token` table. This migration ensures
generated codes can only be used for their purpose and ensure they are
invalidated after their usage by deleting it from the database, this
also should help making auditing of the security code easier, as we're
no longer trying to stuff a lot of data into a HMAC construction.
-Helper functions are rewritten to ensure a safe-by-design approach to
these tokens.
- Add the `forgejo_auth_token` to dbconsistency doctor and add it to the
`deleteUser` function.
- TODO: Add cron job to delete expired authorization tokens.
- Unit and integration tests added.
2024-11-15 10:59:36 +01:00
Michael Kriese 0fa436c373 Merge pull request 'ci: use oci mirror images' (#5963) from viceice/ci/oci-mirror into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5963
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
2024-11-15 08:22:35 +00:00
Michael Kriese 296935b0d7 Merge pull request 'chore: improve preparing tests' (#5948) from gusted/improve-testz into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5948
Reviewed-by: Otto <otto@codeberg.org>
Reviewed-by: Michael Kriese <michael.kriese@gmx.de>
2024-11-15 07:35:22 +00:00
Michael Kriese 1c25bbe773
test: fix e2e tests 2024-11-15 08:29:58 +01:00
Michael Kriese c8d97e5594
ci: use oci mirror images 2024-11-15 08:19:50 +01:00
Earl Warren e426a52a87 Merge pull request 'chore(release-notes): update the v9.0.2 & v7.0.11 links' (#5943) from earl-warren/forgejo:wip-release-notes into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5943
2024-11-15 07:11:46 +00:00
Michael Kriese faa796feb9 Merge pull request 'ci: proper job name' (#5964) from viceice/ci/job-name into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5964
Reviewed-by: Antonin Delpeuch <wetneb@noreply.codeberg.org>
2024-11-15 07:02:58 +00:00
Renovate Bot cdc38ace39 Update module google.golang.org/grpc to v1.68.0 2024-11-15 02:03:08 +00:00
Renovate Bot 4043377377 Update dependency tailwindcss to v3.4.15 2024-11-15 00:09:10 +00:00
Michael Kriese c226b4d00a
feat: use oci mirror for tonistiigi/xx image 2024-11-15 00:55:43 +01:00
Michael Kriese 19c9e0a0c2
ci: proper job name 2024-11-15 00:48:45 +01:00
Gusted d1520cf08d
chore: improve preparing tests
- Only prepare repositories once.
- Move the repositories to temporary directories (these should usually be stored in
memory) which are recreated for each test to avoid persistentance
between tests. Doing some dirty profiling suggests that the preparing
test functions from 140-100ms to 70-40ms
2024-11-14 10:07:52 +01:00
Kwonunn f9169eac96
re-add the string for read-only viewers 2024-11-14 09:47:49 +01:00
Kwonunn f8cbd6301e
reword the no-workflows page 2024-11-14 09:47:48 +01:00
Earl Warren a5ba7cadf7
chore(release-notes): update the v9.0.2 & v7.0.11 links 2024-11-13 23:23:47 +01:00
126 changed files with 3723 additions and 1389 deletions

View file

@ -1,59 +0,0 @@
# Copyright 2024 The Forgejo Authors
# SPDX-License-Identifier: MIT
#
# To modify this workflow:
#
# - change pull_request_target: to pull_request:
# so that it runs from a pull request instead of the default branch
#
# - push it to the wip-ci-backport branch on the forgejo repository
# otherwise it will not have access to the secrets required to push
# the PR
#
# - open a pull request targetting wip-ci-backport that includes a change
# that can be backported without conflict in v1.21 and set the
# `backport/v1.21` label.
#
# - once it works, open a pull request for the sake of keeping track
# of the change even if the PR won't run it because it will use
# whatever is in the default branch instead
#
# - after it is merged, double check it works by setting a
# `backport/v1.21` label on a merged pull request that can be backported
# without conflict.
#
on:
pull_request_target:
types:
- closed
- labeled
jobs:
backporting:
if: >
( vars.ROLE == 'forgejo-coding' ) && (
github.event.pull_request.merged
&&
contains(toJSON(github.event.pull_request.labels), 'backport/v')
)
runs-on: docker
container:
image: 'code.forgejo.org/oci/node:20-bookworm'
steps:
- name: event info
run: |
cat <<'EOF'
${{ toJSON(github) }}
EOF
- uses: https://code.forgejo.org/actions/git-backporting@v4.8.4
with:
target-branch-pattern: "^backport/(?<target>(v.*))$"
strategy: ort
strategy-option: find-renames
cherry-pick-options: -x
auth: ${{ secrets.BACKPORT_TOKEN }}
pull-request: ${{ github.event.pull_request.url }}
auto-no-squash: true
enable-err-notification: true
git-user: forgejo-backport-action
git-email: forgejo-backport-action@noreply.codeberg.org

View file

@ -1,75 +0,0 @@
# Copyright 2024 The Forgejo Authors
# SPDX-License-Identifier: MIT
#
# To modify this workflow:
#
# - push it to the wip-ci-end-to-end branch on the forgejo repository
# otherwise it will not have access to the secrets required to push
# the cascading PR
#
# - once it works, open a pull request for the sake of keeping track
# of the change even if the PR won't run it because it will use
# whatever is in the default branch instead
#
# - after it is merged, double check it works by setting the
# run-end-to-end-test on a pull request (any pull request will do)
#
name: end-to-end
on:
push:
branches:
- 'wip-ci-end-to-end'
pull_request_target:
types:
- labeled
jobs:
info:
if: vars.ROLE == 'forgejo-coding'
runs-on: docker
container:
image: code.forgejo.org/oci/node:20-bookworm
steps:
- name: event
run: |
echo github.event.pull_request.head.repo.fork = ${{ github.event.pull_request.head.repo.fork }}
echo github.event.action = ${{ github.event.action }}
echo github.event.label
cat <<'EOF'
${{ toJSON(github.event.label) }}
EOF
cat <<'EOF'
${{ toJSON(github.event) }}
EOF
cascade:
if: >
vars.ROLE == 'forgejo-coding' && (
github.event_name == 'push' ||
(
github.event.action == 'label_updated' && github.event.label.name == 'run-end-to-end-tests'
)
)
runs-on: docker
container:
image: code.forgejo.org/oci/node:20-bookworm
steps:
- uses: actions/checkout@v4
with:
fetch-depth: '0'
show-progress: 'false'
- uses: actions/cascading-pr@v2
with:
origin-url: ${{ env.GITHUB_SERVER_URL }}
origin-repo: ${{ github.repository }}
origin-token: ${{ secrets.END_TO_END_CASCADING_PR_ORIGIN }}
origin-pr: ${{ github.event.pull_request.number }}
origin-ref: ${{ github.event_name == 'push' && github.event.ref || '' }}
destination-url: https://code.forgejo.org
destination-fork-repo: cascading-pr/end-to-end
destination-repo: forgejo/end-to-end
destination-branch: main
destination-token: ${{ secrets.END_TO_END_CASCADING_PR_DESTINATION }}
close-merge: true
update: .forgejo/cascading-pr-end-to-end

View file

@ -0,0 +1,205 @@
# Copyright 2024 The Forgejo Authors
# SPDX-License-Identifier: MIT
#
# To modify the pull_request_target jobs:
#
# - push it to the wip-ci-issue-labels branch on the forgejo repository
# otherwise it will not have access to the required secrets.
#
# - once it works, open a pull request for the sake of keeping track
# of the change even if the PR won't run it because it will use
# whatever is in the default branch instead
#
# - after it is merged, double check it works by changing the labels
# to trigger the job.
#
name: issue-labels
on:
push:
branches:
- 'wip-ci-issue-labels'
pull_request_target:
types:
- closed
- edited
- labeled
- synchronize
pull_request:
types:
- edited
- labeled
- opened
- synchronize
jobs:
info:
if: vars.ROLE == 'forgejo-coding'
runs-on: docker
container:
image: code.forgejo.org/oci/node:20-bookworm
steps:
- name: Debug info
run: |
cat <<'EOF'
${{ toJSON(github) }}
EOF
end-to-end:
if: >
vars.ROLE == 'forgejo-coding' &&
secrets.END_TO_END_CASCADING_PR_DESTINATION != '' &&
secrets.END_TO_END_CASCADING_PR_ORIGIN != '' &&
(
github.event_name == 'push' ||
(
github.event_name == 'pull_request_target' &&
github.event.action == 'label_updated' &&
github.event.label.name == 'run-end-to-end-tests'
)
)
runs-on: docker
container:
image: code.forgejo.org/oci/node:20-bookworm
steps:
- name: Debug info
run: |
cat <<'EOF'
${{ toJSON(github) }}
EOF
- uses: actions/checkout@v4
with:
fetch-depth: '0'
show-progress: 'false'
- uses: actions/cascading-pr@v2
with:
origin-url: ${{ env.GITHUB_SERVER_URL }}
origin-repo: ${{ github.repository }}
origin-token: ${{ secrets.END_TO_END_CASCADING_PR_ORIGIN }}
origin-pr: ${{ github.event.pull_request.number }}
origin-ref: ${{ github.event_name == 'push' && github.event.ref || '' }}
destination-url: https://code.forgejo.org
destination-fork-repo: cascading-pr/end-to-end
destination-repo: forgejo/end-to-end
destination-branch: main
destination-token: ${{ secrets.END_TO_END_CASCADING_PR_DESTINATION }}
close-merge: true
update: .forgejo/cascading-pr-end-to-end
backporting:
if: >
vars.ROLE == 'forgejo-coding' &&
secrets.BACKPORT_TOKEN != '' &&
github.event_name == 'pull_request_target' &&
(
github.event.pull_request.merged &&
contains(toJSON(github.event.pull_request.labels), 'backport/v')
)
runs-on: docker
container:
image: 'code.forgejo.org/oci/node:20-bookworm'
steps:
- name: Debug info
run: |
cat <<'EOF'
${{ toJSON(github) }}
EOF
- uses: https://code.forgejo.org/actions/git-backporting@v4.8.4
with:
target-branch-pattern: "^backport/(?<target>(v.*))$"
strategy: ort
strategy-option: find-renames
cherry-pick-options: -x
auth: ${{ secrets.BACKPORT_TOKEN }}
pull-request: ${{ github.event.pull_request.url }}
auto-no-squash: true
enable-err-notification: true
git-user: forgejo-backport-action
git-email: forgejo-backport-action@noreply.codeberg.org
merge-conditions:
if: >
vars.ROLE == 'forgejo-coding' &&
github.event_name == 'pull_request' &&
(
github.event.action == 'label_updated' ||
github.event.action == 'edited' ||
github.event.action == 'opened'
)
runs-on: docker
container:
image: 'code.forgejo.org/oci/node:20-bookworm'
steps:
- name: Debug info
run: |
cat <<'EOF'
${{ toJSON(github) }}
EOF
- name: Missing test label
if: >
!(
contains(toJSON(github.event.pull_request.labels), 'test/present')
|| contains(toJSON(github.event.pull_request.labels), 'test/not-needed')
|| contains(toJSON(github.event.pull_request.labels), 'test/manual')
)
run: |
echo "Test label must be set to either 'present', 'not-needed' or 'manual'."
exit 1
- name: Missing manual test instructions
if: >
(
contains(toJSON(github.event.pull_request.labels), 'test/manual')
&& !contains(toJSON(github.event.pull_request.body), '# Test')
)
run: |
echo "Manual test label is set. The PR description needs to contain test steps introduced by a heading like:"
echo "# Testing"
exit 1
release-notes:
if: >
vars.ROLE == 'forgejo-coding' &&
secrets.RELEASE_NOTES_ASSISTANT_TOKEN != '' &&
github.event_name == 'pull_request_target' &&
contains(github.event.pull_request.labels.*.name, 'worth a release-note') &&
(
github.event.action == 'label_updated' ||
github.event.action == 'edited' ||
github.event.action == 'synchronized'
)
runs-on: docker
container:
image: 'code.forgejo.org/oci/node:20-bookworm'
steps:
- name: Debug info
run: |
cat <<'EOF'
${{ toJSON(github) }}
EOF
- uses: https://code.forgejo.org/actions/checkout@v4
- uses: https://code.forgejo.org/actions/setup-go@v5
with:
go-version-file: "go.mod"
cache: false
- name: apt install jq
run: |
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get -q install -y -qq jq
- name: release-notes-assistant preview
run: |
go run code.forgejo.org/forgejo/release-notes-assistant@v1.1.1 --config .release-notes-assistant.yaml --storage pr --storage-location ${{ github.event.pull_request.number }} --forgejo-url $GITHUB_SERVER_URL --repository $GITHUB_REPOSITORY --token ${{ secrets.RELEASE_NOTES_ASSISTANT_TOKEN }} preview ${{ github.event.pull_request.number }}

View file

@ -1,45 +0,0 @@
# Copyright 2024 The Forgejo Authors
# SPDX-License-Identifier: MIT
name: requirements
on:
pull_request:
types:
- labeled
- edited
- opened
- synchronize
jobs:
merge-conditions:
if: vars.ROLE == 'forgejo-coding'
runs-on: docker
container:
image: 'code.forgejo.org/oci/node:20-bookworm'
steps:
- name: Debug output
run: |
cat <<'EOF'
${{ toJSON(github.event) }}
EOF
- name: Missing test label
if: >
!(
contains(toJSON(github.event.pull_request.labels), 'test/present')
|| contains(toJSON(github.event.pull_request.labels), 'test/not-needed')
|| contains(toJSON(github.event.pull_request.labels), 'test/manual')
)
run: |
echo "Test label must be set to either 'present', 'not-needed' or 'manual'."
exit 1
- name: Missing manual test instructions
if: >
(
contains(toJSON(github.event.pull_request.labels), 'test/manual')
&& !contains(toJSON(github.event.pull_request.body), '# Test')
)
run: |
echo "Manual test label is set. The PR description needs to contain test steps introduced by a heading like:"
echo "# Testing"
exit 1

View file

@ -84,20 +84,3 @@ jobs:
ref_name: '${{ github.ref_name }}' ref_name: '${{ github.ref_name }}'
image: 'codeberg.org/forgejo-experimental/forgejo' image: 'codeberg.org/forgejo-experimental/forgejo'
tag_suffix: '-rootless' tag_suffix: '-rootless'
- name: set up go for the DNS update below
if: vars.ROLE == 'forgejo-experimental' && secrets.OVH_APP_KEY != ''
uses: https://code.forgejo.org/actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: update the _release.experimental DNS record
if: vars.ROLE == 'forgejo-experimental' && secrets.OVH_APP_KEY != ''
uses: https://code.forgejo.org/actions/ovh-dns-update@v1
with:
subdomain: _release.experimental
domain: forgejo.com # there is a CNAME from .org to .com (for security reasons)
record-id: 5283602601
value: v=${{ github.ref_name }}
ovh-app-key: ${{ secrets.OVH_APP_KEY }}
ovh-app-secret: ${{ secrets.OVH_APP_SECRET }}
ovh-consumer-key: ${{ secrets.OVH_CON_KEY }}

View file

@ -1,39 +0,0 @@
on:
pull_request_target:
types:
- edited
- synchronize
- labeled
jobs:
release-notes:
if: ( vars.ROLE == 'forgejo-coding' ) && contains(github.event.pull_request.labels.*.name, 'worth a release-note')
runs-on: docker
container:
image: 'code.forgejo.org/oci/node:20-bookworm'
steps:
- uses: https://code.forgejo.org/actions/checkout@v4
- name: event
run: |
cat <<'EOF'
${{ toJSON(github.event.pull_request.labels.*.name) }}
EOF
cat <<'EOF'
${{ toJSON(github.event) }}
EOF
- uses: https://code.forgejo.org/actions/setup-go@v5
with:
go-version-file: "go.mod"
cache: false
- name: apt install jq
run: |
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get -q install -y -qq jq
- name: release-notes-assistant preview
run: |
go run code.forgejo.org/forgejo/release-notes-assistant@v1.1.1 --config .release-notes-assistant.yaml --storage pr --storage-location ${{ github.event.pull_request.number }} --forgejo-url $GITHUB_SERVER_URL --repository $GITHUB_REPOSITORY --token ${{ secrets.RELEASE_NOTES_ASSISTANT_TOKEN }} preview ${{ github.event.pull_request.number }}

View file

@ -25,7 +25,7 @@ jobs:
runs-on: docker-runner-one runs-on: docker-runner-one
container: container:
image: code.forgejo.org/forgejo-contrib/renovate:39.9.1 image: code.forgejo.org/forgejo-contrib/renovate:39.19.1
steps: steps:
- name: Load renovate repo cache - name: Load renovate repo cache

View file

@ -56,13 +56,13 @@ jobs:
image: 'code.forgejo.org/oci/node:20-bookworm' image: 'code.forgejo.org/oci/node:20-bookworm'
services: services:
elasticsearch: elasticsearch:
image: docker.io/bitnami/elasticsearch:7 image: code.forgejo.org/oci/bitnami/elasticsearch:7
options: --tmpfs /bitnami/elasticsearch/data options: --tmpfs /bitnami/elasticsearch/data
env: env:
discovery.type: single-node discovery.type: single-node
ES_JAVA_OPTS: "-Xms512m -Xmx512m" ES_JAVA_OPTS: "-Xms512m -Xmx512m"
minio: minio:
image: docker.io/bitnami/minio:2024.8.17 image: code.forgejo.org/oci/bitnami/minio:2024.8.17
options: >- options: >-
--hostname gitea.minio --tmpfs /bitnami/minio/data --hostname gitea.minio --tmpfs /bitnami/minio/data
env: env:
@ -122,12 +122,12 @@ jobs:
USE_REPO_TEST_DIR: 1 USE_REPO_TEST_DIR: 1
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
CHANGED_FILES: ${{steps.changed-files.outputs.all_changed_files}} CHANGED_FILES: ${{steps.changed-files.outputs.all_changed_files}}
- name: Upload screenshots on failure - name: Upload test artifacts on failure
if: failure() if: failure()
uses: https://code.forgejo.org/forgejo/upload-artifact@v4 uses: https://code.forgejo.org/forgejo/upload-artifact@v4
with: with:
name: screenshots.zip name: test-artifacts.zip
path: /workspace/forgejo/forgejo/tests/e2e/test-artifacts/*/*.png path: tests/e2e/test-artifacts/
retention-days: 3 retention-days: 3
test-remote-cacher: test-remote-cacher:
if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing' if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing'
@ -135,20 +135,21 @@ jobs:
needs: [backend-checks, frontend-checks, test-unit] needs: [backend-checks, frontend-checks, test-unit]
container: container:
image: 'code.forgejo.org/oci/node:20-bookworm' image: 'code.forgejo.org/oci/node:20-bookworm'
name: ${{ format('test-remote-cacher ({0})', matrix.cacher.name) }}
strategy: strategy:
matrix: matrix:
cacher: cacher:
# redis - name: redis
- image: docker.io/bitnami/redis:7.2 image: code.forgejo.org/oci/bitnami/redis:7.2
options: --tmpfs /bitnami/redis/data options: --tmpfs /bitnami/redis/data
# redict - name: redict
- image: registry.redict.io/redict:7.3.0-scratch image: registry.redict.io/redict:7.3.0-scratch
options: --tmpfs /data options: --tmpfs /data
# valkey - name: valkey
- image: docker.io/bitnami/valkey:7.2 image: code.forgejo.org/oci/bitnami/valkey:7.2
options: --tmpfs /bitnami/redis/data options: --tmpfs /bitnami/redis/data
# garnet - name: garnet
- image: ghcr.io/microsoft/garnet-alpine:1.0.14 image: ghcr.io/microsoft/garnet-alpine:1.0.14
options: --tmpfs /data options: --tmpfs /data
services: services:
cacher: cacher:
@ -177,7 +178,7 @@ jobs:
image: 'code.forgejo.org/oci/node:20-bookworm' image: 'code.forgejo.org/oci/node:20-bookworm'
services: services:
mysql: mysql:
image: 'docker.io/bitnami/mysql:8.4' image: 'code.forgejo.org/oci/bitnami/mysql:8.4'
env: env:
ALLOW_EMPTY_PASSWORD: yes ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: testgitea MYSQL_DATABASE: testgitea
@ -207,19 +208,21 @@ jobs:
image: 'code.forgejo.org/oci/node:20-bookworm' image: 'code.forgejo.org/oci/node:20-bookworm'
services: services:
minio: minio:
image: docker.io/bitnami/minio:2024.8.17 image: code.forgejo.org/oci/bitnami/minio:2024.8.17
env: env:
MINIO_ROOT_USER: 123456 MINIO_ROOT_USER: 123456
MINIO_ROOT_PASSWORD: 12345678 MINIO_ROOT_PASSWORD: 12345678
options: --tmpfs /bitnami/minio/data options: --tmpfs /bitnami/minio/data
ldap: ldap:
image: docker.io/gitea/test-openldap:latest image: code.forgejo.org/oci/test-openldap:latest
pgsql: pgsql:
image: 'code.forgejo.org/oci/postgres:15' image: code.forgejo.org/oci/bitnami/postgresql:15
env: env:
POSTGRES_DB: test POSTGRESQL_DATABASE: test
POSTGRES_PASSWORD: postgres POSTGRESQL_PASSWORD: postgres
options: --tmpfs /var/lib/postgresql/data POSTGRESQL_FSYNC: off
POSTGRESQL_EXTRA_FLAGS: -c full_page_writes=off
options: --tmpfs /bitnami/postgresql
steps: steps:
- uses: https://code.forgejo.org/actions/checkout@v4 - uses: https://code.forgejo.org/actions/checkout@v4
- uses: ./.forgejo/workflows-composite/setup-env - uses: ./.forgejo/workflows-composite/setup-env

View file

@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM docker.io/tonistiigi/xx AS xx FROM --platform=$BUILDPLATFORM code.forgejo.org/oci/xx AS xx
FROM --platform=$BUILDPLATFORM code.forgejo.org/oci/golang:1.23-alpine3.20 as build-env FROM --platform=$BUILDPLATFORM code.forgejo.org/oci/golang:1.23-alpine3.20 as build-env

View file

@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM docker.io/tonistiigi/xx AS xx FROM --platform=$BUILDPLATFORM code.forgejo.org/oci/xx AS xx
FROM --platform=$BUILDPLATFORM code.forgejo.org/oci/golang:1.23-alpine3.20 as build-env FROM --platform=$BUILDPLATFORM code.forgejo.org/oci/golang:1.23-alpine3.20 as build-env

View file

@ -49,7 +49,10 @@ GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 # renovate: datasour
DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.26.0 # renovate: datasource=go DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.26.0 # renovate: datasource=go
GOMOCK_PACKAGE ?= go.uber.org/mock/mockgen@v0.4.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.16.2 # renovate: datasource=go GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.16.2 # renovate: datasource=go
RENOVATE_NPM_PACKAGE ?= renovate@39.9.1 # renovate: datasource=docker packageName=code.forgejo.org/forgejo-contrib/renovate RENOVATE_NPM_PACKAGE ?= renovate@39.19.1 # renovate: datasource=docker packageName=code.forgejo.org/forgejo-contrib/renovate
# https://github.com/disposable-email-domains/disposable-email-domains/commits/main/
DISPOSABLE_EMAILS_SHA ?= 0c27e671231d27cf66370034d7f6818037416989 # renovate: ...
ifeq ($(HAS_GO), yes) ifeq ($(HAS_GO), yes)
CGO_EXTRA_CFLAGS := -DSQLITE_MAX_VARIABLE_NUMBER=32766 CGO_EXTRA_CFLAGS := -DSQLITE_MAX_VARIABLE_NUMBER=32766
@ -417,10 +420,10 @@ lint-frontend: lint-js lint-css
lint-frontend-fix: lint-js-fix lint-css-fix lint-frontend-fix: lint-js-fix lint-css-fix
.PHONY: lint-backend .PHONY: lint-backend
lint-backend: lint-go lint-go-vet lint-editorconfig lint-renovate lint-locale lint-backend: lint-go lint-go-vet lint-editorconfig lint-renovate lint-locale lint-disposable-emails
.PHONY: lint-backend-fix .PHONY: lint-backend-fix
lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig lint-disposable-emails-fix
.PHONY: lint-codespell .PHONY: lint-codespell
lint-codespell: lint-codespell:
@ -511,6 +514,14 @@ lint-go-gopls:
lint-editorconfig: lint-editorconfig:
$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) templates .forgejo/workflows $(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) templates .forgejo/workflows
.PHONY: lint-disposable-emails
lint-disposable-emails:
$(GO) run build/generate-disposable-email.go -check -r $(DISPOSABLE_EMAILS_SHA)
.PHONY: lint-disposable-emails-fix
lint-disposable-emails-fix:
$(GO) run build/generate-disposable-email.go -r $(DISPOSABLE_EMAILS_SHA)
.PHONY: lint-templates .PHONY: lint-templates
lint-templates: .venv node_modules lint-templates: .venv node_modules
@node tools/lint-templates-svg.js @node tools/lint-templates-svg.js

View file

@ -6,6 +6,10 @@ A [patch or minor release](https://semver.org/spec/v2.0.0.html) (e.g. upgrading
The release notes of each release [are available in the corresponding milestone](https://codeberg.org/forgejo/forgejo/milestones), starting with [Forgejo 7.0.7](https://codeberg.org/forgejo/forgejo/milestone/7683) and [Forgejo 8.0.1](https://codeberg.org/forgejo/forgejo/milestone/7682). The release notes of each release [are available in the corresponding milestone](https://codeberg.org/forgejo/forgejo/milestones), starting with [Forgejo 7.0.7](https://codeberg.org/forgejo/forgejo/milestone/7683) and [Forgejo 8.0.1](https://codeberg.org/forgejo/forgejo/milestone/7682).
## 9.0.2
The Forgejo v9.0.2 release notes are [available in the v9.0.2 milestone](https://codeberg.org/forgejo/forgejo/milestone/8610).
## 9.0.1 ## 9.0.1
The Forgejo v9.0.1 release notes are [available in the v9.0.1 milestone](https://codeberg.org/forgejo/forgejo/milestone/8544). The Forgejo v9.0.1 release notes are [available in the v9.0.1 milestone](https://codeberg.org/forgejo/forgejo/milestone/8544).
@ -163,6 +167,10 @@ A [companion blog post](https://forgejo.org/2024-07-release-v8-0/) provides addi
- [PR](https://codeberg.org/forgejo/forgejo/pulls/2937): <!--number 2937 --><!--number--><!--description -->31 March updates<!--description--> - [PR](https://codeberg.org/forgejo/forgejo/pulls/2937): <!--number 2937 --><!--number--><!--description -->31 March updates<!--description-->
<!--end release-notes-assistant--> <!--end release-notes-assistant-->
## 7.0.11
The Forgejo v7.0.11 release notes are [available in the v7.0.11 milestone](https://codeberg.org/forgejo/forgejo/milestone/8609).
## 7.0.10 ## 7.0.10
The Forgejo v7.0.10 release notes are [available in the v7.0.10 milestone](https://codeberg.org/forgejo/forgejo/milestone/8286). The Forgejo v7.0.10 release notes are [available in the v7.0.10 milestone](https://codeberg.org/forgejo/forgejo/milestone/8286).

View file

@ -8,6 +8,7 @@ package main
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"crypto"
"flag" "flag"
"fmt" "fmt"
"go/format" "go/format"
@ -15,6 +16,7 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"regexp"
"strings" "strings"
) )
@ -23,25 +25,105 @@ const disposableEmailListURL string = "https://raw.githubusercontent.com/disposa
var ( var (
gitRef *string = flag.String("r", "master", "Git reference of the domain list version") gitRef *string = flag.String("r", "master", "Git reference of the domain list version")
outPat *string = flag.String("o", "modules/setting/disposable_email_domain_data.go", "Output path") outPat *string = flag.String("o", "modules/setting/disposable_email_domain_data.go", "Output path")
check *bool = flag.Bool("check", false, "Check if the current output file matches the current upstream list")
) )
func main() { func main() {
flag.Parse() flag.Parse()
// generate the source code (array of domains) if *check {
res, err := generate() // read in the local copy of the domain list
if err != nil { local, err := get_local_file()
log.Fatalf("Generation Error: %v", err) if err != nil {
} log.Fatalf("File Read Error: %v", err)
}
// write result to a file // generate the remote copy of the domain list
err = os.WriteFile(*outPat, res, 0o644) remote, err := generate()
if err != nil { if err != nil {
log.Fatalf("File Write Error: %v", err) log.Fatalf("Generation Error: %v", err)
}
// strip the comments from both (so we dont fail simply due to git ref difference)
local = strip_comments(local)
remote = strip_comments(remote)
// generate the hash of the local copy
local_sha, err := hash(local)
if err != nil {
log.Fatalf("Local Hash Generation Error: %v", err)
}
// generate the hash of the remote copy
remote_sha, err := hash(remote)
if err != nil {
log.Fatalf("Remote Hash Generation Error: %v", err)
}
// if the hashes dont match then the local copy needs to be updated
if local_sha != remote_sha {
log.Fatalf("Disposable email domain list needs to be updated!! \"make lint-disposable-emails-fix\"")
}
} else {
// generate the source code (array of domains)
res, err := generate()
if err != nil {
log.Fatalf("Generation Error: %v", err)
}
// write result to a file
err = os.WriteFile(*outPat, res, 0o644)
if err != nil {
log.Fatalf("File Write Error: %v", err)
}
} }
} }
func generate() ([]byte, error) { func strip_comments(data []byte) []byte {
result := make([]byte, 0, len(data))
re := regexp.MustCompile(`^\W*//.*$`)
for _, line := range bytes.Split(data, []byte("\n")) {
if !re.Match(line) {
result = append(result, line...)
}
}
return result
}
func hash(data []byte) (string, error) {
var err error
hash := crypto.SHA3_256.New()
_, err = hash.Write(data)
if err != nil {
return "", err
}
return fmt.Sprintf("%x", hash.Sum(nil)), err
}
func get_local_file() ([]byte, error) {
var err error
f, err := os.Open(*outPat)
if err != nil {
return nil, err
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return nil, err
}
return data, err
}
func get_remote() ([]string, error) {
var err error var err error
var url string = fmt.Sprintf(disposableEmailListURL, *gitRef) var url string = fmt.Sprintf(disposableEmailListURL, *gitRef)
@ -63,10 +145,22 @@ func generate() ([]byte, error) {
var arrDomains []string var arrDomains []string
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
arrDomains = append(arrDomains, line) arrDomains = append(arrDomains, line)
} }
return arrDomains, err
}
func generate() ([]byte, error) {
var err error
var url string = fmt.Sprintf(disposableEmailListURL, *gitRef)
// download the domains list
arrDomains, err := get_remote()
if err != nil {
return nil, err
}
// build the string in a readable way // build the string in a readable way
var sb strings.Builder var sb strings.Builder

6
go.mod
View file

@ -107,7 +107,7 @@ require (
golang.org/x/sys v0.27.0 golang.org/x/sys v0.27.0
golang.org/x/text v0.20.0 golang.org/x/text v0.20.0
golang.org/x/tools v0.26.0 golang.org/x/tools v0.26.0
google.golang.org/grpc v1.67.1 google.golang.org/grpc v1.68.0
google.golang.org/protobuf v1.35.1 google.golang.org/protobuf v1.35.1
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/ini.v1 v1.67.0 gopkg.in/ini.v1 v1.67.0
@ -283,7 +283,7 @@ require (
golang.org/x/mod v0.21.0 // indirect golang.org/x/mod v0.21.0 // indirect
golang.org/x/sync v0.9.0 // indirect golang.org/x/sync v0.9.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
@ -297,4 +297,4 @@ replace github.com/nektos/act => code.forgejo.org/forgejo/act v1.22.0
replace github.com/mholt/archiver/v3 => code.forgejo.org/forgejo/archiver/v3 v3.5.1 replace github.com/mholt/archiver/v3 => code.forgejo.org/forgejo/archiver/v3 v3.5.1
replace github.com/goccy/go-json => github.com/grafana/go-json v0.0.0-20241106155216-71a03f133f5c replace github.com/goccy/go-json => github.com/grafana/go-json v0.0.0-20241115232854-f14426c40ff2

12
go.sum
View file

@ -381,8 +381,8 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/grafana/go-json v0.0.0-20241106155216-71a03f133f5c h1:yKBKEC347YZpgii1KazRCfxHsTaxMqWZzoivM1OTT50= github.com/grafana/go-json v0.0.0-20241115232854-f14426c40ff2 h1:8xGrYqQ1GM4aaMk7pNDfecBdL/VGhEbpvvGBoqO6BIY=
github.com/grafana/go-json v0.0.0-20241106155216-71a03f133f5c/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/grafana/go-json v0.0.0-20241115232854-f14426c40ff2/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
@ -843,10 +843,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=

View file

@ -15,12 +15,31 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
) )
type AuthorizationPurpose string
var (
// Used to store long term authorization tokens.
LongTermAuthorization AuthorizationPurpose = "long_term_authorization"
// Used to activate a user account.
UserActivation AuthorizationPurpose = "user_activation"
// Used to reset the password.
PasswordReset AuthorizationPurpose = "password_reset"
)
// Used to activate the specified email address for a user.
func EmailActivation(email string) AuthorizationPurpose {
return AuthorizationPurpose("email_activation:" + email)
}
// AuthorizationToken represents a authorization token to a user. // AuthorizationToken represents a authorization token to a user.
type AuthorizationToken struct { type AuthorizationToken struct {
ID int64 `xorm:"pk autoincr"` ID int64 `xorm:"pk autoincr"`
UID int64 `xorm:"INDEX"` UID int64 `xorm:"INDEX"`
LookupKey string `xorm:"INDEX UNIQUE"` LookupKey string `xorm:"INDEX UNIQUE"`
HashedValidator string HashedValidator string
Purpose AuthorizationPurpose `xorm:"NOT NULL DEFAULT 'long_term_authorization'"`
Expiry timeutil.TimeStamp Expiry timeutil.TimeStamp
} }
@ -41,7 +60,7 @@ func (authToken *AuthorizationToken) IsExpired() bool {
// GenerateAuthToken generates a new authentication token for the given user. // GenerateAuthToken generates a new authentication token for the given user.
// It returns the lookup key and validator values that should be passed to the // It returns the lookup key and validator values that should be passed to the
// user via a long-term cookie. // user via a long-term cookie.
func GenerateAuthToken(ctx context.Context, userID int64, expiry timeutil.TimeStamp) (lookupKey, validator string, err error) { func GenerateAuthToken(ctx context.Context, userID int64, expiry timeutil.TimeStamp, purpose AuthorizationPurpose) (lookupKey, validator string, err error) {
// Request 64 random bytes. The first 32 bytes will be used for the lookupKey // Request 64 random bytes. The first 32 bytes will be used for the lookupKey
// and the other 32 bytes will be used for the validator. // and the other 32 bytes will be used for the validator.
rBytes, err := util.CryptoRandomBytes(64) rBytes, err := util.CryptoRandomBytes(64)
@ -56,14 +75,15 @@ func GenerateAuthToken(ctx context.Context, userID int64, expiry timeutil.TimeSt
Expiry: expiry, Expiry: expiry,
LookupKey: lookupKey, LookupKey: lookupKey,
HashedValidator: HashValidator(rBytes[32:]), HashedValidator: HashValidator(rBytes[32:]),
Purpose: purpose,
}) })
return lookupKey, validator, err return lookupKey, validator, err
} }
// FindAuthToken will find a authorization token via the lookup key. // FindAuthToken will find a authorization token via the lookup key.
func FindAuthToken(ctx context.Context, lookupKey string) (*AuthorizationToken, error) { func FindAuthToken(ctx context.Context, lookupKey string, purpose AuthorizationPurpose) (*AuthorizationToken, error) {
var authToken AuthorizationToken var authToken AuthorizationToken
has, err := db.GetEngine(ctx).Where("lookup_key = ?", lookupKey).Get(&authToken) has, err := db.GetEngine(ctx).Where("lookup_key = ? AND purpose = ?", lookupKey, purpose).Get(&authToken)
if err != nil { if err != nil {
return nil, err return nil, err
} else if !has { } else if !has {

View file

@ -7,6 +7,7 @@
target_url: https://example.com/builds/ target_url: https://example.com/builds/
description: My awesome CI-service description: My awesome CI-service
context: ci/awesomeness context: ci/awesomeness
context_hash: c65f4d64a3b14a3eced0c9b36799e66e1bd5ced7
creator_id: 2 creator_id: 2
- -
@ -18,6 +19,7 @@
target_url: https://example.com/coverage/ target_url: https://example.com/coverage/
description: My awesome Coverage service description: My awesome Coverage service
context: cov/awesomeness context: cov/awesomeness
context_hash: 3929ac7bccd3fa1bf9b38ddedb77973b1b9a8cfe
creator_id: 2 creator_id: 2
- -
@ -29,6 +31,7 @@
target_url: https://example.com/coverage/ target_url: https://example.com/coverage/
description: My awesome Coverage service description: My awesome Coverage service
context: cov/awesomeness context: cov/awesomeness
context_hash: 3929ac7bccd3fa1bf9b38ddedb77973b1b9a8cfe
creator_id: 2 creator_id: 2
- -
@ -40,6 +43,7 @@
target_url: https://example.com/builds/ target_url: https://example.com/builds/
description: My awesome CI-service description: My awesome CI-service
context: ci/awesomeness context: ci/awesomeness
context_hash: c65f4d64a3b14a3eced0c9b36799e66e1bd5ced7
creator_id: 2 creator_id: 2
- -
@ -51,15 +55,41 @@
target_url: https://example.com/builds/ target_url: https://example.com/builds/
description: My awesome deploy service description: My awesome deploy service
context: deploy/awesomeness context: deploy/awesomeness
context_hash: ae9547713a6665fc4261d0756904932085a41cf2
creator_id: 2 creator_id: 2
- -
id: 6 id: 6
index: 6 index: 1
repo_id: 62 repo_id: 62
state: "failure" state: "failure"
sha: "774f93df12d14931ea93259ae93418da4482fcc1" sha: "774f93df12d14931ea93259ae93418da4482fcc1"
target_url: "/user2/test_workflows/actions" target_url: "/user2/test_workflows/actions"
description: My awesome deploy service description: My awesome deploy service
context: deploy/awesomeness context: deploy/awesomeness
context_hash: ae9547713a6665fc4261d0756904932085a41cf2
creator_id: 2
-
id: 7
index: 6
repo_id: 1
state: "pending"
sha: "1234123412341234123412341234123412341234"
target_url: https://example.com/builds/
description: My awesome deploy service
context: deploy/awesomeness
context_hash: ae9547713a6665fc4261d0756904932085a41cf2
creator_id: 2
-
id: 8
index: 2
repo_id: 62
state: "error"
sha: "774f93df12d14931ea93259ae93418da4482fcc1"
target_url: "/user2/test_workflows/actions"
description: "My awesome deploy service - v2"
context: deploy/awesomeness
context_hash: ae9547713a6665fc4261d0756904932085a41cf2
creator_id: 2 creator_id: 2

View file

@ -84,6 +84,8 @@ var migrations = []*Migration{
NewMigration("Add `legacy` to `web_authn_credential` table", AddLegacyToWebAuthnCredential), NewMigration("Add `legacy` to `web_authn_credential` table", AddLegacyToWebAuthnCredential),
// v23 -> v24 // v23 -> v24
NewMigration("Add `delete_branch_after_merge` to `auto_merge` table", AddDeleteBranchAfterMergeToAutoMerge), NewMigration("Add `delete_branch_after_merge` to `auto_merge` table", AddDeleteBranchAfterMergeToAutoMerge),
// v24 -> v25
NewMigration("Add `purpose` column to `forgejo_auth_token` table", AddPurposeToForgejoAuthToken),
} }
// GetCurrentDBVersion returns the current Forgejo database version. // GetCurrentDBVersion returns the current Forgejo database version.

View file

@ -0,0 +1,19 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package forgejo_migrations //nolint:revive
import "xorm.io/xorm"
func AddPurposeToForgejoAuthToken(x *xorm.Engine) error {
type ForgejoAuthToken struct {
ID int64 `xorm:"pk autoincr"`
Purpose string `xorm:"NOT NULL DEFAULT 'long_term_authorization'"`
}
if err := x.Sync(new(ForgejoAuthToken)); err != nil {
return err
}
_, err := x.Exec("UPDATE `forgejo_auth_token` SET purpose = 'long_term_authorization' WHERE purpose = ''")
return err
}

View file

@ -288,27 +288,18 @@ func GetLatestCommitStatus(ctx context.Context, repoID int64, sha string, listOp
// GetLatestCommitStatusForPairs returns all statuses with a unique context for a given list of repo-sha pairs // GetLatestCommitStatusForPairs returns all statuses with a unique context for a given list of repo-sha pairs
func GetLatestCommitStatusForPairs(ctx context.Context, repoSHAs []RepoSHA) (map[int64][]*CommitStatus, error) { func GetLatestCommitStatusForPairs(ctx context.Context, repoSHAs []RepoSHA) (map[int64][]*CommitStatus, error) {
type result struct { results := []*CommitStatus{}
Index int64
RepoID int64
SHA string
}
results := make([]result, 0, len(repoSHAs))
getBase := func() *xorm.Session {
return db.GetEngine(ctx).Table(&CommitStatus{})
}
// Create a disjunction of conditions for each repoID and SHA pair // Create a disjunction of conditions for each repoID and SHA pair
conds := make([]builder.Cond, 0, len(repoSHAs)) conds := make([]builder.Cond, 0, len(repoSHAs))
for _, repoSHA := range repoSHAs { for _, repoSHA := range repoSHAs {
conds = append(conds, builder.Eq{"repo_id": repoSHA.RepoID, "sha": repoSHA.SHA}) conds = append(conds, builder.Eq{"repo_id": repoSHA.RepoID, "sha": repoSHA.SHA})
} }
sess := getBase().Where(builder.Or(conds...)).
Select("max( `index` ) as `index`, repo_id, sha").
GroupBy("context_hash, repo_id, sha").OrderBy("max( `index` ) desc")
sess := db.GetEngine(ctx).Table(&CommitStatus{}).
Select("MAX(`index`) AS `index`, *").
Where(builder.Or(conds...)).
GroupBy("context_hash, repo_id, sha").OrderBy("MAX(`index`) DESC")
err := sess.Find(&results) err := sess.Find(&results)
if err != nil { if err != nil {
return nil, err return nil, err
@ -316,27 +307,9 @@ func GetLatestCommitStatusForPairs(ctx context.Context, repoSHAs []RepoSHA) (map
repoStatuses := make(map[int64][]*CommitStatus) repoStatuses := make(map[int64][]*CommitStatus)
if len(results) > 0 { // Group the statuses by repo ID
statuses := make([]*CommitStatus, 0, len(results)) for _, status := range results {
repoStatuses[status.RepoID] = append(repoStatuses[status.RepoID], status)
conds = make([]builder.Cond, 0, len(results))
for _, result := range results {
cond := builder.Eq{
"`index`": result.Index,
"repo_id": result.RepoID,
"sha": result.SHA,
}
conds = append(conds, cond)
}
err = getBase().Where(builder.Or(conds...)).Find(&statuses)
if err != nil {
return nil, err
}
// Group the statuses by repo ID
for _, status := range statuses {
repoStatuses[status.RepoID] = append(repoStatuses[status.RepoID], status)
}
} }
return repoStatuses, nil return repoStatuses, nil

View file

@ -35,8 +35,8 @@ func TestGetCommitStatuses(t *testing.T) {
SHA: sha1, SHA: sha1,
}) })
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 5, int(maxResults)) assert.EqualValues(t, 6, maxResults)
assert.Len(t, statuses, 5) assert.Len(t, statuses, 6)
assert.Equal(t, "ci/awesomeness", statuses[0].Context) assert.Equal(t, "ci/awesomeness", statuses[0].Context)
assert.Equal(t, structs.CommitStatusPending, statuses[0].State) assert.Equal(t, structs.CommitStatusPending, statuses[0].State)
@ -58,13 +58,17 @@ func TestGetCommitStatuses(t *testing.T) {
assert.Equal(t, structs.CommitStatusError, statuses[4].State) assert.Equal(t, structs.CommitStatusError, statuses[4].State)
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[4].APIURL(db.DefaultContext)) assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[4].APIURL(db.DefaultContext))
assert.Equal(t, "deploy/awesomeness", statuses[5].Context)
assert.Equal(t, structs.CommitStatusPending, statuses[5].State)
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[5].APIURL(db.DefaultContext))
statuses, maxResults, err = db.FindAndCount[git_model.CommitStatus](db.DefaultContext, &git_model.CommitStatusOptions{ statuses, maxResults, err = db.FindAndCount[git_model.CommitStatus](db.DefaultContext, &git_model.CommitStatusOptions{
ListOptions: db.ListOptions{Page: 2, PageSize: 50}, ListOptions: db.ListOptions{Page: 2, PageSize: 50},
RepoID: repo1.ID, RepoID: repo1.ID,
SHA: sha1, SHA: sha1,
}) })
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 5, int(maxResults)) assert.EqualValues(t, 6, maxResults)
assert.Empty(t, statuses) assert.Empty(t, statuses)
} }
@ -265,3 +269,148 @@ func TestCommitStatusesHideActionsURL(t *testing.T) {
assert.Empty(t, statuses[0].TargetURL) assert.Empty(t, statuses[0].TargetURL)
assert.Equal(t, "https://mycicd.org/1", statuses[1].TargetURL) assert.Equal(t, "https://mycicd.org/1", statuses[1].TargetURL)
} }
func TestGetLatestCommitStatusForPairs(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
t.Run("All", func(t *testing.T) {
pairs, err := git_model.GetLatestCommitStatusForPairs(db.DefaultContext, nil)
require.NoError(t, err)
assert.EqualValues(t, map[int64][]*git_model.CommitStatus{
1: {
{
ID: 7,
Index: 6,
RepoID: 1,
State: structs.CommitStatusPending,
SHA: "1234123412341234123412341234123412341234",
TargetURL: "https://example.com/builds/",
Description: "My awesome deploy service",
ContextHash: "ae9547713a6665fc4261d0756904932085a41cf2",
Context: "deploy/awesomeness",
CreatorID: 2,
},
{
ID: 4,
Index: 4,
State: structs.CommitStatusFailure,
TargetURL: "https://example.com/builds/",
Description: "My awesome CI-service",
Context: "ci/awesomeness",
CreatorID: 2,
RepoID: 1,
SHA: "1234123412341234123412341234123412341234",
ContextHash: "c65f4d64a3b14a3eced0c9b36799e66e1bd5ced7",
},
{
ID: 3,
Index: 3,
State: structs.CommitStatusSuccess,
TargetURL: "https://example.com/coverage/",
Description: "My awesome Coverage service",
Context: "cov/awesomeness",
CreatorID: 2,
RepoID: 1,
SHA: "1234123412341234123412341234123412341234",
ContextHash: "3929ac7bccd3fa1bf9b38ddedb77973b1b9a8cfe",
},
},
62: {
{
ID: 8,
Index: 2,
RepoID: 62,
State: structs.CommitStatusError,
TargetURL: "/user2/test_workflows/actions",
Description: "My awesome deploy service - v2",
Context: "deploy/awesomeness",
SHA: "774f93df12d14931ea93259ae93418da4482fcc1",
ContextHash: "ae9547713a6665fc4261d0756904932085a41cf2",
CreatorID: 2,
},
},
}, pairs)
})
t.Run("Repo 1", func(t *testing.T) {
pairs, err := git_model.GetLatestCommitStatusForPairs(db.DefaultContext, []git_model.RepoSHA{{1, "1234123412341234123412341234123412341234"}})
require.NoError(t, err)
assert.EqualValues(t, map[int64][]*git_model.CommitStatus{
1: {
{
ID: 7,
Index: 6,
RepoID: 1,
State: structs.CommitStatusPending,
SHA: "1234123412341234123412341234123412341234",
TargetURL: "https://example.com/builds/",
Description: "My awesome deploy service",
ContextHash: "ae9547713a6665fc4261d0756904932085a41cf2",
Context: "deploy/awesomeness",
CreatorID: 2,
},
{
ID: 4,
Index: 4,
State: structs.CommitStatusFailure,
TargetURL: "https://example.com/builds/",
Description: "My awesome CI-service",
Context: "ci/awesomeness",
CreatorID: 2,
RepoID: 1,
SHA: "1234123412341234123412341234123412341234",
ContextHash: "c65f4d64a3b14a3eced0c9b36799e66e1bd5ced7",
},
{
ID: 3,
Index: 3,
State: structs.CommitStatusSuccess,
TargetURL: "https://example.com/coverage/",
Description: "My awesome Coverage service",
Context: "cov/awesomeness",
CreatorID: 2,
RepoID: 1,
SHA: "1234123412341234123412341234123412341234",
ContextHash: "3929ac7bccd3fa1bf9b38ddedb77973b1b9a8cfe",
},
},
}, pairs)
})
t.Run("Repo 62", func(t *testing.T) {
pairs, err := git_model.GetLatestCommitStatusForPairs(db.DefaultContext, []git_model.RepoSHA{{62, "774f93df12d14931ea93259ae93418da4482fcc1"}})
require.NoError(t, err)
assert.EqualValues(t, map[int64][]*git_model.CommitStatus{
62: {
{
ID: 8,
Index: 2,
RepoID: 62,
State: structs.CommitStatusError,
TargetURL: "/user2/test_workflows/actions",
Description: "My awesome deploy service - v2",
Context: "deploy/awesomeness",
SHA: "774f93df12d14931ea93259ae93418da4482fcc1",
ContextHash: "ae9547713a6665fc4261d0756904932085a41cf2",
CreatorID: 2,
},
},
}, pairs)
})
t.Run("Repo 62 nonexistant sha", func(t *testing.T) {
pairs, err := git_model.GetLatestCommitStatusForPairs(db.DefaultContext, []git_model.RepoSHA{{62, "774f93df12d14931ea93259ae93418da4482fcc"}})
require.NoError(t, err)
assert.EqualValues(t, map[int64][]*git_model.CommitStatus{}, pairs)
})
t.Run("SHA with non existant repo id", func(t *testing.T) {
pairs, err := git_model.GetLatestCommitStatusForPairs(db.DefaultContext, []git_model.RepoSHA{{1, "774f93df12d14931ea93259ae93418da4482fcc1"}})
require.NoError(t, err)
assert.EqualValues(t, map[int64][]*git_model.CommitStatus{}, pairs)
})
}

View file

@ -0,0 +1,11 @@
-
id: 3
user_id: 1
issue_id: 2
created_unix: 1500988004
-
id: 4
user_id: 3
issue_id: 0
created_unix: 1500988003

View file

@ -60,34 +60,19 @@ func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, ex
return sw, exists, err return sw, exists, err
} }
// UserIDCount is a simple coalition of UserID and Count
type UserStopwatch struct {
UserID int64
StopWatches []*Stopwatch
}
// GetUIDsAndNotificationCounts between the two provided times // GetUIDsAndNotificationCounts between the two provided times
func GetUIDsAndStopwatch(ctx context.Context) ([]*UserStopwatch, error) { func GetUIDsAndStopwatch(ctx context.Context) (map[int64][]*Stopwatch, error) {
sws := []*Stopwatch{} sws := []*Stopwatch{}
if err := db.GetEngine(ctx).Where("issue_id != 0").Find(&sws); err != nil { if err := db.GetEngine(ctx).Where("issue_id != 0").Find(&sws); err != nil {
return nil, err return nil, err
} }
res := map[int64][]*Stopwatch{}
if len(sws) == 0 { if len(sws) == 0 {
return []*UserStopwatch{}, nil return res, nil
} }
lastUserID := int64(-1)
res := []*UserStopwatch{}
for _, sw := range sws { for _, sw := range sws {
if lastUserID == sw.UserID { res[sw.UserID] = append(res[sw.UserID], sw)
lastUserStopwatch := res[len(res)-1]
lastUserStopwatch.StopWatches = append(lastUserStopwatch.StopWatches, sw)
} else {
res = append(res, &UserStopwatch{
UserID: sw.UserID,
StopWatches: []*Stopwatch{sw},
})
}
} }
return res, nil return res, nil
} }

View file

@ -4,12 +4,14 @@
package issues_test package issues_test
import ( import (
"path/filepath"
"testing" "testing"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -77,3 +79,41 @@ func TestCreateOrStopIssueStopwatch(t *testing.T) {
unittest.AssertNotExistsBean(t, &issues_model.Stopwatch{UserID: 2, IssueID: 2}) unittest.AssertNotExistsBean(t, &issues_model.Stopwatch{UserID: 2, IssueID: 2})
unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{UserID: 2, IssueID: 2}) unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{UserID: 2, IssueID: 2})
} }
func TestGetUIDsAndStopwatch(t *testing.T) {
defer unittest.OverrideFixtures(
unittest.FixturesOptions{
Dir: filepath.Join(setting.AppWorkPath, "models/fixtures/"),
Base: setting.AppWorkPath,
Dirs: []string{"models/issues/TestGetUIDsAndStopwatch/"},
},
)()
require.NoError(t, unittest.PrepareTestDatabase())
uidStopwatches, err := issues_model.GetUIDsAndStopwatch(db.DefaultContext)
require.NoError(t, err)
assert.EqualValues(t, map[int64][]*issues_model.Stopwatch{
1: {
{
ID: 1,
UserID: 1,
IssueID: 1,
CreatedUnix: timeutil.TimeStamp(1500988001),
},
{
ID: 3,
UserID: 1,
IssueID: 2,
CreatedUnix: timeutil.TimeStamp(1500988004),
},
},
2: {
{
ID: 2,
UserID: 2,
IssueID: 2,
CreatedUnix: timeutil.TimeStamp(1500988002),
},
},
}, uidStopwatches)
}

View file

@ -0,0 +1,30 @@
-
id: 1001
owner_id: 33
owner_name: user33
lower_name: repo1001
name: repo1001
default_branch: main
num_watches: 0
num_stars: 0
num_forks: 0
num_issues: 0
num_closed_issues: 0
num_pulls: 0
num_closed_pulls: 0
num_milestones: 0
num_closed_milestones: 0
num_projects: 0
num_closed_projects: 0
is_private: false
is_empty: false
is_archived: false
is_mirror: false
status: 0
is_fork: false
fork_id: 0
is_template: false
template_id: 0
size: 0
is_fsck_enabled: true
close_issues_via_commit_in_any_branch: false

View file

@ -7,6 +7,7 @@ import (
"context" "context"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"xorm.io/builder" "xorm.io/builder"
@ -54,9 +55,9 @@ func GetUserFork(ctx context.Context, repoID, userID int64) (*Repository, error)
return &forkedRepo, nil return &forkedRepo, nil
} }
// GetForks returns all the forks of the repository // GetForks returns all the forks of the repository that are visible to the user.
func GetForks(ctx context.Context, repo *Repository, listOptions db.ListOptions) ([]*Repository, error) { func GetForks(ctx context.Context, repo *Repository, user *user_model.User, listOptions db.ListOptions) ([]*Repository, int64, error) {
sess := db.GetEngine(ctx) sess := db.GetEngine(ctx).Where(AccessibleRepositoryCondition(user, unit.TypeInvalid))
var forks []*Repository var forks []*Repository
if listOptions.Page == 0 { if listOptions.Page == 0 {
@ -66,7 +67,8 @@ func GetForks(ctx context.Context, repo *Repository, listOptions db.ListOptions)
sess = db.SetSessionPagination(sess, &listOptions) sess = db.SetSessionPagination(sess, &listOptions)
} }
return forks, sess.Find(&forks, &Repository{ForkID: repo.ID}) count, err := sess.FindAndCount(&forks, &Repository{ForkID: repo.ID})
return forks, count, err
} }
// IncrementRepoForkNum increment repository fork number // IncrementRepoForkNum increment repository fork number

View file

@ -641,12 +641,9 @@ func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) bu
// 1. Be able to see all non-private repositories that either: // 1. Be able to see all non-private repositories that either:
cond = cond.Or(builder.And( cond = cond.Or(builder.And(
builder.Eq{"`repository`.is_private": false}, builder.Eq{"`repository`.is_private": false},
// 2. Aren't in an private organisation or limited organisation if we're not logged in // 2. Aren't in an private organisation/user or limited organisation/user if the doer is not logged in.
builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where( builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where(
builder.And( builder.In("visibility", orgVisibilityLimit)))))
builder.Eq{"type": user_model.UserTypeOrganization},
builder.In("visibility", orgVisibilityLimit)),
))))
} }
if user != nil { if user != nil {

View file

@ -4,13 +4,18 @@
package repo_test package repo_test
import ( import (
"path/filepath"
"slices"
"strings" "strings"
"testing" "testing"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -403,3 +408,43 @@ func TestSearchRepositoryByTopicName(t *testing.T) {
}) })
} }
} }
func TestSearchRepositoryIDsByCondition(t *testing.T) {
defer unittest.OverrideFixtures(
unittest.FixturesOptions{
Dir: filepath.Join(setting.AppWorkPath, "models/fixtures/"),
Base: setting.AppWorkPath,
Dirs: []string{"models/repo/TestSearchRepositoryIDsByCondition/"},
},
)()
require.NoError(t, unittest.PrepareTestDatabase())
// Sanity check of the database
limitedUser := unittest.AssertExistsAndLoadBean(t, &user.User{ID: 33, Visibility: structs.VisibleTypeLimited})
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1001, OwnerID: limitedUser.ID})
testCases := []struct {
user *user.User
repoIDs []int64
}{
{
user: nil,
repoIDs: []int64{1, 4, 8, 9, 10, 11, 12, 14, 17, 18, 21, 23, 25, 27, 29, 32, 33, 34, 35, 36, 37, 42, 44, 45, 46, 47, 48, 49, 50, 51, 53, 57, 58, 60, 61, 62, 1059},
},
{
user: unittest.AssertExistsAndLoadBean(t, &user.User{ID: 4}),
repoIDs: []int64{1, 3, 4, 8, 9, 10, 11, 12, 14, 17, 18, 21, 23, 25, 27, 29, 32, 33, 34, 35, 36, 37, 38, 40, 42, 44, 45, 46, 47, 48, 49, 50, 51, 53, 57, 58, 60, 61, 62, 1001, 1059},
},
{
user: unittest.AssertExistsAndLoadBean(t, &user.User{ID: 5}),
repoIDs: []int64{1, 4, 8, 9, 10, 11, 12, 14, 17, 18, 21, 23, 25, 27, 29, 32, 33, 34, 35, 36, 37, 38, 40, 42, 44, 45, 46, 47, 48, 49, 50, 51, 53, 57, 58, 60, 61, 62, 1001, 1059},
},
}
for _, testCase := range testCases {
repoIDs, err := repo_model.FindUserCodeAccessibleRepoIDs(db.DefaultContext, testCase.user)
require.NoError(t, err)
slices.Sort(repoIDs)
assert.EqualValues(t, testCase.repoIDs, repoIDs)
}
}

View file

@ -75,26 +75,28 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us
return nil, err return nil, err
} }
additionalUserIDs := make([]int64, 0, 10)
if err = e.Table("team_user").
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))",
repo.ID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests).
Distinct("`team_user`.uid").
Select("`team_user`.uid").
Find(&additionalUserIDs); err != nil {
return nil, err
}
uniqueUserIDs := make(container.Set[int64]) uniqueUserIDs := make(container.Set[int64])
uniqueUserIDs.AddMultiple(userIDs...) uniqueUserIDs.AddMultiple(userIDs...)
uniqueUserIDs.AddMultiple(additionalUserIDs...)
if repo.Owner.IsOrganization() {
additionalUserIDs := make([]int64, 0, 10)
if err = e.Table("team_user").
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))",
repo.ID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests).
Distinct("`team_user`.uid").
Select("`team_user`.uid").
Find(&additionalUserIDs); err != nil {
return nil, err
}
uniqueUserIDs.AddMultiple(additionalUserIDs...)
}
// Leave a seat for owner itself to append later, but if owner is an organization // Leave a seat for owner itself to append later, but if owner is an organization
// and just waste 1 unit is cheaper than re-allocate memory once. // and just waste 1 unit is cheaper than re-allocate memory once.
users := make([]*user_model.User, 0, len(uniqueUserIDs)+1) users := make([]*user_model.User, 0, len(uniqueUserIDs)+1)
if len(userIDs) > 0 { if len(uniqueUserIDs) > 0 {
if err = e.In("id", uniqueUserIDs.Values()). if err = e.In("id", uniqueUserIDs.Values()).
Where(builder.Eq{"`user`.is_active": true}). Where(builder.Eq{"`user`.is_active": true}).
OrderBy(user_model.GetOrderByName()). OrderBy(user_model.GetOrderByName()).

View file

@ -8,10 +8,8 @@ import (
"context" "context"
"fmt" "fmt"
"strings" "strings"
"time"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -141,6 +139,38 @@ func GetPrimaryEmailAddressOfUser(ctx context.Context, uid int64) (*EmailAddress
return ea, nil return ea, nil
} }
// Deletes the primary email address of the user
// This is only allowed if the user is a organization
func DeletePrimaryEmailAddressOfUser(ctx context.Context, uid int64) error {
user, err := GetUserByID(ctx, uid)
if err != nil {
return err
}
if user.Type != UserTypeOrganization {
return fmt.Errorf("%s is not a organization", user.Name)
}
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
_, err = db.GetEngine(ctx).Exec("DELETE FROM email_address WHERE uid = ? AND is_primary = true", uid)
if err != nil {
return err
}
user.Email = ""
err = UpdateUserCols(ctx, user, "email")
if err != nil {
return err
}
return committer.Commit()
}
// GetEmailAddresses returns all email addresses belongs to given user. // GetEmailAddresses returns all email addresses belongs to given user.
func GetEmailAddresses(ctx context.Context, uid int64) ([]*EmailAddress, error) { func GetEmailAddresses(ctx context.Context, uid int64) ([]*EmailAddress, error) {
emails := make([]*EmailAddress, 0, 5) emails := make([]*EmailAddress, 0, 5)
@ -246,23 +276,6 @@ func updateActivation(ctx context.Context, email *EmailAddress, activate bool) e
return UpdateUserCols(ctx, user, "rands") return UpdateUserCols(ctx, user, "rands")
} }
// VerifyActiveEmailCode verifies active email code when active account
func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddress {
if user := GetVerifyUser(ctx, code); user != nil {
// time limit code
prefix := code[:base.TimeLimitCodeLength]
data := fmt.Sprintf("%d%s%s%s%s", user.ID, email, user.LowerName, user.Passwd, user.Rands)
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
emailAddress := &EmailAddress{UID: user.ID, Email: email}
if has, _ := db.GetEngine(ctx).Get(emailAddress); has {
return emailAddress
}
}
}
return nil
}
// SearchEmailOrderBy is used to sort the results from SearchEmails() // SearchEmailOrderBy is used to sort the results from SearchEmails()
type SearchEmailOrderBy string type SearchEmailOrderBy string

View file

@ -163,3 +163,21 @@ func TestGetActivatedEmailAddresses(t *testing.T) {
}) })
} }
} }
func TestDeletePrimaryEmailAddressOfUser(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
user, err := user_model.GetUserByName(db.DefaultContext, "org3")
require.NoError(t, err)
assert.Equal(t, "org3@example.com", user.Email)
require.NoError(t, user_model.DeletePrimaryEmailAddressOfUser(db.DefaultContext, user.ID))
user, err = user_model.GetUserByName(db.DefaultContext, "org3")
require.NoError(t, err)
assert.Empty(t, user.Email)
email, err := user_model.GetPrimaryEmailAddressOfUser(db.DefaultContext, user.ID)
assert.True(t, user_model.IsErrEmailAddressNotExist(err))
assert.Nil(t, email)
}

View file

@ -7,7 +7,9 @@ package user
import ( import (
"context" "context"
"crypto/subtle"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"net/mail" "net/mail"
"net/url" "net/url"
@ -318,15 +320,14 @@ func (u *User) OrganisationLink() string {
return setting.AppSubURL + "/org/" + url.PathEscape(u.Name) return setting.AppSubURL + "/org/" + url.PathEscape(u.Name)
} }
// GenerateEmailActivateCode generates an activate code based on user information and given e-mail. // GenerateEmailAuthorizationCode generates an activation code based for the user for the specified purpose.
func (u *User) GenerateEmailActivateCode(email string) string { // The standard expiry is ActiveCodeLives minutes.
code := base.CreateTimeLimitCode( func (u *User) GenerateEmailAuthorizationCode(ctx context.Context, purpose auth.AuthorizationPurpose) (string, error) {
fmt.Sprintf("%d%s%s%s%s", u.ID, email, u.LowerName, u.Passwd, u.Rands), lookup, validator, err := auth.GenerateAuthToken(ctx, u.ID, timeutil.TimeStampNow().Add(int64(setting.Service.ActiveCodeLives)*60), purpose)
setting.Service.ActiveCodeLives, time.Now(), nil) if err != nil {
return "", err
// Add tail hex username }
code += hex.EncodeToString([]byte(u.LowerName)) return lookup + ":" + validator, nil
return code
} }
// GetUserFollowers returns range of user's followers. // GetUserFollowers returns range of user's followers.
@ -838,35 +839,50 @@ func countUsers(ctx context.Context, opts *CountUserFilter) int64 {
return count return count
} }
// GetVerifyUser get user by verify code // VerifyUserActiveCode verifies that the code is valid for the given purpose for this user.
func GetVerifyUser(ctx context.Context, code string) (user *User) { // If delete is specified, the token will be deleted.
if len(code) <= base.TimeLimitCodeLength { func VerifyUserAuthorizationToken(ctx context.Context, code string, purpose auth.AuthorizationPurpose, delete bool) (*User, error) {
return nil lookupKey, validator, found := strings.Cut(code, ":")
if !found {
return nil, nil
} }
// use tail hex username query user authToken, err := auth.FindAuthToken(ctx, lookupKey, purpose)
hexStr := code[base.TimeLimitCodeLength:] if err != nil {
if b, err := hex.DecodeString(hexStr); err == nil { if errors.Is(err, util.ErrNotExist) {
if user, err = GetUserByName(ctx, string(b)); user != nil { return nil, nil
return user
} }
log.Error("user.getVerifyUser: %v", err) return nil, err
} }
return nil if authToken.IsExpired() {
} return nil, auth.DeleteAuthToken(ctx, authToken)
}
// VerifyUserActiveCode verifies active code when active account rawValidator, err := hex.DecodeString(validator)
func VerifyUserActiveCode(ctx context.Context, code string) (user *User) { if err != nil {
if user = GetVerifyUser(ctx, code); user != nil { return nil, err
// time limit code }
prefix := code[:base.TimeLimitCodeLength]
data := fmt.Sprintf("%d%s%s%s%s", user.ID, user.Email, user.LowerName, user.Passwd, user.Rands) if subtle.ConstantTimeCompare([]byte(authToken.HashedValidator), []byte(auth.HashValidator(rawValidator))) == 0 {
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) { return nil, errors.New("validator doesn't match")
return user }
u, err := GetUserByID(ctx, authToken.UID)
if err != nil {
if IsErrUserNotExist(err) {
return nil, nil
}
return nil, err
}
if delete {
if err := auth.DeleteAuthToken(ctx, authToken); err != nil {
return nil, err
} }
} }
return nil
return u, nil
} }
// ValidateUser check if user is valid to insert / update into database // ValidateUser check if user is valid to insert / update into database

View file

@ -7,6 +7,7 @@ package user_test
import ( import (
"context" "context"
"crypto/rand" "crypto/rand"
"encoding/hex"
"fmt" "fmt"
"strings" "strings"
"testing" "testing"
@ -21,7 +22,9 @@ import (
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
@ -700,3 +703,66 @@ func TestDisabledUserFeatures(t *testing.T) {
assert.True(t, user_model.IsFeatureDisabledWithLoginType(user, f)) assert.True(t, user_model.IsFeatureDisabledWithLoginType(user, f))
} }
} }
func TestGenerateEmailAuthorizationCode(t *testing.T) {
defer test.MockVariableValue(&setting.Service.ActiveCodeLives, 2)()
require.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
code, err := user.GenerateEmailAuthorizationCode(db.DefaultContext, auth.UserActivation)
require.NoError(t, err)
lookupKey, validator, ok := strings.Cut(code, ":")
assert.True(t, ok)
rawValidator, err := hex.DecodeString(validator)
require.NoError(t, err)
authToken, err := auth.FindAuthToken(db.DefaultContext, lookupKey, auth.UserActivation)
require.NoError(t, err)
assert.False(t, authToken.IsExpired())
assert.EqualValues(t, authToken.HashedValidator, auth.HashValidator(rawValidator))
authToken.Expiry = authToken.Expiry.Add(-int64(setting.Service.ActiveCodeLives) * 60)
assert.True(t, authToken.IsExpired())
}
func TestVerifyUserAuthorizationToken(t *testing.T) {
defer test.MockVariableValue(&setting.Service.ActiveCodeLives, 2)()
require.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
code, err := user.GenerateEmailAuthorizationCode(db.DefaultContext, auth.UserActivation)
require.NoError(t, err)
lookupKey, _, ok := strings.Cut(code, ":")
assert.True(t, ok)
t.Run("Wrong purpose", func(t *testing.T) {
u, err := user_model.VerifyUserAuthorizationToken(db.DefaultContext, code, auth.PasswordReset, false)
require.NoError(t, err)
assert.Nil(t, u)
})
t.Run("No delete", func(t *testing.T) {
u, err := user_model.VerifyUserAuthorizationToken(db.DefaultContext, code, auth.UserActivation, false)
require.NoError(t, err)
assert.EqualValues(t, user.ID, u.ID)
authToken, err := auth.FindAuthToken(db.DefaultContext, lookupKey, auth.UserActivation)
require.NoError(t, err)
assert.NotNil(t, authToken)
})
t.Run("Delete", func(t *testing.T) {
u, err := user_model.VerifyUserAuthorizationToken(db.DefaultContext, code, auth.UserActivation, true)
require.NoError(t, err)
assert.EqualValues(t, user.ID, u.ID)
authToken, err := auth.FindAuthToken(db.DefaultContext, lookupKey, auth.UserActivation)
require.ErrorIs(t, err, util.ErrNotExist)
assert.Nil(t, authToken)
})
}

View file

@ -4,26 +4,20 @@
package base package base
import ( import (
"crypto/hmac"
"crypto/sha1"
"crypto/sha256" "crypto/sha256"
"crypto/subtle"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"hash"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"time"
"unicode/utf8" "unicode/utf8"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
) )
@ -54,66 +48,6 @@ func BasicAuthDecode(encoded string) (string, string, error) {
return "", "", errors.New("invalid basic authentication") return "", "", errors.New("invalid basic authentication")
} }
// VerifyTimeLimitCode verify time limit code
func VerifyTimeLimitCode(now time.Time, data string, minutes int, code string) bool {
if len(code) <= 18 {
return false
}
startTimeStr := code[:12]
aliveTimeStr := code[12:18]
aliveTime, _ := strconv.Atoi(aliveTimeStr) // no need to check err, if anything wrong, the following code check will fail soon
// check code
retCode := CreateTimeLimitCode(data, aliveTime, startTimeStr, nil)
if subtle.ConstantTimeCompare([]byte(retCode), []byte(code)) != 1 {
retCode = CreateTimeLimitCode(data, aliveTime, startTimeStr, sha1.New()) // TODO: this is only for the support of legacy codes, remove this in/after 1.23
if subtle.ConstantTimeCompare([]byte(retCode), []byte(code)) != 1 {
return false
}
}
// check time is expired or not: startTime <= now && now < startTime + minutes
startTime, _ := time.ParseInLocation("200601021504", startTimeStr, time.Local)
return (startTime.Before(now) || startTime.Equal(now)) && now.Before(startTime.Add(time.Minute*time.Duration(minutes)))
}
// TimeLimitCodeLength default value for time limit code
const TimeLimitCodeLength = 12 + 6 + 40
// CreateTimeLimitCode create a time-limited code.
// Format: 12 length date time string + 6 minutes string (not used) + 40 hash string, some other code depends on this fixed length
// If h is nil, then use the default hmac hash.
func CreateTimeLimitCode[T time.Time | string](data string, minutes int, startTimeGeneric T, h hash.Hash) string {
const format = "200601021504"
var start time.Time
var startTimeAny any = startTimeGeneric
if t, ok := startTimeAny.(time.Time); ok {
start = t
} else {
var err error
start, err = time.ParseInLocation(format, startTimeAny.(string), time.Local)
if err != nil {
return "" // return an invalid code because the "parse" failed
}
}
startStr := start.Format(format)
end := start.Add(time.Minute * time.Duration(minutes))
if h == nil {
h = hmac.New(sha1.New, setting.GetGeneralTokenSigningSecret())
}
_, _ = fmt.Fprintf(h, "%s%s%s%s%d", data, hex.EncodeToString(setting.GetGeneralTokenSigningSecret()), startStr, end.Format(format), minutes)
encoded := hex.EncodeToString(h.Sum(nil))
code := fmt.Sprintf("%s%06d%s", startStr, minutes, encoded)
if len(code) != TimeLimitCodeLength {
panic("there is a hard requirement for the length of time-limited code") // it shouldn't happen
}
return code
}
// FileSize calculates the file size and generate user-friendly string. // FileSize calculates the file size and generate user-friendly string.
func FileSize(s int64) string { func FileSize(s int64) string {
return humanize.IBytes(uint64(s)) return humanize.IBytes(uint64(s))

View file

@ -4,13 +4,7 @@
package base package base
import ( import (
"crypto/sha1"
"fmt"
"testing" "testing"
"time"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -46,57 +40,6 @@ func TestBasicAuthDecode(t *testing.T) {
require.Error(t, err) require.Error(t, err)
} }
func TestVerifyTimeLimitCode(t *testing.T) {
defer test.MockVariableValue(&setting.InstallLock, true)()
initGeneralSecret := func(secret string) {
setting.InstallLock = true
setting.CfgProvider, _ = setting.NewConfigProviderFromData(fmt.Sprintf(`
[oauth2]
JWT_SECRET = %s
`, secret))
setting.LoadCommonSettings()
}
initGeneralSecret("KZb_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko")
now := time.Now()
t.Run("TestGenericParameter", func(t *testing.T) {
time2000 := time.Date(2000, 1, 2, 3, 4, 5, 0, time.Local)
assert.Equal(t, "2000010203040000026fa5221b2731b7cf80b1b506f5e39e38c115fee5", CreateTimeLimitCode("test-sha1", 2, time2000, sha1.New()))
assert.Equal(t, "2000010203040000026fa5221b2731b7cf80b1b506f5e39e38c115fee5", CreateTimeLimitCode("test-sha1", 2, "200001020304", sha1.New()))
assert.Equal(t, "2000010203040000024842227a2f87041ff82025199c0187410a9297bf", CreateTimeLimitCode("test-hmac", 2, time2000, nil))
assert.Equal(t, "2000010203040000024842227a2f87041ff82025199c0187410a9297bf", CreateTimeLimitCode("test-hmac", 2, "200001020304", nil))
})
t.Run("TestInvalidCode", func(t *testing.T) {
assert.False(t, VerifyTimeLimitCode(now, "data", 2, ""))
assert.False(t, VerifyTimeLimitCode(now, "data", 2, "invalid code"))
})
t.Run("TestCreateAndVerify", func(t *testing.T) {
code := CreateTimeLimitCode("data", 2, now, nil)
assert.False(t, VerifyTimeLimitCode(now.Add(-time.Minute), "data", 2, code)) // not started yet
assert.True(t, VerifyTimeLimitCode(now, "data", 2, code))
assert.True(t, VerifyTimeLimitCode(now.Add(time.Minute), "data", 2, code))
assert.False(t, VerifyTimeLimitCode(now.Add(time.Minute), "DATA", 2, code)) // invalid data
assert.False(t, VerifyTimeLimitCode(now.Add(2*time.Minute), "data", 2, code)) // expired
})
t.Run("TestDifferentSecret", func(t *testing.T) {
// use another secret to ensure the code is invalid for different secret
verifyDataCode := func(c string) bool {
return VerifyTimeLimitCode(now, "data", 2, c)
}
code1 := CreateTimeLimitCode("data", 2, now, sha1.New())
code2 := CreateTimeLimitCode("data", 2, now, nil)
assert.True(t, verifyDataCode(code1))
assert.True(t, verifyDataCode(code2))
initGeneralSecret("000_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko")
assert.False(t, verifyDataCode(code1))
assert.False(t, verifyDataCode(code2))
})
}
func TestFileSize(t *testing.T) { func TestFileSize(t *testing.T) {
var size int64 = 512 var size int64 = 512
assert.Equal(t, "512 B", FileSize(size)) assert.Equal(t, "512 B", FileSize(size))

View file

@ -90,8 +90,8 @@ loop:
return return
} }
for _, userStopwatches := range usersStopwatches { for uid, stopwatches := range usersStopwatches {
apiSWs, err := convert.ToStopWatches(ctx, userStopwatches.StopWatches) apiSWs, err := convert.ToStopWatches(ctx, stopwatches)
if err != nil { if err != nil {
if !issues_model.IsErrIssueNotExist(err) { if !issues_model.IsErrIssueNotExist(err) {
log.Error("Unable to APIFormat stopwatches: %v", err) log.Error("Unable to APIFormat stopwatches: %v", err)
@ -103,7 +103,7 @@ loop:
log.Error("Unable to marshal stopwatches: %v", err) log.Error("Unable to marshal stopwatches: %v", err)
continue continue
} }
m.SendMessage(userStopwatches.UserID, &Event{ m.SendMessage(uid, &Event{
Name: "stopwatches", Name: "stopwatches",
Data: string(dataBs), Data: string(dataBs),
}) })

View file

@ -6,6 +6,7 @@ package git
import ( import (
"context" "context"
"io" "io"
"os"
"strings" "strings"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -97,3 +98,41 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note)
return nil return nil
} }
func SetNote(ctx context.Context, repo *Repository, commitID, notes, doerName, doerEmail string) error {
_, err := repo.GetCommit(commitID)
if err != nil {
return err
}
env := append(os.Environ(),
"GIT_AUTHOR_NAME="+doerName,
"GIT_AUTHOR_EMAIL="+doerEmail,
"GIT_COMMITTER_NAME="+doerName,
"GIT_COMMITTER_EMAIL="+doerEmail,
)
cmd := NewCommand(ctx, "notes", "add", "-f", "-m")
cmd.AddDynamicArguments(notes, commitID)
_, stderr, err := cmd.RunStdString(&RunOpts{Dir: repo.Path, Env: env})
if err != nil {
log.Error("Error while running git notes add: %s", stderr)
return err
}
return nil
}
func RemoveNote(ctx context.Context, repo *Repository, commitID string) error {
cmd := NewCommand(ctx, "notes", "remove")
cmd.AddDynamicArguments(commitID)
_, stderr, err := cmd.RunStdString(&RunOpts{Dir: repo.Path})
if err != nil {
log.Error("Error while running git notes remove: %s", stderr)
return err
}
return nil
}

View file

@ -1,25 +1,38 @@
// Copyright 2019 The Gitea Authors. All rights reserved. // Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package git package git_test
import ( import (
"context" "context"
"os"
"path/filepath" "path/filepath"
"testing" "testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/git"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
const (
testReposDir = "tests/repos/"
)
// openRepositoryWithDefaultContext opens the repository at the given path with DefaultContext.
func openRepositoryWithDefaultContext(repoPath string) (*git.Repository, error) {
return git.OpenRepository(git.DefaultContext, repoPath)
}
func TestGetNotes(t *testing.T) { func TestGetNotes(t *testing.T) {
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
bareRepo1, err := openRepositoryWithDefaultContext(bareRepo1Path) bareRepo1, err := openRepositoryWithDefaultContext(bareRepo1Path)
require.NoError(t, err) require.NoError(t, err)
defer bareRepo1.Close() defer bareRepo1.Close()
note := Note{} note := git.Note{}
err = GetNote(context.Background(), bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653", &note) err = git.GetNote(context.Background(), bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653", &note)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, []byte("Note contents\n"), note.Message) assert.Equal(t, []byte("Note contents\n"), note.Message)
assert.Equal(t, "Vladimir Panteleev", note.Commit.Author.Name) assert.Equal(t, "Vladimir Panteleev", note.Commit.Author.Name)
@ -31,11 +44,11 @@ func TestGetNestedNotes(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
defer repo.Close() defer repo.Close()
note := Note{} note := git.Note{}
err = GetNote(context.Background(), repo, "3e668dbfac39cbc80a9ff9c61eb565d944453ba4", &note) err = git.GetNote(context.Background(), repo, "3e668dbfac39cbc80a9ff9c61eb565d944453ba4", &note)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, []byte("Note 2"), note.Message) assert.Equal(t, []byte("Note 2"), note.Message)
err = GetNote(context.Background(), repo, "ba0a96fa63532d6c5087ecef070b0250ed72fa47", &note) err = git.GetNote(context.Background(), repo, "ba0a96fa63532d6c5087ecef070b0250ed72fa47", &note)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, []byte("Note 1"), note.Message) assert.Equal(t, []byte("Note 1"), note.Message)
} }
@ -46,8 +59,48 @@ func TestGetNonExistentNotes(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
defer bareRepo1.Close() defer bareRepo1.Close()
note := Note{} note := git.Note{}
err = GetNote(context.Background(), bareRepo1, "non_existent_sha", &note) err = git.GetNote(context.Background(), bareRepo1, "non_existent_sha", &note)
require.Error(t, err) require.Error(t, err)
assert.IsType(t, ErrNotExist{}, err) assert.IsType(t, git.ErrNotExist{}, err)
}
func TestSetNote(t *testing.T) {
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
tempDir, err := os.MkdirTemp("", "")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
require.NoError(t, unittest.CopyDir(bareRepo1Path, filepath.Join(tempDir, "repo1")))
bareRepo1, err := openRepositoryWithDefaultContext(filepath.Join(tempDir, "repo1"))
require.NoError(t, err)
defer bareRepo1.Close()
require.NoError(t, git.SetNote(context.Background(), bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653", "This is a new note", "Test", "test@test.com"))
note := git.Note{}
err = git.GetNote(context.Background(), bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653", &note)
require.NoError(t, err)
assert.Equal(t, []byte("This is a new note\n"), note.Message)
assert.Equal(t, "Test", note.Commit.Author.Name)
}
func TestRemoveNote(t *testing.T) {
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
tempDir := t.TempDir()
require.NoError(t, unittest.CopyDir(bareRepo1Path, filepath.Join(tempDir, "repo1")))
bareRepo1, err := openRepositoryWithDefaultContext(filepath.Join(tempDir, "repo1"))
require.NoError(t, err)
defer bareRepo1.Close()
require.NoError(t, git.RemoveNote(context.Background(), bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653"))
note := git.Note{}
err = git.GetNote(context.Background(), bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653", &note)
require.Error(t, err)
assert.IsType(t, git.ErrNotExist{}, err)
} }

View file

@ -254,7 +254,7 @@ func TestGitAttributeCheckerError(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
_, err = ac.CheckPath("i-am-a-python.p") _, err = ac.CheckPath("i-am-a-python.p")
require.ErrorIs(t, err, context.Canceled) require.Error(t, err)
}) })
t.Run("Cancelled/DuringRun", func(t *testing.T) { t.Run("Cancelled/DuringRun", func(t *testing.T) {

View file

@ -97,7 +97,7 @@ func createDefaultPolicy() *bluemonday.Policy {
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(ref-issue( ref-external-issue)?|mention)$`)).OnElements("a") policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(ref-issue( ref-external-issue)?|mention)$`)).OnElements("a")
// Allow classes for task lists // Allow classes for task lists
policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list-item`)).OnElements("li") policy.AllowAttrs("class").Matching(regexp.MustCompile(`^task-list-item$`)).OnElements("li")
// Allow classes for org mode list item status. // Allow classes for org mode list item status.
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li") policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li")
@ -106,7 +106,7 @@ func createDefaultPolicy() *bluemonday.Policy {
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i") policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i")
// Allow classes for emojis // Allow classes for emojis
policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img") policy.AllowAttrs("class").Matching(regexp.MustCompile(`^emoji$`)).OnElements("img")
// Allow icons, emojis, chroma syntax and keyword markup on span // Allow icons, emojis, chroma syntax and keyword markup on span
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span") policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span")
@ -123,13 +123,13 @@ func createDefaultPolicy() *bluemonday.Policy {
policy.AllowAttrs("class").Matching(regexp.MustCompile("^header$")).OnElements("div") policy.AllowAttrs("class").Matching(regexp.MustCompile("^header$")).OnElements("div")
policy.AllowAttrs("data-line-number").Matching(regexp.MustCompile("^[0-9]+$")).OnElements("span") policy.AllowAttrs("data-line-number").Matching(regexp.MustCompile("^[0-9]+$")).OnElements("span")
policy.AllowAttrs("class").Matching(regexp.MustCompile("^text small grey$")).OnElements("span") policy.AllowAttrs("class").Matching(regexp.MustCompile("^text small grey$")).OnElements("span")
policy.AllowAttrs("class").Matching(regexp.MustCompile("^file-preview*")).OnElements("table") policy.AllowAttrs("class").Matching(regexp.MustCompile("^file-preview$")).OnElements("table")
policy.AllowAttrs("class").Matching(regexp.MustCompile("^lines-escape$")).OnElements("td") policy.AllowAttrs("class").Matching(regexp.MustCompile("^lines-escape$")).OnElements("td")
policy.AllowAttrs("class").Matching(regexp.MustCompile("^toggle-escape-button btn interact-bg$")).OnElements("button") policy.AllowAttrs("class").Matching(regexp.MustCompile("^toggle-escape-button btn interact-bg$")).OnElements("button")
policy.AllowAttrs("title").OnElements("button") policy.AllowAttrs("title").OnElements("button")
policy.AllowAttrs("class").Matching(regexp.MustCompile("^ambiguous-code-point$")).OnElements("span") policy.AllowAttrs("class").Matching(regexp.MustCompile("^ambiguous-code-point$")).OnElements("span")
policy.AllowAttrs("data-tooltip-content").OnElements("span") policy.AllowAttrs("data-tooltip-content").OnElements("span")
policy.AllowAttrs("class").Matching(regexp.MustCompile("muted|(text black)")).OnElements("a") policy.AllowAttrs("class").Matching(regexp.MustCompile("^muted|(text black)$")).OnElements("a")
policy.AllowAttrs("class").Matching(regexp.MustCompile("^ui warning message tw-text-left$")).OnElements("div") policy.AllowAttrs("class").Matching(regexp.MustCompile("^ui warning message tw-text-left$")).OnElements("div")
// Allow generally safe attributes // Allow generally safe attributes

View file

@ -4,6 +4,8 @@
package setting package setting
import ( import (
"fmt"
"sort"
"strings" "strings"
"testing" "testing"
@ -12,6 +14,7 @@ import (
"github.com/gobwas/glob" "github.com/gobwas/glob"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/net/publicsuffix"
) )
func match(globs []glob.Glob, s string) bool { func match(globs []glob.Glob, s string) bool {
@ -93,11 +96,34 @@ func TestLoadServiceBlockDisposableWithExistingGlobs(t *testing.T) {
Service = oldService Service = oldService
}() }()
cfg, err := NewConfigProviderFromData(` tldCounts := make(map[string]int)
for _, domain := range DisposableEmailDomains() {
tld, _ := publicsuffix.PublicSuffix(domain)
tldCounts[tld]++
}
type tldkv struct {
Tld string
Count int
}
sortedTldCounts := make([]tldkv, 0)
for tld, count := range tldCounts {
sortedTldCounts = append(sortedTldCounts, tldkv{tld, count})
}
sort.Slice(sortedTldCounts, func(i, j int) bool {
return sortedTldCounts[i].Count > sortedTldCounts[j].Count
})
require.GreaterOrEqual(t, len(sortedTldCounts), 2)
blockString := fmt.Sprintf("*.%s,*.%s", sortedTldCounts[0].Tld, sortedTldCounts[1].Tld)
cfg, err := NewConfigProviderFromData(fmt.Sprintf(`
[service] [service]
EMAIL_DOMAIN_BLOCKLIST = *.ru,*.org EMAIL_DOMAIN_BLOCKLIST = %s
EMAIL_DOMAIN_BLOCK_DISPOSABLE = true EMAIL_DOMAIN_BLOCK_DISPOSABLE = true
`) `, blockString))
require.NoError(t, err) require.NoError(t, err)
loadServiceFrom(cfg) loadServiceFrom(cfg)
@ -108,7 +134,8 @@ EMAIL_DOMAIN_BLOCK_DISPOSABLE = true
redundant := 0 redundant := 0
for _, val := range DisposableEmailDomains() { for _, val := range DisposableEmailDomains() {
if strings.HasSuffix(val, ".ru") || strings.HasSuffix(val, ".org") { if strings.HasSuffix(val, sortedTldCounts[0].Tld) ||
strings.HasSuffix(val, sortedTldCounts[1].Tld) {
redundant++ redundant++
} }
} }

View file

@ -47,11 +47,11 @@ type CreateOrgOption struct {
// EditOrgOption options for editing an organization // EditOrgOption options for editing an organization
type EditOrgOption struct { type EditOrgOption struct {
FullName string `json:"full_name" binding:"MaxSize(100)"` FullName string `json:"full_name" binding:"MaxSize(100)"`
Email string `json:"email" binding:"MaxSize(255)"` Email *string `json:"email" binding:"MaxSize(255)"`
Description string `json:"description" binding:"MaxSize(255)"` Description string `json:"description" binding:"MaxSize(255)"`
Website string `json:"website" binding:"ValidUrl;MaxSize(255)"` Website string `json:"website" binding:"ValidUrl;MaxSize(255)"`
Location string `json:"location" binding:"MaxSize(50)"` Location string `json:"location" binding:"MaxSize(50)"`
// possible values are `public`, `limited` or `private` // possible values are `public`, `limited` or `private`
// enum: ["public", "limited", "private"] // enum: ["public", "limited", "private"]
Visibility string `json:"visibility" binding:"In(,public,limited,private)"` Visibility string `json:"visibility" binding:"In(,public,limited,private)"`

View file

@ -8,3 +8,7 @@ type Note struct {
Message string `json:"message"` Message string `json:"message"`
Commit *Commit `json:"commit"` Commit *Commit `json:"commit"`
} }
type NoteOptions struct {
Message string `json:"message"`
}

View file

@ -2622,6 +2622,9 @@ diff.browse_source = Browse source
diff.parent = parent diff.parent = parent
diff.commit = commit diff.commit = commit
diff.git-notes = Notes diff.git-notes = Notes
diff.git-notes.add = Add note
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.data_not_available = Diff content is not available
diff.options_button = Diff options diff.options_button = Diff options
diff.show_diff_stats = Show stats diff.show_diff_stats = Show stats
@ -3837,8 +3840,8 @@ runs.actors_no_select = All actors
runs.status_no_select = All status runs.status_no_select = All status
runs.no_results = No results matched. runs.no_results = No results matched.
runs.no_workflows = There are no workflows yet. runs.no_workflows = There are no workflows yet.
runs.no_workflows.quick_start = Don't know how to start with Forgejo Actions? See <a target="_blank" rel="noopener noreferrer" href="%s">the quick start guide</a>. runs.no_workflows.help_write_access = Don't know how to start with Forgejo Actions? Check out the <a target="_blank" rel="noopener noreferrer" href="%s">quick start in the user documentation</a> to write your first workflow, then <a target="_blank" rel="noopener noreferrer" href="%s">set up a Forgejo runner</a> to execute your jobs.
runs.no_workflows.documentation = For more information on Forgejo Actions, see <a target="_blank" rel="noopener noreferrer" href="%s">the documentation</a>. runs.no_workflows.help_no_write_access = To learn about Forgejo Actions, see <a target="_blank" rel="noopener noreferrer" href="%s">the documentation</a>.
runs.no_runs = The workflow has no runs yet. runs.no_runs = The workflow has no runs yet.
runs.empty_commit_message = (empty commit message) runs.empty_commit_message = (empty commit message)
runs.expire_log_message = Logs have been purged because they were too old. runs.expire_log_message = Logs have been purged because they were too old.

896
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -17,7 +17,7 @@
"asciinema-player": "3.8.0", "asciinema-player": "3.8.0",
"chart.js": "4.4.5", "chart.js": "4.4.5",
"chartjs-adapter-dayjs-4": "1.0.4", "chartjs-adapter-dayjs-4": "1.0.4",
"chartjs-plugin-zoom": "2.0.1", "chartjs-plugin-zoom": "2.1.0",
"clippie": "4.1.1", "clippie": "4.1.1",
"css-loader": "7.0.0", "css-loader": "7.0.0",
"dayjs": "1.11.12", "dayjs": "1.11.12",
@ -42,7 +42,7 @@
"pretty-ms": "9.0.0", "pretty-ms": "9.0.0",
"sortablejs": "1.15.3", "sortablejs": "1.15.3",
"swagger-ui-dist": "5.17.14", "swagger-ui-dist": "5.17.14",
"tailwindcss": "3.4.13", "tailwindcss": "3.4.15",
"throttle-debounce": "5.0.0", "throttle-debounce": "5.0.0",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tippy.js": "6.3.7", "tippy.js": "6.3.7",
@ -50,7 +50,7 @@
"tributejs": "5.1.3", "tributejs": "5.1.3",
"uint8-to-base64": "0.2.0", "uint8-to-base64": "0.2.0",
"vanilla-colorful": "0.7.2", "vanilla-colorful": "0.7.2",
"vue": "3.5.12", "vue": "3.5.13",
"vue-chartjs": "5.3.1", "vue-chartjs": "5.3.1",
"vue-loader": "17.4.2", "vue-loader": "17.4.2",
"vue3-calendar-heatmap": "2.0.5", "vue3-calendar-heatmap": "2.0.5",

214
poetry.lock generated
View file

@ -126,15 +126,18 @@ six = ">=1.13.0"
[[package]] [[package]]
name = "json5" name = "json5"
version = "0.9.25" version = "0.9.28"
description = "A Python implementation of the JSON5 data format." description = "A Python implementation of the JSON5 data format."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8.0"
files = [ files = [
{file = "json5-0.9.25-py3-none-any.whl", hash = "sha256:34ed7d834b1341a86987ed52f3f76cd8ee184394906b6e22a1e0deb9ab294e8f"}, {file = "json5-0.9.28-py3-none-any.whl", hash = "sha256:29c56f1accdd8bc2e037321237662034a7e07921e2b7223281a5ce2c46f0c4df"},
{file = "json5-0.9.25.tar.gz", hash = "sha256:548e41b9be043f9426776f05df8635a00fe06104ea51ed24b67f908856e151ae"}, {file = "json5-0.9.28.tar.gz", hash = "sha256:1f82f36e615bc5b42f1bbd49dbc94b12563c56408c6ffa06414ea310890e9a6e"},
] ]
[package.extras]
dev = ["build (==1.2.2.post1)", "coverage (==7.5.3)", "mypy (==1.13.0)", "pip (==24.3.1)", "pylint (==3.2.3)", "ruff (==0.7.3)", "twine (==5.1.1)", "uv (==0.5.1)"]
[[package]] [[package]]
name = "pathspec" name = "pathspec"
version = "0.12.1" version = "0.12.1"
@ -210,105 +213,105 @@ files = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "2024.9.11" version = "2024.11.6"
description = "Alternative regular expression module, to replace re." description = "Alternative regular expression module, to replace re."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "regex-2024.9.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408"}, {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"},
{file = "regex-2024.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d"}, {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"},
{file = "regex-2024.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16e13a7929791ac1216afde26f712802e3df7bf0360b32e4914dca3ab8baeea5"}, {file = "regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e"},
{file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46989629904bad940bbec2106528140a218b4a36bb3042d8406980be1941429c"}, {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde"},
{file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a906ed5e47a0ce5f04b2c981af1c9acf9e8696066900bf03b9d7879a6f679fc8"}, {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e"},
{file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a091b0550b3b0207784a7d6d0f1a00d1d1c8a11699c1a4d93db3fbefc3ad35"}, {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2"},
{file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ddcd9a179c0a6fa8add279a4444015acddcd7f232a49071ae57fa6e278f1f71"}, {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf"},
{file = "regex-2024.9.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b41e1adc61fa347662b09398e31ad446afadff932a24807d3ceb955ed865cc8"}, {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c"},
{file = "regex-2024.9.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a"}, {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86"},
{file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:635a1d96665f84b292e401c3d62775851aedc31d4f8784117b3c68c4fcd4118d"}, {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67"},
{file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c0256beda696edcf7d97ef16b2a33a8e5a875affd6fa6567b54f7c577b30a137"}, {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d"},
{file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3ce4f1185db3fbde8ed8aa223fc9620f276c58de8b0d4f8cc86fd1360829edb6"}, {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2"},
{file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:09d77559e80dcc9d24570da3745ab859a9cf91953062e4ab126ba9d5993688ca"}, {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008"},
{file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a22ccefd4db3f12b526eccb129390942fe874a3a9fdbdd24cf55773a1faab1a"}, {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62"},
{file = "regex-2024.9.11-cp310-cp310-win32.whl", hash = "sha256:f745ec09bc1b0bd15cfc73df6fa4f726dcc26bb16c23a03f9e3367d357eeedd0"}, {file = "regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e"},
{file = "regex-2024.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:01c2acb51f8a7d6494c8c5eafe3d8e06d76563d8a8a4643b37e9b2dd8a2ff623"}, {file = "regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519"},
{file = "regex-2024.9.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2cce2449e5927a0bf084d346da6cd5eb016b2beca10d0013ab50e3c226ffc0df"}, {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638"},
{file = "regex-2024.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b37fa423beefa44919e009745ccbf353d8c981516e807995b2bd11c2c77d268"}, {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7"},
{file = "regex-2024.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64ce2799bd75039b480cc0360907c4fb2f50022f030bf9e7a8705b636e408fad"}, {file = "regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20"},
{file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4cc92bb6db56ab0c1cbd17294e14f5e9224f0cc6521167ef388332604e92679"}, {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114"},
{file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d05ac6fa06959c4172eccd99a222e1fbf17b5670c4d596cb1e5cde99600674c4"}, {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3"},
{file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040562757795eeea356394a7fb13076ad4f99d3c62ab0f8bdfb21f99a1f85664"}, {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f"},
{file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50"}, {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"},
{file = "regex-2024.9.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e5fb5f77c8745a60105403a774fe2c1759b71d3e7b4ca237a5e67ad066c7199"}, {file = "regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55"},
{file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:54d9ff35d4515debf14bc27f1e3b38bfc453eff3220f5bce159642fa762fe5d4"}, {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89"},
{file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df5cbb1fbc74a8305b6065d4ade43b993be03dbe0f8b30032cced0d7740994bd"}, {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d"},
{file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7fb89ee5d106e4a7a51bce305ac4efb981536301895f7bdcf93ec92ae0d91c7f"}, {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34"},
{file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a738b937d512b30bf75995c0159c0ddf9eec0775c9d72ac0202076c72f24aa96"}, {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d"},
{file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e28f9faeb14b6f23ac55bfbbfd3643f5c7c18ede093977f1df249f73fd22c7b1"}, {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45"},
{file = "regex-2024.9.11-cp311-cp311-win32.whl", hash = "sha256:18e707ce6c92d7282dfce370cd205098384b8ee21544e7cb29b8aab955b66fa9"}, {file = "regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9"},
{file = "regex-2024.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:313ea15e5ff2a8cbbad96ccef6be638393041b0a7863183c2d31e0c6116688cf"}, {file = "regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60"},
{file = "regex-2024.9.11-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b0d0a6c64fcc4ef9c69bd5b3b3626cc3776520a1637d8abaa62b9edc147a58f7"}, {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a"},
{file = "regex-2024.9.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:49b0e06786ea663f933f3710a51e9385ce0cba0ea56b67107fd841a55d56a231"}, {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9"},
{file = "regex-2024.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5b513b6997a0b2f10e4fd3a1313568e373926e8c252bd76c960f96fd039cd28d"}, {file = "regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2"},
{file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee439691d8c23e76f9802c42a95cfeebf9d47cf4ffd06f18489122dbb0a7ad64"}, {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4"},
{file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8f877c89719d759e52783f7fe6e1c67121076b87b40542966c02de5503ace42"}, {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577"},
{file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23b30c62d0f16827f2ae9f2bb87619bc4fba2044911e2e6c2eb1af0161cdb766"}, {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3"},
{file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85ab7824093d8f10d44330fe1e6493f756f252d145323dd17ab6b48733ff6c0a"}, {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e"},
{file = "regex-2024.9.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8dee5b4810a89447151999428fe096977346cf2f29f4d5e29609d2e19e0199c9"}, {file = "regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe"},
{file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98eeee2f2e63edae2181c886d7911ce502e1292794f4c5ee71e60e23e8d26b5d"}, {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e"},
{file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:57fdd2e0b2694ce6fc2e5ccf189789c3e2962916fb38779d3e3521ff8fe7a822"}, {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29"},
{file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d552c78411f60b1fdaafd117a1fca2f02e562e309223b9d44b7de8be451ec5e0"}, {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39"},
{file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a0b2b80321c2ed3fcf0385ec9e51a12253c50f146fddb2abbb10f033fe3d049a"}, {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51"},
{file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:18406efb2f5a0e57e3a5881cd9354c1512d3bb4f5c45d96d110a66114d84d23a"}, {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"},
{file = "regex-2024.9.11-cp312-cp312-win32.whl", hash = "sha256:e464b467f1588e2c42d26814231edecbcfe77f5ac414d92cbf4e7b55b2c2a776"}, {file = "regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"},
{file = "regex-2024.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:9e8719792ca63c6b8340380352c24dcb8cd7ec49dae36e963742a275dfae6009"}, {file = "regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"},
{file = "regex-2024.9.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c157bb447303070f256e084668b702073db99bbb61d44f85d811025fcf38f784"}, {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84"},
{file = "regex-2024.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4db21ece84dfeefc5d8a3863f101995de646c6cb0536952c321a2650aa202c36"}, {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4"},
{file = "regex-2024.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:220e92a30b426daf23bb67a7962900ed4613589bab80382be09b48896d211e92"}, {file = "regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0"},
{file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb1ae19e64c14c7ec1995f40bd932448713d3c73509e82d8cd7744dc00e29e86"}, {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0"},
{file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f47cd43a5bfa48f86925fe26fbdd0a488ff15b62468abb5d2a1e092a4fb10e85"}, {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7"},
{file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9d4a76b96f398697fe01117093613166e6aa8195d63f1b4ec3f21ab637632963"}, {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7"},
{file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ea51dcc0835eea2ea31d66456210a4e01a076d820e9039b04ae8d17ac11dee6"}, {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c"},
{file = "regex-2024.9.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7aaa315101c6567a9a45d2839322c51c8d6e81f67683d529512f5bcfb99c802"}, {file = "regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3"},
{file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c57d08ad67aba97af57a7263c2d9006d5c404d721c5f7542f077f109ec2a4a29"}, {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07"},
{file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f8404bf61298bb6f8224bb9176c1424548ee1181130818fcd2cbffddc768bed8"}, {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e"},
{file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dd4490a33eb909ef5078ab20f5f000087afa2a4daa27b4c072ccb3cb3050ad84"}, {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6"},
{file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:eee9130eaad130649fd73e5cd92f60e55708952260ede70da64de420cdcad554"}, {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4"},
{file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a2644a93da36c784e546de579ec1806bfd2763ef47babc1b03d765fe560c9f8"}, {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d"},
{file = "regex-2024.9.11-cp313-cp313-win32.whl", hash = "sha256:e997fd30430c57138adc06bba4c7c2968fb13d101e57dd5bb9355bf8ce3fa7e8"}, {file = "regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff"},
{file = "regex-2024.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:042c55879cfeb21a8adacc84ea347721d3d83a159da6acdf1116859e2427c43f"}, {file = "regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a"},
{file = "regex-2024.9.11-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:35f4a6f96aa6cb3f2f7247027b07b15a374f0d5b912c0001418d1d55024d5cb4"}, {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b"},
{file = "regex-2024.9.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:55b96e7ce3a69a8449a66984c268062fbaa0d8ae437b285428e12797baefce7e"}, {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3"},
{file = "regex-2024.9.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb130fccd1a37ed894824b8c046321540263013da72745d755f2d35114b81a60"}, {file = "regex-2024.11.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467"},
{file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:323c1f04be6b2968944d730e5c2091c8c89767903ecaa135203eec4565ed2b2b"}, {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd"},
{file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be1c8ed48c4c4065ecb19d882a0ce1afe0745dfad8ce48c49586b90a55f02366"}, {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf"},
{file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5b029322e6e7b94fff16cd120ab35a253236a5f99a79fb04fda7ae71ca20ae8"}, {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd"},
{file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6fff13ef6b5f29221d6904aa816c34701462956aa72a77f1f151a8ec4f56aeb"}, {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6"},
{file = "regex-2024.9.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:587d4af3979376652010e400accc30404e6c16b7df574048ab1f581af82065e4"}, {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f"},
{file = "regex-2024.9.11-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:079400a8269544b955ffa9e31f186f01d96829110a3bf79dc338e9910f794fca"}, {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5"},
{file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f9268774428ec173654985ce55fc6caf4c6d11ade0f6f914d48ef4719eb05ebb"}, {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df"},
{file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:23f9985c8784e544d53fc2930fc1ac1a7319f5d5332d228437acc9f418f2f168"}, {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773"},
{file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2941333154baff9838e88aa71c1d84f4438189ecc6021a12c7573728b5838e"}, {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c"},
{file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e93f1c331ca8e86fe877a48ad64e77882c0c4da0097f2212873a69bbfea95d0c"}, {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc"},
{file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:846bc79ee753acf93aef4184c040d709940c9d001029ceb7b7a52747b80ed2dd"}, {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f"},
{file = "regex-2024.9.11-cp38-cp38-win32.whl", hash = "sha256:c94bb0a9f1db10a1d16c00880bdebd5f9faf267273b8f5bd1878126e0fbde771"}, {file = "regex-2024.11.6-cp38-cp38-win32.whl", hash = "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4"},
{file = "regex-2024.9.11-cp38-cp38-win_amd64.whl", hash = "sha256:2b08fce89fbd45664d3df6ad93e554b6c16933ffa9d55cb7e01182baaf971508"}, {file = "regex-2024.11.6-cp38-cp38-win_amd64.whl", hash = "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001"},
{file = "regex-2024.9.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:07f45f287469039ffc2c53caf6803cd506eb5f5f637f1d4acb37a738f71dd066"}, {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839"},
{file = "regex-2024.9.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4838e24ee015101d9f901988001038f7f0d90dc0c3b115541a1365fb439add62"}, {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e"},
{file = "regex-2024.9.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6edd623bae6a737f10ce853ea076f56f507fd7726bee96a41ee3d68d347e4d16"}, {file = "regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf"},
{file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c69ada171c2d0e97a4b5aa78fbb835e0ffbb6b13fc5da968c09811346564f0d3"}, {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b"},
{file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02087ea0a03b4af1ed6ebab2c54d7118127fee8d71b26398e8e4b05b78963199"}, {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0"},
{file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69dee6a020693d12a3cf892aba4808fe168d2a4cef368eb9bf74f5398bfd4ee8"}, {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b"},
{file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297f54910247508e6e5cae669f2bc308985c60540a4edd1c77203ef19bfa63ca"}, {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef"},
{file = "regex-2024.9.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecea58b43a67b1b79805f1a0255730edaf5191ecef84dbc4cc85eb30bc8b63b9"}, {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48"},
{file = "regex-2024.9.11-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eab4bb380f15e189d1313195b062a6aa908f5bd687a0ceccd47c8211e9cf0d4a"}, {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13"},
{file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0cbff728659ce4bbf4c30b2a1be040faafaa9eca6ecde40aaff86f7889f4ab39"}, {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2"},
{file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:54c4a097b8bc5bb0dfc83ae498061d53ad7b5762e00f4adaa23bee22b012e6ba"}, {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95"},
{file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:73d6d2f64f4d894c96626a75578b0bf7d9e56dcda8c3d037a2118fdfe9b1c664"}, {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9"},
{file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:e53b5fbab5d675aec9f0c501274c467c0f9a5d23696cfc94247e1fb56501ed89"}, {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f"},
{file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ffbcf9221e04502fc35e54d1ce9567541979c3fdfb93d2c554f0ca583a19b35"}, {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b"},
{file = "regex-2024.9.11-cp39-cp39-win32.whl", hash = "sha256:e4c22e1ac1f1ec1e09f72e6c44d8f2244173db7eb9629cc3a346a8d7ccc31142"}, {file = "regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57"},
{file = "regex-2024.9.11-cp39-cp39-win_amd64.whl", hash = "sha256:faa3c142464efec496967359ca99696c896c591c56c53506bac1ad465f66e919"}, {file = "regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983"},
{file = "regex-2024.9.11.tar.gz", hash = "sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd"}, {file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"},
] ]
[[package]] [[package]]
@ -324,24 +327,24 @@ files = [
[[package]] [[package]]
name = "tomli" name = "tomli"
version = "2.0.2" version = "2.1.0"
description = "A lil' TOML parser" description = "A lil' TOML parser"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"},
{file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"},
] ]
[[package]] [[package]]
name = "tqdm" name = "tqdm"
version = "4.66.6" version = "4.67.0"
description = "Fast, Extensible Progress Meter" description = "Fast, Extensible Progress Meter"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "tqdm-4.66.6-py3-none-any.whl", hash = "sha256:223e8b5359c2efc4b30555531f09e9f2f3589bcd7fdd389271191031b49b7a63"}, {file = "tqdm-4.67.0-py3-none-any.whl", hash = "sha256:0cd8af9d56911acab92182e88d763100d4788bdf421d251616040cc4d44863be"},
{file = "tqdm-4.66.6.tar.gz", hash = "sha256:4bdd694238bef1485ce839d67967ab50af8f9272aab687c0d7702a01da0be090"}, {file = "tqdm-4.67.0.tar.gz", hash = "sha256:fe5a6f95e6fe0b9755e9469b77b9c3cf850048224ecaa8293d7d2d31f97d869a"},
] ]
[package.dependencies] [package.dependencies]
@ -349,6 +352,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
[package.extras] [package.extras]
dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"]
discord = ["requests"]
notebook = ["ipywidgets (>=6)"] notebook = ["ipywidgets (>=6)"]
slack = ["slack-sdk"] slack = ["slack-sdk"]
telegram = ["requests"] telegram = ["requests"]

View file

@ -7,6 +7,7 @@ label_bug=bug
label_feature=feature label_feature=feature
label_ui=forgejo/ui label_ui=forgejo/ui
label_breaking=breaking label_breaking=breaking
label_security=security
label_localization=forgejo/i18n label_localization=forgejo/i18n
payload=$(mktemp) payload=$(mktemp)
@ -17,50 +18,71 @@ function test_main() {
set -ex set -ex
PS4='${BASH_SOURCE[0]}:$LINENO: ${FUNCNAME[0]}: ' PS4='${BASH_SOURCE[0]}:$LINENO: ${FUNCNAME[0]}: '
test_payload_labels $label_worth $label_breaking $label_security $label_bug
test "$(categorize)" = 'AA Breaking security bug fixes'
test_payload_labels $label_worth $label_security $label_bug
test "$(categorize)" = 'AB Security bug fixes'
test_payload_labels $label_worth $label_breaking $label_security $label_feature
test "$(categorize)" = 'AC Breaking security features'
test_payload_labels $label_worth $label_security $label_feature
test "$(categorize)" = 'AD Security features'
test_payload_labels $label_worth $label_security
test "$(categorize)" = 'ZA Security changes without a feature or bug label'
test_payload_labels $label_worth $label_breaking $label_feature test_payload_labels $label_worth $label_breaking $label_feature
test "$(categorize)" = 'AA Breaking features' test "$(categorize)" = 'BA Breaking features'
test_payload_labels $label_worth $label_breaking $label_bug test_payload_labels $label_worth $label_breaking $label_bug
test "$(categorize)" = 'AB Breaking bug fixes' test "$(categorize)" = 'BB Breaking bug fixes'
test_payload_labels $label_worth $label_breaking test_payload_labels $label_worth $label_breaking
test "$(categorize)" = 'ZC Breaking changes without a feature or bug label' test "$(categorize)" = 'ZB Breaking changes without a feature or bug label'
test_payload_labels $label_worth $label_ui $label_feature test_payload_labels $label_worth $label_ui $label_feature
test "$(categorize)" = 'BA User Interface features' test "$(categorize)" = 'CA User Interface features'
test_payload_labels $label_worth $label_ui $label_bug test_payload_labels $label_worth $label_ui $label_bug
test "$(categorize)" = 'BB User Interface bug fixes' test "$(categorize)" = 'CB User Interface bug fixes'
test_payload_labels $label_worth $label_ui test_payload_labels $label_worth $label_ui
test "$(categorize)" = 'ZD User Interface changes without a feature or bug label' test "$(categorize)" = 'ZC User Interface changes without a feature or bug label'
test_payload_labels $label_worth $label_feature
test "$(categorize)" = 'CA Features'
test_payload_labels $label_worth $label_bug
test "$(categorize)" = 'CB Bug fixes'
test_payload_labels $label_worth $label_localization test_payload_labels $label_worth $label_localization
test "$(categorize)" = 'DA Localization' test "$(categorize)" = 'DA Localization'
test_payload_labels $label_worth $label_feature
test "$(categorize)" = 'EA Features'
test_payload_labels $label_worth $label_bug
test "$(categorize)" = 'EB Bug fixes'
test_payload_labels $label_worth test_payload_labels $label_worth
test "$(categorize)" = 'ZE Other changes without a feature or bug label' test "$(categorize)" = 'ZE Other changes without a feature or bug label'
test_payload_labels test_payload_labels
test "$(categorize)" = 'ZF Included for completeness but not worth a release note' test "$(categorize)" = 'ZF Included for completeness but not worth a release note'
test_payload_draft "fix(security)!: breaking security bug fix"
test "$(categorize)" = 'AA Breaking security bug fixes'
test_payload_draft "fix(security): security bug fix"
test "$(categorize)" = 'AB Security bug fixes'
test_payload_draft "feat!: breaking feature" test_payload_draft "feat!: breaking feature"
test "$(categorize)" = 'AA Breaking features' test "$(categorize)" = 'BA Breaking features'
test_payload_draft "fix!: breaking bug fix" test_payload_draft "fix!: breaking bug fix"
test "$(categorize)" = 'AB Breaking bug fixes' test "$(categorize)" = 'BB Breaking bug fixes'
test_payload_draft "feat: feature" test_payload_draft "feat: feature"
test "$(categorize)" = 'CA Features' test "$(categorize)" = 'EA Features'
test_payload_draft "fix: bug fix" test_payload_draft "fix: bug fix"
test "$(categorize)" = 'CB Bug fixes' test "$(categorize)" = 'EB Bug fixes'
test_payload_draft "something with no prefix" test_payload_draft "something with no prefix"
test "$(categorize)" = 'ZE Other changes without a feature or bug label' test "$(categorize)" = 'ZE Other changes without a feature or bug label'
@ -109,6 +131,7 @@ function categorize() {
is_feature=false is_feature=false
is_localization=false is_localization=false
is_breaking=false is_breaking=false
is_security=false
# #
# first try to figure out the category from the labels # first try to figure out the category from the labels
@ -125,6 +148,12 @@ function categorize() {
;; ;;
esac esac
case "$labels" in
*$label_security*)
is_security=true
;;
esac
case "$labels" in case "$labels" in
*$label_breaking*) *$label_breaking*)
is_breaking=true is_breaking=true
@ -143,6 +172,15 @@ function categorize() {
if ! $is_bug && ! $is_feature; then if ! $is_bug && ! $is_feature; then
draft="$(jq --raw-output .Draft <$payload)" draft="$(jq --raw-output .Draft <$payload)"
case "$draft" in case "$draft" in
fix\(security\)!:*)
is_bug=true
is_breaking=true
is_security=true
;;
fix\(security\):*)
is_bug=true
is_security=true
;;
fix!:*) fix!:*)
is_bug=true is_bug=true
is_breaking=true is_breaking=true
@ -171,29 +209,45 @@ function categorize() {
fi fi
fi fi
if $is_breaking; then if $is_security; then
if $is_feature; then if $is_bug; then
echo -n AA Breaking features if $is_breaking; then
elif $is_bug; then echo -n AA Breaking security bug fixes
echo -n AB Breaking bug fixes else
echo -n AB Security bug fixes
fi
elif $is_feature; then
if $is_breaking; then
echo -n AC Breaking security features
else
echo -n AD Security features
fi
else else
echo -n ZC Breaking changes without a feature or bug label echo -n ZA Security changes without a feature or bug label
fi
elif $is_breaking; then
if $is_feature; then
echo -n BA Breaking features
elif $is_bug; then
echo -n BB Breaking bug fixes
else
echo -n ZB Breaking changes without a feature or bug label
fi fi
elif $is_ui; then elif $is_ui; then
if $is_feature; then if $is_feature; then
echo -n BA User Interface features echo -n CA User Interface features
elif $is_bug; then elif $is_bug; then
echo -n BB User Interface bug fixes echo -n CB User Interface bug fixes
else else
echo -n ZD User Interface changes without a feature or bug label echo -n ZC User Interface changes without a feature or bug label
fi fi
elif $is_localization; then elif $is_localization; then
echo -n DA Localization echo -n DA Localization
else else
if $is_feature; then if $is_feature; then
echo -n CA Features echo -n EA Features
elif $is_bug; then elif $is_bug; then
echo -n CB Bug fixes echo -n EB Bug fixes
else else
echo -n ZE Other changes without a feature or bug label echo -n ZE Other changes without a feature or bug label
fi fi

8
release-notes/5974.md Normal file
View file

@ -0,0 +1,8 @@
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/1ce33aa38d1d258d14523ff2c7c2dbf339f22b74) it was possible to use a token sent via email for secondary email validation to reset the password instead. In other words, a token sent for a given action (registration, password reset or secondary email validation) could be used to perform a different action. It is no longer possible to use a token for an action that is different from its original purpose.
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/061abe60045212acf8c3f5c49b5cc758b4cbcde9) a fork of a public repository would show in the list of forks, even if its owner was not a public user or organization. Such a fork is now hidden from the list of forks of the public repository.
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/3e3ef76808100cb1c853378733d0f6a910324ac6) the members of an organization team with read access to a repository (e.g. to read issues) but no read access to the code could read the RSS or atom feeds which include the commit activity. Reading the RSS or atom feeds is now denied unless the team has read permissions on the code.
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/9508aa7713632ed40124a933d91d5766cf2369c2) the tokens used when [replying by email to issues or pull requests](https://forgejo.org/docs/v9.0/user/incoming/) were weaker than the [rfc2104 recommendations](https://datatracker.ietf.org/doc/html/rfc2104#section-5). The tokens are now truncated to 128 bits instead of 80 bits. It is no longer possible to reply to emails sent before the upgrade because the weaker tokens are invalid.
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/786dfc7fb81ee76d4292ca5fcb33e6ea7bdccc29) a registered user could modify the update frequency of any push mirror (e.g. every 4h instead of every 8h). They are now only able to do that if they have administrative permissions on the repository.
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/e6bbecb02d47730d3cc630d419fe27ef2fb5cb39) it was possible to use basic authorization (i.e. user:password) for requests to the API even when security keys were enrolled for a user. It is no longer possible, an application token must be used instead.
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/7067cc7da4f144cc8a2fd2ae6e5307e0465ace7f) some markup sanitation rules were not as strong as they could be (e.g. allowing `emoji somethingelse` as well as `emoji`). The rules are now stricter and do not allow for such cases.
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/b70196653f9d7d3b9d4e72d114e5cc6f472988c4) when Forgejo is configured to enable instance wide search (e.g. with [bleve](https://blevesearch.com/)), results found in the repositories of private or limited users were displayed to anonymous visitors. The results found in private or limited organizations were not displayed. The search results found in the repositories of private or limited user are no longer displayed to anonymous visitors.

1
release-notes/5988.md Normal file
View file

@ -0,0 +1 @@
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/fc26becba4b08877a726f2e7e453992310245fe5) when a tag was removed and a release existed for that tag, it would be broken. The release is no longer broken the tag can be added again.

View file

@ -1316,7 +1316,11 @@ func Routes() *web.Route {
m.Get("/trees/{sha}", repo.GetTree) m.Get("/trees/{sha}", repo.GetTree)
m.Get("/blobs/{sha}", repo.GetBlob) m.Get("/blobs/{sha}", repo.GetBlob)
m.Get("/tags/{sha}", repo.GetAnnotatedTag) m.Get("/tags/{sha}", repo.GetAnnotatedTag)
m.Get("/notes/{sha}", repo.GetNote) m.Group("/notes/{sha}", func() {
m.Get("", repo.GetNote)
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), bind(api.NoteOptions{}), repo.SetNote)
m.Delete("", reqToken(), reqRepoWriter(unit.TypeCode), repo.RemoveNote)
})
}, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode)) }, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode))
m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.ApplyDiffPatch) m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.ApplyDiffPatch)
m.Group("/contents", func() { m.Group("/contents", func() {

View file

@ -15,6 +15,7 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user" "code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
@ -340,13 +341,28 @@ func Edit(ctx *context.APIContext) {
// "$ref": "#/responses/Organization" // "$ref": "#/responses/Organization"
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/error"
form := web.GetForm(ctx).(*api.EditOrgOption) form := web.GetForm(ctx).(*api.EditOrgOption)
if form.Email != "" { if form.Email != nil {
if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.Org.Organization.AsUser(), form.Email); err != nil { if *form.Email == "" {
ctx.Error(http.StatusInternalServerError, "ReplacePrimaryEmailAddress", err) err := user_model.DeletePrimaryEmailAddressOfUser(ctx, ctx.Org.Organization.ID)
return if err != nil {
ctx.Error(http.StatusInternalServerError, "DeletePrimaryEmailAddressOfUser", err)
return
}
ctx.Org.Organization.Email = ""
} else {
if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.Org.Organization.AsUser(), *form.Email); err != nil {
if validation.IsErrEmailInvalid(err) || validation.IsErrEmailCharIsNotSupported(err) {
ctx.Error(http.StatusUnprocessableEntity, "ReplacePrimaryEmailAddress", err)
} else {
ctx.Error(http.StatusInternalServerError, "ReplacePrimaryEmailAddress", err)
}
return
}
} }
} }

View file

@ -56,7 +56,7 @@ func ListForks(ctx *context.APIContext) {
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
forks, err := repo_model.GetForks(ctx, ctx.Repo.Repository, utils.GetListOptions(ctx)) forks, total, err := repo_model.GetForks(ctx, ctx.Repo.Repository, ctx.Doer, utils.GetListOptions(ctx))
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "GetForks", err) ctx.Error(http.StatusInternalServerError, "GetForks", err)
return return
@ -71,7 +71,7 @@ func ListForks(ctx *context.APIContext) {
apiForks[i] = convert.ToRepo(ctx, fork, permission) apiForks[i] = convert.ToRepo(ctx, fork, permission)
} }
ctx.SetTotalCountHeader(int64(ctx.Repo.Repository.NumForks)) ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, apiForks) ctx.JSON(http.StatusOK, apiForks)
} }

View file

@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/convert"
) )
@ -102,3 +103,107 @@ func getNote(ctx *context.APIContext, identifier string) {
apiNote := api.Note{Message: string(note.Message), Commit: cmt} apiNote := api.Note{Message: string(note.Message), Commit: cmt}
ctx.JSON(http.StatusOK, apiNote) ctx.JSON(http.StatusOK, apiNote)
} }
// SetNote Sets a note corresponding to a single commit from a repository
func SetNote(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/git/notes/{sha} repository repoSetNote
// ---
// summary: Set a note corresponding to a single commit from a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: sha
// in: path
// description: a git ref or commit sha
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/NoteOptions"
// responses:
// "200":
// "$ref": "#/responses/Note"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
sha := ctx.Params(":sha")
if !git.IsValidRefPattern(sha) {
ctx.Error(http.StatusUnprocessableEntity, "no valid ref or sha", fmt.Sprintf("no valid ref or sha: %s", sha))
return
}
form := web.GetForm(ctx).(*api.NoteOptions)
err := git.SetNote(ctx, ctx.Repo.GitRepo, sha, form.Message, ctx.Doer.Name, ctx.Doer.GetEmail())
if err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound(sha)
} else {
ctx.Error(http.StatusInternalServerError, "SetNote", err)
}
return
}
getNote(ctx, sha)
}
// RemoveNote Removes a note corresponding to a single commit from a repository
func RemoveNote(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/git/notes/{sha} repository repoRemoveNote
// ---
// summary: Removes a note corresponding to a single commit from a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: sha
// in: path
// description: a git ref or commit sha
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
sha := ctx.Params(":sha")
if !git.IsValidRefPattern(sha) {
ctx.Error(http.StatusUnprocessableEntity, "no valid ref or sha", fmt.Sprintf("no valid ref or sha: %s", sha))
return
}
err := git.RemoveNote(ctx, ctx.Repo.GitRepo, sha)
if err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound(sha)
} else {
ctx.Error(http.StatusInternalServerError, "RemoveNote", err)
}
return
}
ctx.Status(http.StatusNoContent)
}

View file

@ -1110,10 +1110,19 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
ctx.Repo.PullRequest.SameRepo = isSameRepo ctx.Repo.PullRequest.SameRepo = isSameRepo
log.Trace("Repo path: %q, base branch: %q, head branch: %q", ctx.Repo.GitRepo.Path, baseBranch, headBranch) log.Trace("Repo path: %q, base branch: %q, head branch: %q", ctx.Repo.GitRepo.Path, baseBranch, headBranch)
// Check if base branch is valid. // Check if base branch is valid.
if !ctx.Repo.GitRepo.IsBranchExist(baseBranch) && !ctx.Repo.GitRepo.IsTagExist(baseBranch) { baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(baseBranch)
ctx.NotFound("BaseNotExist") baseIsBranch := ctx.Repo.GitRepo.IsBranchExist(baseBranch)
return nil, nil, nil, "", "" baseIsTag := ctx.Repo.GitRepo.IsTagExist(baseBranch)
if !baseIsCommit && !baseIsBranch && !baseIsTag {
// Check for short SHA usage
if baseCommit, _ := ctx.Repo.GitRepo.GetCommit(baseBranch); baseCommit != nil {
baseBranch = baseCommit.ID.String()
} else {
ctx.NotFound("BaseNotExist")
return nil, nil, nil, "", ""
}
} }
// Check if current user has fork of repository or in the same repository. // Check if current user has fork of repository or in the same repository.
@ -1186,13 +1195,34 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
} }
// Check if head branch is valid. // Check if head branch is valid.
if !headGitRepo.IsBranchExist(headBranch) && !headGitRepo.IsTagExist(headBranch) { headIsCommit := headGitRepo.IsBranchExist(headBranch)
headGitRepo.Close() headIsBranch := headGitRepo.IsTagExist(headBranch)
ctx.NotFound() headIsTag := headGitRepo.IsCommitExist(baseBranch)
return nil, nil, nil, "", "" if !headIsCommit && !headIsBranch && !headIsTag {
// Check if headBranch is short sha commit hash
if headCommit, _ := headGitRepo.GetCommit(headBranch); headCommit != nil {
headBranch = headCommit.ID.String()
} else {
headGitRepo.Close()
ctx.NotFound("IsRefExist", nil)
return nil, nil, nil, "", ""
}
} }
compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseBranch, headBranch, false, false) baseBranchRef := baseBranch
if baseIsBranch {
baseBranchRef = git.BranchPrefix + baseBranch
} else if baseIsTag {
baseBranchRef = git.TagPrefix + baseBranch
}
headBranchRef := headBranch
if headIsBranch {
headBranchRef = headBranch
} else if headIsTag {
headBranchRef = headBranch
}
compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseBranchRef, headBranchRef, false, false)
if err != nil { if err != nil {
headGitRepo.Close() headGitRepo.Close()
ctx.Error(http.StatusInternalServerError, "GetCompareInfo", err) ctx.Error(http.StatusInternalServerError, "GetCompareInfo", err)

View file

@ -231,4 +231,7 @@ type swaggerParameterBodies struct {
// in:body // in:body
SetUserQuotaGroupsOptions api.SetUserQuotaGroupsOptions SetUserQuotaGroupsOptions api.SetUserQuotaGroupsOptions
// in:body
NoteOptions api.NoteOptions
} }

View file

@ -5,8 +5,6 @@
package auth package auth
import ( import (
"crypto/subtle"
"encoding/hex"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@ -63,38 +61,11 @@ func autoSignIn(ctx *context.Context) (bool, error) {
return false, nil return false, nil
} }
lookupKey, validator, found := strings.Cut(authCookie, ":") u, err := user_model.VerifyUserAuthorizationToken(ctx, authCookie, auth.LongTermAuthorization, false)
if !found {
return false, nil
}
authToken, err := auth.FindAuthToken(ctx, lookupKey)
if err != nil { if err != nil {
if errors.Is(err, util.ErrNotExist) { return false, fmt.Errorf("VerifyUserAuthorizationToken: %w", err)
return false, nil
}
return false, err
} }
if u == nil {
if authToken.IsExpired() {
err = auth.DeleteAuthToken(ctx, authToken)
return false, err
}
rawValidator, err := hex.DecodeString(validator)
if err != nil {
return false, err
}
if subtle.ConstantTimeCompare([]byte(authToken.HashedValidator), []byte(auth.HashValidator(rawValidator))) == 0 {
return false, nil
}
u, err := user_model.GetUserByID(ctx, authToken.UID)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
return false, fmt.Errorf("GetUserByID: %w", err)
}
return false, nil return false, nil
} }
@ -633,7 +604,10 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
return false return false
} }
mailer.SendActivateAccountMail(ctx.Locale, u) if err := mailer.SendActivateAccountMail(ctx, u); err != nil {
ctx.ServerError("SendActivateAccountMail", err)
return false
}
ctx.Data["IsSendRegisterMail"] = true ctx.Data["IsSendRegisterMail"] = true
ctx.Data["Email"] = u.Email ctx.Data["Email"] = u.Email
@ -674,7 +648,10 @@ func Activate(ctx *context.Context) {
ctx.Data["ResendLimited"] = true ctx.Data["ResendLimited"] = true
} else { } else {
ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale) ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer) if err := mailer.SendActivateAccountMail(ctx, ctx.Doer); err != nil {
ctx.ServerError("SendActivateAccountMail", err)
return
}
if err := ctx.Cache.Put(cacheKey+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil { if err := ctx.Cache.Put(cacheKey+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err) log.Error("Set cache(MailResendLimit) fail: %v", err)
@ -687,7 +664,12 @@ func Activate(ctx *context.Context) {
return return
} }
user := user_model.VerifyUserActiveCode(ctx, code) user, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.UserActivation, false)
if err != nil {
ctx.ServerError("VerifyUserAuthorizationToken", err)
return
}
// if code is wrong // if code is wrong
if user == nil { if user == nil {
ctx.Data["IsCodeInvalid"] = true ctx.Data["IsCodeInvalid"] = true
@ -751,7 +733,12 @@ func ActivatePost(ctx *context.Context) {
return return
} }
user := user_model.VerifyUserActiveCode(ctx, code) user, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.UserActivation, true)
if err != nil {
ctx.ServerError("VerifyUserAuthorizationToken", err)
return
}
// if code is wrong // if code is wrong
if user == nil { if user == nil {
ctx.Data["IsCodeInvalid"] = true ctx.Data["IsCodeInvalid"] = true
@ -835,23 +822,32 @@ func ActivateEmail(ctx *context.Context) {
code := ctx.FormString("code") code := ctx.FormString("code")
emailStr := ctx.FormString("email") emailStr := ctx.FormString("email")
// Verify code. u, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.EmailActivation(emailStr), true)
if email := user_model.VerifyActiveEmailCode(ctx, code, emailStr); email != nil { if err != nil {
if err := user_model.ActivateEmail(ctx, email); err != nil { ctx.ServerError("VerifyUserAuthorizationToken", err)
ctx.ServerError("ActivateEmail", err) return
return
}
log.Trace("Email activated: %s", email.Email)
ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
if u, err := user_model.GetUserByID(ctx, email.UID); err != nil {
log.Warn("GetUserByID: %d", email.UID)
} else {
// Allow user to validate more emails
_ = ctx.Cache.Delete("MailResendLimit_" + u.LowerName)
}
} }
if u == nil {
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
email, err := user_model.GetEmailAddressOfUser(ctx, emailStr, u.ID)
if err != nil {
ctx.ServerError("GetEmailAddressOfUser", err)
return
}
if err := user_model.ActivateEmail(ctx, email); err != nil {
ctx.ServerError("ActivateEmail", err)
return
}
log.Trace("Email activated: %s", email.Email)
ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
// Allow user to validate more emails
_ = ctx.Cache.Delete("MailResendLimit_" + u.LowerName)
// FIXME: e-mail verification does not require the user to be logged in, // FIXME: e-mail verification does not require the user to be logged in,
// so this could be redirecting to the login page. // so this could be redirecting to the login page.

View file

@ -86,7 +86,10 @@ func ForgotPasswdPost(ctx *context.Context) {
return return
} }
mailer.SendResetPasswordMail(u) if err := mailer.SendResetPasswordMail(ctx, u); err != nil {
ctx.ServerError("SendResetPasswordMail", err)
return
}
if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil { if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err) log.Error("Set cache(MailResendLimit) fail: %v", err)
@ -97,7 +100,7 @@ func ForgotPasswdPost(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplForgotPassword) ctx.HTML(http.StatusOK, tplForgotPassword)
} }
func commonResetPassword(ctx *context.Context) (*user_model.User, *auth.TwoFactor) { func commonResetPassword(ctx *context.Context, shouldDeleteToken bool) (*user_model.User, *auth.TwoFactor) {
code := ctx.FormString("code") code := ctx.FormString("code")
ctx.Data["Title"] = ctx.Tr("auth.reset_password") ctx.Data["Title"] = ctx.Tr("auth.reset_password")
@ -113,7 +116,12 @@ func commonResetPassword(ctx *context.Context) (*user_model.User, *auth.TwoFacto
} }
// Fail early, don't frustrate the user // Fail early, don't frustrate the user
u := user_model.VerifyUserActiveCode(ctx, code) u, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.PasswordReset, shouldDeleteToken)
if err != nil {
ctx.ServerError("VerifyUserAuthorizationToken", err)
return nil, nil
}
if u == nil { if u == nil {
ctx.Flash.Error(ctx.Tr("auth.invalid_code_forgot_password", fmt.Sprintf("%s/user/forgot_password", setting.AppSubURL)), true) ctx.Flash.Error(ctx.Tr("auth.invalid_code_forgot_password", fmt.Sprintf("%s/user/forgot_password", setting.AppSubURL)), true)
return nil, nil return nil, nil
@ -145,7 +153,7 @@ func commonResetPassword(ctx *context.Context) (*user_model.User, *auth.TwoFacto
func ResetPasswd(ctx *context.Context) { func ResetPasswd(ctx *context.Context) {
ctx.Data["IsResetForm"] = true ctx.Data["IsResetForm"] = true
commonResetPassword(ctx) commonResetPassword(ctx, false)
if ctx.Written() { if ctx.Written() {
return return
} }
@ -155,7 +163,7 @@ func ResetPasswd(ctx *context.Context) {
// ResetPasswdPost response from account recovery request // ResetPasswdPost response from account recovery request
func ResetPasswdPost(ctx *context.Context) { func ResetPasswdPost(ctx *context.Context) {
u, twofa := commonResetPassword(ctx) u, twofa := commonResetPassword(ctx, true)
if ctx.Written() { if ctx.Written() {
return return
} }

View file

@ -93,7 +93,13 @@ func SettingsPost(ctx *context.Context) {
ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(org.Name) ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(org.Name)
} }
if form.Email != "" { if form.Email == "" {
err := user_model.DeletePrimaryEmailAddressOfUser(ctx, org.ID)
if err != nil {
ctx.ServerError("DeletePrimaryEmailAddressOfUser", err)
return
}
} else {
if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil { if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil {
ctx.Data["Err_Email"] = true ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsOptions, &form) ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsOptions, &form)

View file

@ -27,7 +27,9 @@ import (
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/gitdiff" "code.gitea.io/gitea/services/gitdiff"
git_service "code.gitea.io/gitea/services/repository" git_service "code.gitea.io/gitea/services/repository"
) )
@ -467,3 +469,29 @@ func processGitCommits(ctx *context.Context, gitCommits []*git.Commit) []*git_mo
} }
return commits return commits
} }
func SetCommitNotes(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CommitNotesForm)
commitID := ctx.Params(":sha")
err := git.SetNote(ctx, ctx.Repo.GitRepo, commitID, form.Notes, ctx.Doer.Name, ctx.Doer.GetEmail())
if err != nil {
ctx.ServerError("SetNote", err)
return
}
ctx.Redirect(fmt.Sprintf("%s/commit/%s", ctx.Repo.Repository.HTMLURL(), commitID))
}
func RemoveCommitNotes(ctx *context.Context) {
commitID := ctx.Params(":sha")
err := git.RemoveNote(ctx, ctx.Repo.GitRepo, commitID)
if err != nil {
ctx.ServerError("RemoveNotes", err)
return
}
ctx.Redirect(fmt.Sprintf("%s/commit/%s", ctx.Repo.Repository.HTMLURL(), commitID))
}

View file

@ -457,16 +457,16 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
ctx.Data["OpenCount"] = issueStats.OpenCount ctx.Data["OpenCount"] = issueStats.OpenCount
ctx.Data["ClosedCount"] = issueStats.ClosedCount ctx.Data["ClosedCount"] = issueStats.ClosedCount
ctx.Data["AllCount"] = issueStats.AllCount ctx.Data["AllCount"] = issueStats.AllCount
linkStr := "%s?q=%s&type=%s&sort=%s&state=%s&labels=%s&milestone=%d&project=%d&assignee=%d&poster=%d&archived=%t" linkStr := "?q=%s&type=%s&sort=%s&state=%s&labels=%s&milestone=%d&project=%d&assignee=%d&poster=%d&fuzzy=%t&archived=%t"
ctx.Data["AllStatesLink"] = fmt.Sprintf(linkStr, ctx.Link, ctx.Data["AllStatesLink"] = fmt.Sprintf(linkStr,
url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "all", url.QueryEscape(selectLabels), url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "all", url.QueryEscape(selectLabels),
milestoneID, projectID, assigneeID, posterID, archived) milestoneID, projectID, assigneeID, posterID, isFuzzy, archived)
ctx.Data["OpenLink"] = fmt.Sprintf(linkStr, ctx.Link, ctx.Data["OpenLink"] = fmt.Sprintf(linkStr,
url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "open", url.QueryEscape(selectLabels), url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "open", url.QueryEscape(selectLabels),
milestoneID, projectID, assigneeID, posterID, archived) milestoneID, projectID, assigneeID, posterID, isFuzzy, archived)
ctx.Data["ClosedLink"] = fmt.Sprintf(linkStr, ctx.Link, ctx.Data["ClosedLink"] = fmt.Sprintf(linkStr,
url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "closed", url.QueryEscape(selectLabels), url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "closed", url.QueryEscape(selectLabels),
milestoneID, projectID, assigneeID, posterID, archived) milestoneID, projectID, assigneeID, posterID, isFuzzy, archived)
ctx.Data["SelLabelIDs"] = labelIDs ctx.Data["SelLabelIDs"] = labelIDs
ctx.Data["SelectLabels"] = selectLabels ctx.Data["SelectLabels"] = selectLabels
ctx.Data["ViewType"] = viewType ctx.Data["ViewType"] = viewType

View file

@ -566,21 +566,19 @@ func SettingsPost(ctx *context.Context) {
// as an error on the UI for this action // as an error on the UI for this action
ctx.Data["Err_RepoName"] = nil ctx.Data["Err_RepoName"] = nil
m, err := selectPushMirrorByForm(ctx, form, repo)
if err != nil {
ctx.NotFound("", nil)
return
}
interval, err := time.ParseDuration(form.PushMirrorInterval) interval, err := time.ParseDuration(form.PushMirrorInterval)
if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) { if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &forms.RepoSettingForm{}) ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &forms.RepoSettingForm{})
return return
} }
id, err := strconv.ParseInt(form.PushMirrorID, 10, 64) m.Interval = interval
if err != nil {
ctx.ServerError("UpdatePushMirrorIntervalPushMirrorID", err)
return
}
m := &repo_model.PushMirror{
ID: id,
Interval: interval,
}
if err := repo_model.UpdatePushMirrorInterval(ctx, m); err != nil { if err := repo_model.UpdatePushMirrorInterval(ctx, m); err != nil {
ctx.ServerError("UpdatePushMirrorInterval", err) ctx.ServerError("UpdatePushMirrorInterval", err)
return return

View file

@ -1232,11 +1232,8 @@ func Forks(ctx *context.Context) {
page = 1 page = 1
} }
pager := context.NewPagination(ctx.Repo.Repository.NumForks, setting.MaxForksPerPage, page, 5) forks, total, err := repo_model.GetForks(ctx, ctx.Repo.Repository, ctx.Doer, db.ListOptions{
ctx.Data["Page"] = pager Page: page,
forks, err := repo_model.GetForks(ctx, ctx.Repo.Repository, db.ListOptions{
Page: pager.Paginater.Current(),
PageSize: setting.MaxForksPerPage, PageSize: setting.MaxForksPerPage,
}) })
if err != nil { if err != nil {
@ -1244,6 +1241,9 @@ func Forks(ctx *context.Context) {
return return
} }
pager := context.NewPagination(int(total), setting.MaxForksPerPage, page, 5)
ctx.Data["Page"] = pager
for _, fork := range forks { for _, fork := range forks {
if err = fork.LoadOwner(ctx); err != nil { if err = fork.LoadOwner(ctx); err != nil {
ctx.ServerError("LoadOwner", err) ctx.ServerError("LoadOwner", err)

View file

@ -155,9 +155,15 @@ func EmailPost(ctx *context.Context) {
return return
} }
// Only fired when the primary email is inactive (Wrong state) // Only fired when the primary email is inactive (Wrong state)
mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer) if err := mailer.SendActivateAccountMail(ctx, ctx.Doer); err != nil {
ctx.ServerError("SendActivateAccountMail", err)
return
}
} else { } else {
mailer.SendActivateEmailMail(ctx.Doer, email.Email) if err := mailer.SendActivateEmailMail(ctx, ctx.Doer, email.Email); err != nil {
ctx.ServerError("SendActivateEmailMail", err)
return
}
} }
address = email.Email address = email.Email
@ -218,7 +224,10 @@ func EmailPost(ctx *context.Context) {
// Send confirmation email // Send confirmation email
if setting.Service.RegisterEmailConfirm { if setting.Service.RegisterEmailConfirm {
mailer.SendActivateEmailMail(ctx.Doer, form.Email) if err := mailer.SendActivateEmailMail(ctx, ctx.Doer, form.Email); err != nil {
ctx.ServerError("SendActivateEmailMail", err)
return
}
if err := ctx.Cache.Put("MailResendLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil { if err := ctx.Cache.Put("MailResendLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err) log.Error("Set cache(MailResendLimit) fail: %v", err)
} }

View file

@ -1559,11 +1559,17 @@ func registerRoutes(m *web.Route) {
m.Get("/graph", repo.Graph) m.Get("/graph", repo.Graph)
m.Get("/commit/{sha:([a-f0-9]{4,64})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff) m.Get("/commit/{sha:([a-f0-9]{4,64})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
m.Get("/commit/{sha:([a-f0-9]{4,64})$}/load-branches-and-tags", repo.LoadBranchesAndTags) m.Get("/commit/{sha:([a-f0-9]{4,64})$}/load-branches-and-tags", repo.LoadBranchesAndTags)
m.Group("/commit/{sha:([a-f0-9]{4,64})$}/notes", func() {
m.Post("", web.Bind(forms.CommitNotesForm{}), repo.SetCommitNotes)
m.Post("/remove", repo.RemoveCommitNotes)
}, reqSignIn, reqRepoCodeWriter)
m.Get("/cherry-pick/{sha:([a-f0-9]{4,64})$}", repo.SetEditorconfigIfExists, repo.CherryPick) m.Get("/cherry-pick/{sha:([a-f0-9]{4,64})$}", repo.SetEditorconfigIfExists, repo.CherryPick)
}, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader) }, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)
m.Get("/rss/branch/*", repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefBranch), feedEnabled, feed.RenderBranchFeed("rss")) m.Group("", func() {
m.Get("/atom/branch/*", repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefBranch), feedEnabled, feed.RenderBranchFeed("atom")) m.Get("/rss/branch/*", feed.RenderBranchFeed("rss"))
m.Get("/atom/branch/*", feed.RenderBranchFeed("atom"))
}, repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefBranch), reqRepoCodeReader, feedEnabled)
m.Group("/src", func() { m.Group("/src", func() {
m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.Home) m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.Home)

View file

@ -5,6 +5,7 @@
package auth package auth
import ( import (
"errors"
"net/http" "net/http"
"strings" "strings"
@ -132,6 +133,16 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
return nil, err return nil, err
} }
hashWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(req.Context(), u.ID)
if err != nil {
log.Error("HasWebAuthnRegistrationsByUID: %v", err)
return nil, err
}
if hashWebAuthn {
return nil, errors.New("Basic authorization is not allowed while having security keys enrolled")
}
if skipper, ok := source.Cfg.(LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() { if skipper, ok := source.Cfg.(LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() {
if err := validateTOTP(req, u); err != nil { if err := validateTOTP(req, u); err != nil {
return nil, err return nil, err

View file

@ -347,12 +347,30 @@ func loadOrCreateAsymmetricKey() (any, error) {
key, err := func() (any, error) { key, err := func() (any, error) {
switch { switch {
case strings.HasPrefix(setting.OAuth2.JWTSigningAlgorithm, "RS"): case strings.HasPrefix(setting.OAuth2.JWTSigningAlgorithm, "RS"):
return rsa.GenerateKey(rand.Reader, 4096) var bits int
switch setting.OAuth2.JWTSigningAlgorithm {
case "RS256":
bits = 2048
case "RS384":
bits = 3072
case "RS512":
bits = 4096
}
return rsa.GenerateKey(rand.Reader, bits)
case setting.OAuth2.JWTSigningAlgorithm == "EdDSA": case setting.OAuth2.JWTSigningAlgorithm == "EdDSA":
_, pk, err := ed25519.GenerateKey(rand.Reader) _, pk, err := ed25519.GenerateKey(rand.Reader)
return pk, err return pk, err
default: default:
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) var curve elliptic.Curve
switch setting.OAuth2.JWTSigningAlgorithm {
case "ES256":
curve = elliptic.P256()
case "ES384":
curve = elliptic.P384()
case "ES512":
curve = elliptic.P521()
}
return ecdsa.GenerateKey(curve, rand.Reader)
} }
}() }()
if err != nil { if err != nil {

View file

@ -0,0 +1,116 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
package oauth2
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"os"
"path/filepath"
"testing"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLoadOrCreateAsymmetricKey(t *testing.T) {
loadKey := func(t *testing.T) any {
t.Helper()
loadOrCreateAsymmetricKey()
fileContent, err := os.ReadFile(setting.OAuth2.JWTSigningPrivateKeyFile)
require.NoError(t, err)
block, _ := pem.Decode(fileContent)
assert.NotNil(t, block)
assert.EqualValues(t, "PRIVATE KEY", block.Type)
parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
require.NoError(t, err)
return parsedKey
}
t.Run("RSA-2048", func(t *testing.T) {
defer test.MockVariableValue(&setting.OAuth2.JWTSigningPrivateKeyFile, filepath.Join(t.TempDir(), "jwt-rsa-2048.priv"))()
defer test.MockVariableValue(&setting.OAuth2.JWTSigningAlgorithm, "RS256")()
parsedKey := loadKey(t)
rsaPrivateKey := parsedKey.(*rsa.PrivateKey)
assert.EqualValues(t, 2048, rsaPrivateKey.N.BitLen())
t.Run("Load key with differ specified algorithm", func(t *testing.T) {
defer test.MockVariableValue(&setting.OAuth2.JWTSigningAlgorithm, "EdDSA")()
parsedKey := loadKey(t)
rsaPrivateKey := parsedKey.(*rsa.PrivateKey)
assert.EqualValues(t, 2048, rsaPrivateKey.N.BitLen())
})
})
t.Run("RSA-3072", func(t *testing.T) {
defer test.MockVariableValue(&setting.OAuth2.JWTSigningPrivateKeyFile, filepath.Join(t.TempDir(), "jwt-rsa-3072.priv"))()
defer test.MockVariableValue(&setting.OAuth2.JWTSigningAlgorithm, "RS384")()
parsedKey := loadKey(t)
rsaPrivateKey := parsedKey.(*rsa.PrivateKey)
assert.EqualValues(t, 3072, rsaPrivateKey.N.BitLen())
})
t.Run("RSA-4096", func(t *testing.T) {
defer test.MockVariableValue(&setting.OAuth2.JWTSigningPrivateKeyFile, filepath.Join(t.TempDir(), "jwt-rsa-4096.priv"))()
defer test.MockVariableValue(&setting.OAuth2.JWTSigningAlgorithm, "RS512")()
parsedKey := loadKey(t)
rsaPrivateKey := parsedKey.(*rsa.PrivateKey)
assert.EqualValues(t, 4096, rsaPrivateKey.N.BitLen())
})
t.Run("ECDSA-256", func(t *testing.T) {
defer test.MockVariableValue(&setting.OAuth2.JWTSigningPrivateKeyFile, filepath.Join(t.TempDir(), "jwt-ecdsa-256.priv"))()
defer test.MockVariableValue(&setting.OAuth2.JWTSigningAlgorithm, "ES256")()
parsedKey := loadKey(t)
ecdsaPrivateKey := parsedKey.(*ecdsa.PrivateKey)
assert.EqualValues(t, 256, ecdsaPrivateKey.Params().BitSize)
})
t.Run("ECDSA-384", func(t *testing.T) {
defer test.MockVariableValue(&setting.OAuth2.JWTSigningPrivateKeyFile, filepath.Join(t.TempDir(), "jwt-ecdsa-384.priv"))()
defer test.MockVariableValue(&setting.OAuth2.JWTSigningAlgorithm, "ES384")()
parsedKey := loadKey(t)
ecdsaPrivateKey := parsedKey.(*ecdsa.PrivateKey)
assert.EqualValues(t, 384, ecdsaPrivateKey.Params().BitSize)
})
t.Run("ECDSA-512", func(t *testing.T) {
defer test.MockVariableValue(&setting.OAuth2.JWTSigningPrivateKeyFile, filepath.Join(t.TempDir(), "jwt-ecdsa-512.priv"))()
defer test.MockVariableValue(&setting.OAuth2.JWTSigningAlgorithm, "ES512")()
parsedKey := loadKey(t)
ecdsaPrivateKey := parsedKey.(*ecdsa.PrivateKey)
assert.EqualValues(t, 521, ecdsaPrivateKey.Params().BitSize)
})
t.Run("EdDSA", func(t *testing.T) {
defer test.MockVariableValue(&setting.OAuth2.JWTSigningPrivateKeyFile, filepath.Join(t.TempDir(), "jwt-eddsa.priv"))()
defer test.MockVariableValue(&setting.OAuth2.JWTSigningAlgorithm, "EdDSA")()
parsedKey := loadKey(t)
assert.NotNil(t, parsedKey.(ed25519.PrivateKey))
})
}

View file

@ -47,7 +47,7 @@ func (ctx *Context) GetSiteCookie(name string) string {
// SetLTACookie will generate a LTA token and add it as an cookie. // SetLTACookie will generate a LTA token and add it as an cookie.
func (ctx *Context) SetLTACookie(u *user_model.User) error { func (ctx *Context) SetLTACookie(u *user_model.User) error {
days := 86400 * setting.LogInRememberDays days := 86400 * setting.LogInRememberDays
lookup, validator, err := auth_model.GenerateAuthToken(ctx, u.ID, timeutil.TimeStampNow().Add(int64(days))) lookup, validator, err := auth_model.GenerateAuthToken(ctx, u.ID, timeutil.TimeStampNow().Add(int64(days)), auth_model.LongTermAuthorization)
if err != nil { if err != nil {
return err return err
} }

View file

@ -243,6 +243,9 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
// find archive download count without existing release // find archive download count without existing release
genericOrphanCheck("Archive download count without existing Release", genericOrphanCheck("Archive download count without existing Release",
"repo_archive_download_count", "release", "repo_archive_download_count.release_id=release.id"), "repo_archive_download_count", "release", "repo_archive_download_count.release_id=release.id"),
// find authorization tokens without existing user
genericOrphanCheck("Authorization token without existing User",
"forgejo_auth_token", "user", "forgejo_auth_token.uid=user.id"),
) )
for _, c := range consistencyChecks { for _, c := range consistencyChecks {

View file

@ -749,3 +749,7 @@ func (f *DeadlineForm) Validate(req *http.Request, errs binding.Errors) binding.
ctx := context.GetValidateContext(req) ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale) return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
} }
type CommitNotesForm struct {
Notes string
}

View file

@ -10,6 +10,8 @@ import (
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
org_model "code.gitea.io/gitea/models/organization" org_model "code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
@ -117,7 +119,11 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue,
} }
for _, u := range uniqUsers { for _, u := range uniqUsers {
if u.ID != issue.Poster.ID { permission, err := access_model.GetUserRepoPermission(ctx, issue.Repo, u)
if err != nil {
return nil, fmt.Errorf("GetUserRepoPermission: %w", err)
}
if u.ID != issue.Poster.ID && permission.CanRead(unit.TypePullRequests) {
comment, err := issues_model.AddReviewRequest(ctx, issue, u, issue.Poster) comment, err := issues_model.AddReviewRequest(ctx, issue, u, issue.Poster)
if err != nil { if err != nil {
log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err) log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err)

View file

@ -70,7 +70,7 @@ func SendTestMail(email string) error {
} }
// sendUserMail sends a mail to the user // sendUserMail sends a mail to the user
func sendUserMail(language string, u *user_model.User, tpl base.TplName, code, subject, info string) { func sendUserMail(language string, u *user_model.User, tpl base.TplName, code, subject, info string) error {
locale := translation.NewLocale(language) locale := translation.NewLocale(language)
data := map[string]any{ data := map[string]any{
"locale": locale, "locale": locale,
@ -84,47 +84,66 @@ func sendUserMail(language string, u *user_model.User, tpl base.TplName, code, s
var content bytes.Buffer var content bytes.Buffer
if err := bodyTemplates.ExecuteTemplate(&content, string(tpl), data); err != nil { if err := bodyTemplates.ExecuteTemplate(&content, string(tpl), data); err != nil {
log.Error("Template: %v", err) return err
return
} }
msg := NewMessage(u.EmailTo(), subject, content.String()) msg := NewMessage(u.EmailTo(), subject, content.String())
msg.Info = fmt.Sprintf("UID: %d, %s", u.ID, info) msg.Info = fmt.Sprintf("UID: %d, %s", u.ID, info)
SendAsync(msg) SendAsync(msg)
return nil
} }
// SendActivateAccountMail sends an activation mail to the user (new user registration) // SendActivateAccountMail sends an activation mail to the user (new user registration)
func SendActivateAccountMail(locale translation.Locale, u *user_model.User) { func SendActivateAccountMail(ctx context.Context, u *user_model.User) error {
if setting.MailService == nil { if setting.MailService == nil {
// No mail service configured // No mail service configured
return return nil
} }
sendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.activate_account"), "activate account")
locale := translation.NewLocale(u.Language)
code, err := u.GenerateEmailAuthorizationCode(ctx, auth_model.UserActivation)
if err != nil {
return err
}
return sendUserMail(locale.Language(), u, mailAuthActivate, code, locale.TrString("mail.activate_account"), "activate account")
} }
// SendResetPasswordMail sends a password reset mail to the user // SendResetPasswordMail sends a password reset mail to the user
func SendResetPasswordMail(u *user_model.User) { func SendResetPasswordMail(ctx context.Context, u *user_model.User) error {
if setting.MailService == nil { if setting.MailService == nil {
// No mail service configured // No mail service configured
return return nil
} }
locale := translation.NewLocale(u.Language) locale := translation.NewLocale(u.Language)
sendUserMail(u.Language, u, mailAuthResetPassword, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.reset_password"), "recover account") code, err := u.GenerateEmailAuthorizationCode(ctx, auth_model.PasswordReset)
if err != nil {
return err
}
return sendUserMail(u.Language, u, mailAuthResetPassword, code, locale.TrString("mail.reset_password"), "recover account")
} }
// SendActivateEmailMail sends confirmation email to confirm new email address // SendActivateEmailMail sends confirmation email to confirm new email address
func SendActivateEmailMail(u *user_model.User, email string) { func SendActivateEmailMail(ctx context.Context, u *user_model.User, email string) error {
if setting.MailService == nil { if setting.MailService == nil {
// No mail service configured // No mail service configured
return return nil
} }
locale := translation.NewLocale(u.Language) locale := translation.NewLocale(u.Language)
code, err := u.GenerateEmailAuthorizationCode(ctx, auth_model.EmailActivation(email))
if err != nil {
return err
}
data := map[string]any{ data := map[string]any{
"locale": locale, "locale": locale,
"DisplayName": u.DisplayName(), "DisplayName": u.DisplayName(),
"ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale), "ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale),
"Code": u.GenerateEmailActivateCode(email), "Code": code,
"Email": email, "Email": email,
"Language": locale.Language(), "Language": locale.Language(),
} }
@ -132,14 +151,14 @@ func SendActivateEmailMail(u *user_model.User, email string) {
var content bytes.Buffer var content bytes.Buffer
if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthActivateEmail), data); err != nil { if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthActivateEmail), data); err != nil {
log.Error("Template: %v", err) return err
return
} }
msg := NewMessage(email, locale.TrString("mail.activate_email"), content.String()) msg := NewMessage(email, locale.TrString("mail.activate_email"), content.String())
msg.Info = fmt.Sprintf("UID: %d, activate email", u.ID) msg.Info = fmt.Sprintf("UID: %d, activate email", u.ID)
SendAsync(msg) SendAsync(msg)
return nil
} }
// SendRegisterNotifyMail triggers a notify e-mail by admin created a account. // SendRegisterNotifyMail triggers a notify e-mail by admin created a account.

View file

@ -22,9 +22,16 @@ import (
// //
// The payload is verifiable by the generated HMAC using the user secret. It contains: // The payload is verifiable by the generated HMAC using the user secret. It contains:
// | Timestamp | Action/Handler Type | Action/Handler Data | // | Timestamp | Action/Handler Type | Action/Handler Data |
//
//
// Version changelog
//
// v1 -> v2:
// Use 128 instead of 80 bits of the HMAC-SHA256 output.
const ( const (
tokenVersion1 byte = 1 tokenVersion1 byte = 1
tokenVersion2 byte = 2
tokenLifetimeInYears int = 1 tokenLifetimeInYears int = 1
) )
@ -70,7 +77,7 @@ func CreateToken(ht HandlerType, user *user_model.User, data []byte) (string, er
return "", err return "", err
} }
return encodingWithoutPadding.EncodeToString(append([]byte{tokenVersion1}, packagedData...)), nil return encodingWithoutPadding.EncodeToString(append([]byte{tokenVersion2}, packagedData...)), nil
} }
// ExtractToken extracts the action/user tuple from the token and verifies the content // ExtractToken extracts the action/user tuple from the token and verifies the content
@ -84,7 +91,7 @@ func ExtractToken(ctx context.Context, token string) (HandlerType, *user_model.U
return UnknownHandlerType, nil, nil, &ErrToken{"no data"} return UnknownHandlerType, nil, nil, &ErrToken{"no data"}
} }
if data[0] != tokenVersion1 { if data[0] != tokenVersion2 {
return UnknownHandlerType, nil, nil, &ErrToken{fmt.Sprintf("unsupported token version: %v", data[0])} return UnknownHandlerType, nil, nil, &ErrToken{fmt.Sprintf("unsupported token version: %v", data[0])}
} }
@ -124,5 +131,8 @@ func generateHmac(secret, payload []byte) []byte {
mac.Write(payload) mac.Write(payload)
hmac := mac.Sum(nil) hmac := mac.Sum(nil)
return hmac[:10] // RFC2104 recommends not using less then 80 bits // RFC2104 section 5 recommends that if you do HMAC truncation, you should use
// the max(80, hash_len/2) of the leftmost bits.
// For SHA256 this works out to using 128 of the leftmost bits.
return hmac[:16]
} }

View file

@ -307,9 +307,10 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
} }
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{ releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
RepoID: repo.ID, RepoID: repo.ID,
TagNames: tags, TagNames: tags,
IncludeTags: true, IncludeDrafts: true,
IncludeTags: true,
}) })
if err != nil { if err != nil {
return fmt.Errorf("db.Find[repo_model.Release]: %w", err) return fmt.Errorf("db.Find[repo_model.Release]: %w", err)
@ -394,13 +395,17 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
} }
newReleases = append(newReleases, rel) newReleases = append(newReleases, rel)
} else { } else {
rel.Title = parts[0]
rel.Note = note
rel.Sha1 = commit.ID.String() rel.Sha1 = commit.ID.String()
rel.CreatedUnix = timeutil.TimeStamp(createdAt.Unix()) rel.CreatedUnix = timeutil.TimeStamp(createdAt.Unix())
rel.NumCommits = commitsCount rel.NumCommits = commitsCount
if rel.IsTag && author != nil { if rel.IsTag {
rel.PublisherID = author.ID rel.Title = parts[0]
rel.Note = note
if author != nil {
rel.PublisherID = author.ID
}
} else {
rel.IsDraft = false
} }
if err = repo_model.UpdateRelease(ctx, rel); err != nil { if err = repo_model.UpdateRelease(ctx, rel); err != nil {
return fmt.Errorf("Update: %w", err) return fmt.Errorf("Update: %w", err)

View file

@ -96,6 +96,7 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
&user_model.BlockedUser{BlockID: u.ID}, &user_model.BlockedUser{BlockID: u.ID},
&user_model.BlockedUser{UserID: u.ID}, &user_model.BlockedUser{UserID: u.ID},
&actions_model.ActionRunnerToken{OwnerID: u.ID}, &actions_model.ActionRunnerToken{OwnerID: u.ID},
&auth_model.AuthorizationToken{UID: u.ID},
); err != nil { ); err != nil {
return fmt.Errorf("deleteBeans: %w", err) return fmt.Errorf("deleteBeans: %w", err)
} }

View file

@ -10,6 +10,7 @@ import (
"html/template" "html/template"
"net/http" "net/http"
"net/url" "net/url"
"regexp"
"strconv" "strconv"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
@ -202,6 +203,9 @@ func (d discordConvertor) Push(p *api.PushPayload) (DiscordPayload, error) {
// limit the commit message display to just the summary, otherwise it would be hard to read // limit the commit message display to just the summary, otherwise it would be hard to read
message := strings.TrimRight(strings.SplitN(commit.Message, "\n", 1)[0], "\r") message := strings.TrimRight(strings.SplitN(commit.Message, "\n", 1)[0], "\r")
// Escaping markdown character
message = escapeMarkdown(message)
// a limit of 50 is set because GitHub does the same // a limit of 50 is set because GitHub does the same
if utf8.RuneCountInString(message) > 50 { if utf8.RuneCountInString(message) > 50 {
message = fmt.Sprintf("%.47s...", message) message = fmt.Sprintf("%.47s...", message)
@ -365,3 +369,40 @@ func (d discordConvertor) createPayload(s *api.User, title, text, url string, co
}, },
} }
} }
var orderedListPattern = regexp.MustCompile(`(\d+)\.`)
var markdownPatterns = map[string]*regexp.Regexp{
"~": regexp.MustCompile(`\~(.*?)\~`),
"*": regexp.MustCompile(`\*(.*?)\*`),
"_": regexp.MustCompile(`\_(.*?)\_`),
}
var markdownToEscape = strings.NewReplacer(
"* ", "\\* ",
"`", "\\`",
"[", "\\[",
"]", "\\]",
"(", "\\(",
")", "\\)",
"#", "\\#",
"+ ", "\\+ ",
"- ", "\\- ",
"---", "\\---",
"!", "\\!",
"|", "\\|",
"<", "\\<",
">", "\\>",
)
// Escape Markdown characters
func escapeMarkdown(input string) string {
// Escaping ordered list
output := orderedListPattern.ReplaceAllString(input, "$1\\.")
for char, pattern := range markdownPatterns {
output = pattern.ReplaceAllString(output, fmt.Sprintf(`\%s$1\%s`, char, char))
}
return markdownToEscape.Replace(output)
}

View file

@ -94,6 +94,20 @@ func TestDiscordPayload(t *testing.T) {
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
}) })
t.Run("PushWithMarkdownCharactersInCommitMessage", func(t *testing.T) {
p := pushTestEscapeCommitMessagePayload()
pl, err := dc.Push(p)
require.NoError(t, err)
assert.Len(t, pl.Embeds, 1)
assert.Equal(t, "[test/repo:test] 2 new commits", pl.Embeds[0].Title)
assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) \\# conflicts\n\\# \\- some/conflicting/file.txt - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) \\# conflicts\n\\# \\- some/conflicting/file.txt - user1", pl.Embeds[0].Description)
assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
})
t.Run("Issue", func(t *testing.T) { t.Run("Issue", func(t *testing.T) {
p := issueTestPayload() p := issueTestPayload()
@ -346,3 +360,89 @@ func TestDiscordJSONPayload(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.Embeds[0].Description) assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.Embeds[0].Description)
} }
var escapedMarkdownTests = map[string]struct {
input string
expected string
}{
"Escape heading level 1": {
input: "# Heading level 1",
expected: "\\# Heading level 1",
},
"Escape heading level 2": {
input: "## Heading level 2",
expected: "\\#\\# Heading level 2",
},
"Escape heading level 3": {
input: "### Heading level 3",
expected: "\\#\\#\\# Heading level 3",
},
"Escape bold text": {
input: "**bold text**",
expected: "\\*\\*bold text\\*\\*",
},
"Escape italic text": {
input: "*italic text*",
expected: "\\*italic text\\*",
},
"Escape italic text underline": {
input: "_italic text_",
expected: "\\_italic text\\_",
},
"Escape strikethrough": {
input: "~~strikethrough~~",
expected: "\\~\\~strikethrough\\~\\~",
},
"Escape Ordered list item": {
input: "1. Ordered list item\n2. Second ordered list item\n999999999999. 999999999999 ordered list item",
expected: "1\\. Ordered list item\n2\\. Second ordered list item\n999999999999\\. 999999999999 ordered list item",
},
"Escape Unordered list item": {
input: "- Unordered list\n + using plus",
expected: "\\- Unordered list\n \\+ using plus",
},
"Escape bullet list item": {
input: "* Bullet list item",
expected: "\\* Bullet list item",
},
"Escape table": {
input: "| Table | Example |\n|-|-|\n| Lorem | Ipsum |",
expected: "\\| Table \\| Example \\|\n\\|-\\|-\\|\n\\| Lorem \\| Ipsum \\|",
},
"Escape link": {
input: "[Link to Forgejo](https://forgejo.org/)",
expected: "\\[Link to Forgejo\\]\\(https://forgejo.org/\\)",
},
"Escape Alt text for an image": {
input: "![Alt text for an image](https://forgejo.org/_astro/mascot-dark.1omhhgvT_Zm0N2n.webp)",
expected: "\\!\\[Alt text for an image\\]\\(https://forgejo.org/\\_astro/mascot-dark.1omhhgvT\\_Zm0N2n.webp\\)",
},
"Escape URL if it has markdown character": {
input: "https://forgejo.org/_astro/mascot-dark.1omhhgvT_Zm0N2n.webp",
expected: "https://forgejo.org/\\_astro/mascot-dark.1omhhgvT\\_Zm0N2n.webp",
},
"Escape blockquote text": {
input: "> Blockquote text.",
expected: "\\> Blockquote text.",
},
"Escape inline code": {
input: "`Inline code`",
expected: "\\`Inline code\\`",
},
"Escape multiple code": {
input: "```\nCode block\nwith multiple lines\n```\n",
expected: "\\`\\`\\`\nCode block\nwith multiple lines\n\\`\\`\\`\n",
},
"Escape horizontal rule": {
input: "---",
expected: "\\---",
},
}
func TestEscapeMarkdownChar(t *testing.T) {
for name, test := range escapedMarkdownTests {
t.Run(name, func(t *testing.T) {
assert.Equal(t, test.expected, escapeMarkdown(test.input))
})
}
}

View file

@ -72,6 +72,10 @@ func pushTestMultilineCommitMessagePayload() *api.PushPayload {
return pushTestPayloadWithCommitMessage("This is a commit summary ⚠️⚠️⚠️⚠️ containing 你好 ⚠️⚠️️\n\nThis is the message body.") return pushTestPayloadWithCommitMessage("This is a commit summary ⚠️⚠️⚠️⚠️ containing 你好 ⚠️⚠️️\n\nThis is the message body.")
} }
func pushTestEscapeCommitMessagePayload() *api.PushPayload {
return pushTestPayloadWithCommitMessage("# conflicts\n# - some/conflicting/file.txt")
}
func pushTestPayloadWithCommitMessage(message string) *api.PushPayload { func pushTestPayloadWithCommitMessage(message string) *api.PushPayload {
commit := &api.PayloadCommit{ commit := &api.PayloadCommit{
ID: "2020558fe2e34debb818a514715839cabd25e778", ID: "2020558fe2e34debb818a514715839cabd25e778",

View file

@ -2,7 +2,8 @@
{{svg "octicon-no-entry" 48}} {{svg "octicon-no-entry" 48}}
<h2>{{ctx.Locale.Tr "actions.runs.no_workflows"}}</h2> <h2>{{ctx.Locale.Tr "actions.runs.no_workflows"}}</h2>
{{if and .CanWriteCode .CanWriteActions}} {{if and .CanWriteCode .CanWriteActions}}
<p>{{ctx.Locale.Tr "actions.runs.no_workflows.quick_start" "https://forgejo.org/docs/latest/admin/actions/"}}</p> <p>{{ctx.Locale.Tr "actions.runs.no_workflows.help_write_access" "https://forgejo.org/docs/latest/user/actions/#quick-start" "https://forgejo.org/docs/latest/admin/runner-installation/"}}</p>
{{else}}
<p>{{ctx.Locale.Tr "actions.runs.no_workflows.help_no_write_access" "https://forgejo.org/docs/latest/user/actions/"}}</p>
{{end}} {{end}}
<p>{{ctx.Locale.Tr "actions.runs.no_workflows.documentation" "https://forgejo.org/docs/latest/admin/actions/"}}</p>
</div> </div>

View file

@ -128,6 +128,9 @@
</form> </form>
</div> </div>
</div> </div>
<div id="commit-notes-add-button" class="item">
{{ctx.Locale.Tr "repo.diff.git-notes.add"}}
</div>
</div> </div>
</div> </div>
{{end}} {{end}}
@ -275,10 +278,60 @@
<strong>{{.NoteCommit.Author.Name}}</strong> <strong>{{.NoteCommit.Author.Name}}</strong>
{{end}} {{end}}
<span class="text grey" id="note-authored-time">{{DateUtils.TimeSince .NoteCommit.Author.When}}</span> <span class="text grey" id="note-authored-time">{{DateUtils.TimeSince .NoteCommit.Author.When}}</span>
{{if and ($.Permission.CanWrite $.UnitTypeCode) (not $.Repository.IsArchived) (not .IsDeleted)}}
<div class="ui right">
<button id="commit-notes-edit-button" class="ui tiny primary button">{{ctx.Locale.Tr "edit"}}</button>
<button class="ui tiny button red show-modal" data-modal="#delete-note-modal">{{ctx.Locale.Tr "remove"}}</button>
</div>
<div class="ui small modal" id="delete-note-modal">
<div class="header">
{{ctx.Locale.Tr "repo.diff.git-notes.remove-header"}}
</div>
<div class="content">
<p>{{ctx.Locale.Tr "repo.diff.git-notes.remove-body"}}</p>
<div class="text right actions">
<form action="{{.Link}}/notes/remove" method="post">
{{.CsrfTokenHtml}}
<button type="button" class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button type="submit" class="ui red button" href="{{.Link}}/notes/remove">{{ctx.Locale.Tr "remove"}}</button>
</form>
</div>
</div>
</div>
{{end}}
</div> </div>
<div class="ui bottom attached info segment git-notes"> <div id="commit-notes-display-area" class="ui bottom attached info segment git-notes">
<pre class="commit-body">{{.NoteRendered | SanitizeHTML}}</pre> <pre class="commit-body">{{.NoteRendered | SanitizeHTML}}</pre>
</div> </div>
{{if and ($.Permission.CanWrite $.UnitTypeCode) (not $.Repository.IsArchived) (not .IsDeleted)}}
<div id="commit-notes-edit-area" class="ui bottom attached info segment git-notes tw-hidden">
<form class="ui form" action="{{.Link}}/notes" method="post">
{{.CsrfTokenHtml}}
<div class="field">
<textarea name="notes">{{.NoteRendered | SanitizeHTML}}</textarea>
</div>
<div class="field">
<button id="notes-save-button" class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
</div>
</form>
</div>
{{end}}
{{else if and ($.Permission.CanWrite $.UnitTypeCode) (not $.Repository.IsArchived) (not .IsDeleted)}}
<div id="commit-notes-add-area" class="ui tw-mt-3 segment tw-hidden">
<form class="ui form" action="{{.Link}}/notes" method="post">
{{.CsrfTokenHtml}}
<div class="field">
<textarea name="notes"></textarea>
</div>
<div class="field">
<button class="ui primary button">{{ctx.Locale.Tr "add"}}</button>
</div>
</form>
</div>
{{end}} {{end}}
{{template "repo/diff/box" .}} {{template "repo/diff/box" .}}
</div> </div>

View file

@ -2263,6 +2263,9 @@
}, },
"404": { "404": {
"$ref": "#/responses/notFound" "$ref": "#/responses/notFound"
},
"422": {
"$ref": "#/responses/error"
} }
} }
} }
@ -7375,6 +7378,101 @@
"$ref": "#/responses/validationError" "$ref": "#/responses/validationError"
} }
} }
},
"post": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Set a note corresponding to a single commit from a repository",
"operationId": "repoSetNote",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "string",
"description": "a git ref or commit sha",
"name": "sha",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/NoteOptions"
}
}
],
"responses": {
"200": {
"$ref": "#/responses/Note"
},
"404": {
"$ref": "#/responses/notFound"
},
"422": {
"$ref": "#/responses/validationError"
}
}
},
"delete": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Removes a note corresponding to a single commit from a repository",
"operationId": "repoRemoveNote",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "string",
"description": "a git ref or commit sha",
"name": "sha",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"$ref": "#/responses/empty"
},
"404": {
"$ref": "#/responses/notFound"
},
"422": {
"$ref": "#/responses/validationError"
}
}
} }
}, },
"/repos/{owner}/{repo}/git/refs": { "/repos/{owner}/{repo}/git/refs": {
@ -24601,6 +24699,16 @@
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"
}, },
"NoteOptions": {
"type": "object",
"properties": {
"message": {
"type": "string",
"x-go-name": "Message"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"NotificationCount": { "NotificationCount": {
"description": "NotificationCount number of unread notifications", "description": "NotificationCount number of unread notifications",
"type": "object", "type": "object",
@ -28350,7 +28458,7 @@
"parameterBodies": { "parameterBodies": {
"description": "parameterBodies", "description": "parameterBodies",
"schema": { "schema": {
"$ref": "#/definitions/SetUserQuotaGroupsOptions" "$ref": "#/definitions/NoteOptions"
} }
}, },
"quotaExceeded": { "quotaExceeded": {

View file

@ -63,9 +63,11 @@
<b>{{ctx.Locale.Tr "settings.subkeys"}}:</b> {{range .SubsKey}} {{.PaddedKeyID}} {{end}} <b>{{ctx.Locale.Tr "settings.subkeys"}}:</b> {{range .SubsKey}} {{.PaddedKeyID}} {{end}}
</div> </div>
<div class="flex-item-body"> <div class="flex-item-body">
<p>{{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .AddedUnix)}}</p> <p>
{{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .AddedUnix)}}
- -
<p>{{if not .ExpiredUnix.IsZero}}{{ctx.Locale.Tr "settings.valid_until_date" (DateUtils.AbsoluteShort .ExpiredUnix)}}{{else}}{{ctx.Locale.Tr "settings.valid_forever"}}{{end}}</p> {{if not .ExpiredUnix.IsZero}}{{ctx.Locale.Tr "settings.valid_until_date" (DateUtils.AbsoluteShort .ExpiredUnix)}}{{else}}{{ctx.Locale.Tr "settings.valid_forever"}}{{end}}
</p>
</div> </div>
</div> </div>
<div class="flex-item-trailing"> <div class="flex-item-trailing">

View file

@ -51,10 +51,12 @@
</div> </div>
<input id="pronouns-custom" name="pronouns" value="{{.SignedUser.Pronouns}}" maxlength="50"> <input id="pronouns-custom" name="pronouns" value="{{.SignedUser.Pronouns}}" maxlength="50">
</div> </div>
<div class="field {{if .Err_Email}}error{{end}}"> {{if not .SignedUser.KeepEmailPrivate}}
<label>{{ctx.Locale.Tr "email"}}</label> <div class="field">
<p id="signed-user-email">{{.SignedUser.Email}}</p> <label>{{ctx.Locale.Tr "email"}}</label>
</div> <p id="signed-user-email">{{.SignedUser.Email}}</p>
</div>
{{end}}
<div class="field {{if .Err_Biography}}error{{end}}"> <div class="field {{if .Err_Biography}}error{{end}}">
<label for="biography">{{ctx.Locale.Tr "user.user_bio"}}</label> <label for="biography">{{ctx.Locale.Tr "user.user_bio"}}</label>
<textarea id="biography" name="biography" rows="2" placeholder="{{ctx.Locale.Tr "settings.biography_placeholder"}}" maxlength="255">{{.SignedUser.Description}}</textarea> <textarea id="biography" name="biography" rows="2" placeholder="{{ctx.Locale.Tr "settings.biography_placeholder"}}" maxlength="255">{{.SignedUser.Description}}</textarea>

View file

@ -20,7 +20,6 @@ const workflow_trigger_notification_text = 'This workflow has a workflow_dispatc
test('workflow dispatch present', async ({browser}, workerInfo) => { test('workflow dispatch present', async ({browser}, workerInfo) => {
const context = await load_logged_in_context(browser, workerInfo, 'user2'); const context = await load_logged_in_context(browser, workerInfo, 'user2');
/** @type {import('@playwright/test').Page} */
const page = await context.newPage(); const page = await context.newPage();
await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0'); await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
@ -40,7 +39,6 @@ test('workflow dispatch error: missing inputs', async ({browser}, workerInfo) =>
test.skip(workerInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari; see https://codeberg.org/forgejo/forgejo/pulls/3334#issuecomment-2033383'); test.skip(workerInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari; see https://codeberg.org/forgejo/forgejo/pulls/3334#issuecomment-2033383');
const context = await load_logged_in_context(browser, workerInfo, 'user2'); const context = await load_logged_in_context(browser, workerInfo, 'user2');
/** @type {import('@playwright/test').Page} */
const page = await context.newPage(); const page = await context.newPage();
await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0'); await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
@ -62,14 +60,13 @@ test('workflow dispatch success', async ({browser}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari; see https://codeberg.org/forgejo/forgejo/pulls/3334#issuecomment-2033383'); test.skip(workerInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari; see https://codeberg.org/forgejo/forgejo/pulls/3334#issuecomment-2033383');
const context = await load_logged_in_context(browser, workerInfo, 'user2'); const context = await load_logged_in_context(browser, workerInfo, 'user2');
/** @type {import('@playwright/test').Page} */
const page = await context.newPage(); const page = await context.newPage();
await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0'); await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
await page.locator('#workflow_dispatch_dropdown>button').click(); await page.locator('#workflow_dispatch_dropdown>button').click();
await page.type('input[name="inputs[string2]"]', 'abc'); await page.fill('input[name="inputs[string2]"]', 'abc');
await page.locator('#workflow-dispatch-submit').click(); await page.locator('#workflow-dispatch-submit').click();
await expect(page.getByText('Workflow run was successfully requested.')).toBeVisible(); await expect(page.getByText('Workflow run was successfully requested.')).toBeVisible();

View file

@ -21,10 +21,10 @@ test('Load Homepage', async ({page}) => {
test('Register Form', async ({page}, workerInfo) => { test('Register Form', async ({page}, workerInfo) => {
const response = await page.goto('/user/sign_up'); const response = await page.goto('/user/sign_up');
expect(response?.status()).toBe(200); // Status OK expect(response?.status()).toBe(200); // Status OK
await page.type('input[name=user_name]', `e2e-test-${workerInfo.workerIndex}`); await page.fill('input[name=user_name]', `e2e-test-${workerInfo.workerIndex}`);
await page.type('input[name=email]', `e2e-test-${workerInfo.workerIndex}@test.com`); await page.fill('input[name=email]', `e2e-test-${workerInfo.workerIndex}@test.com`);
await page.type('input[name=password]', 'test123test123'); await page.fill('input[name=password]', 'test123test123');
await page.type('input[name=retype]', 'test123test123'); await page.fill('input[name=retype]', 'test123test123');
await page.click('form button.ui.primary.button:visible'); await page.click('form button.ui.primary.button:visible');
// Make sure we routed to the home page. Else login failed. // Make sure we routed to the home page. Else login failed.
expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`); expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);

View file

@ -0,0 +1,30 @@
// @ts-check
import {test, expect} from '@playwright/test';
import {login_user, load_logged_in_context} from './utils_e2e.ts';
test.beforeAll(async ({browser}, workerInfo) => {
await login_user(browser, workerInfo, 'user2');
});
test('Change git note', async ({browser}, workerInfo) => {
const context = await load_logged_in_context(browser, workerInfo, 'user2');
const page = await context.newPage();
let response = await page.goto('/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d');
expect(response?.status()).toBe(200);
await page.locator('#commit-notes-edit-button').click();
let textarea = page.locator('textarea[name="notes"]');
await expect(textarea).toBeVisible();
await textarea.fill('This is a new note');
await page.locator('#notes-save-button').click();
expect(response?.status()).toBe(200);
response = await page.goto('/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d');
expect(response?.status()).toBe(200);
textarea = page.locator('textarea[name="notes"]');
await expect(textarea).toHaveText('This is a new note');
});

View file

@ -4,19 +4,18 @@
// web_src/js/features/repo-issue** // web_src/js/features/repo-issue**
// @watch end // @watch end
import {expect} from '@playwright/test'; /* eslint playwright/expect-expect: ["error", { "assertFunctionNames": ["check_wip"] }] */
import {expect, type Page} from '@playwright/test';
import {test, login_user, login} from './utils_e2e.ts'; import {test, login_user, login} from './utils_e2e.ts';
test.beforeAll(async ({browser}, workerInfo) => { test.beforeAll(async ({browser}, workerInfo) => {
await login_user(browser, workerInfo, 'user2'); await login_user(browser, workerInfo, 'user2');
}); });
/* eslint-disable playwright/expect-expect */
// some tests are reported to have no assertions,
// which is not correct, because they use the global helper function
test.describe('Pull: Toggle WIP', () => { test.describe('Pull: Toggle WIP', () => {
const prTitle = 'pull5'; const prTitle = 'pull5';
async function toggle_wip_to({page}, should) { async function toggle_wip_to({page}, should: boolean) {
await page.waitForLoadState('domcontentloaded'); await page.waitForLoadState('domcontentloaded');
if (should) { if (should) {
await page.getByText('Still in progress?').click(); await page.getByText('Still in progress?').click();
@ -25,7 +24,7 @@ test.describe('Pull: Toggle WIP', () => {
} }
} }
async function check_wip({page}, is) { async function check_wip({page}, is: boolean) {
const elemTitle = 'h1'; const elemTitle = 'h1';
const stateLabel = '.issue-state-label'; const stateLabel = '.issue-state-label';
await page.waitForLoadState('domcontentloaded'); await page.waitForLoadState('domcontentloaded');
@ -96,12 +95,11 @@ test.describe('Pull: Toggle WIP', () => {
await expect(page.locator('h1')).toContainText(maxLenStr); await expect(page.locator('h1')).toContainText(maxLenStr);
}); });
}); });
/* eslint-enable playwright/expect-expect */
test('Issue: Labels', async ({browser}, workerInfo) => { test('Issue: Labels', async ({browser}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636'); test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636');
async function submitLabels({page}) { async function submitLabels({page}: {page: Page}) {
const submitted = page.waitForResponse('/user2/repo1/issues/labels'); const submitted = page.waitForResponse('/user2/repo1/issues/labels');
await page.locator('textarea').first().click(); // close via unrelated element await page.locator('textarea').first().click(); // close via unrelated element
await submitted; await submitted;
@ -199,7 +197,7 @@ test('New Issue: Assignees', async ({browser}, workerInfo) => {
// Assign other user (with searchbox) // Assign other user (with searchbox)
await page.locator('.select-assignees.dropdown').click(); await page.locator('.select-assignees.dropdown').click();
await page.type('.select-assignees .menu .search input', 'user4'); await page.fill('.select-assignees .menu .search input', 'user4');
await expect(page.locator('.select-assignees .menu .item').filter({hasText: 'user2'})).toBeHidden(); await expect(page.locator('.select-assignees .menu .item').filter({hasText: 'user2'})).toBeHidden();
await expect(page.locator('.select-assignees .menu .item').filter({hasText: 'user4'})).toBeVisible(); await expect(page.locator('.select-assignees .menu .item').filter({hasText: 'user4'})).toBeVisible();
await page.locator('.select-assignees .menu .item').filter({hasText: 'user4'}).click(); await page.locator('.select-assignees .menu .item').filter({hasText: 'user4'}).click();

View file

@ -29,7 +29,7 @@ test('markdown indentation', async ({browser}, workerInfo) => {
// Indent, then unindent first line // Indent, then unindent first line
await textarea.focus(); await textarea.focus();
await textarea.evaluate((it) => it.setSelectionRange(0, 0)); await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(0, 0));
await indent.click(); await indent.click();
await expect(textarea).toHaveValue(`${tab}* first\n* second\n* third\n* last`); await expect(textarea).toHaveValue(`${tab}* first\n* second\n* third\n* last`);
await unindent.click(); await unindent.click();
@ -45,7 +45,7 @@ test('markdown indentation', async ({browser}, workerInfo) => {
// Subsequently, select a chunk of 2nd and 3rd line and indent both, preserving the cursor position in relation to text // Subsequently, select a chunk of 2nd and 3rd line and indent both, preserving the cursor position in relation to text
await textarea.focus(); await textarea.focus();
await textarea.evaluate((it) => it.setSelectionRange(it.value.indexOf('cond'), it.value.indexOf('hird'))); await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('cond'), it.value.indexOf('hird')));
await indent.click(); await indent.click();
const lines23 = `* first\n${tab}${tab}* second\n${tab}* third\n* last`; const lines23 = `* first\n${tab}${tab}* second\n${tab}* third\n* last`;
await expect(textarea).toHaveValue(lines23); await expect(textarea).toHaveValue(lines23);
@ -60,7 +60,7 @@ test('markdown indentation', async ({browser}, workerInfo) => {
// Indent and unindent with cursor at the end of the line // Indent and unindent with cursor at the end of the line
await textarea.focus(); await textarea.focus();
await textarea.evaluate((it) => it.setSelectionRange(it.value.indexOf('cond'), it.value.indexOf('cond'))); await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('cond'), it.value.indexOf('cond')));
await textarea.press('End'); await textarea.press('End');
await indent.click(); await indent.click();
await expect(textarea).toHaveValue(`* first\n${tab}* second\n* third\n* last`); await expect(textarea).toHaveValue(`* first\n${tab}* second\n* third\n* last`);
@ -69,7 +69,7 @@ test('markdown indentation', async ({browser}, workerInfo) => {
// Check that Tab does work after input // Check that Tab does work after input
await textarea.focus(); await textarea.focus();
await textarea.evaluate((it) => it.setSelectionRange(it.value.length, it.value.length)); await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
await textarea.press('Shift+Enter'); // Avoid triggering the prefix continuation feature await textarea.press('Shift+Enter'); // Avoid triggering the prefix continuation feature
await textarea.pressSequentially('* least'); await textarea.pressSequentially('* least');
await indent.click(); await indent.click();
@ -78,7 +78,7 @@ test('markdown indentation', async ({browser}, workerInfo) => {
// Check that partial indents are cleared // Check that partial indents are cleared
await textarea.focus(); await textarea.focus();
await textarea.fill(initText); await textarea.fill(initText);
await textarea.evaluate((it) => it.setSelectionRange(it.value.indexOf('* second'), it.value.indexOf('* second'))); await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('* second'), it.value.indexOf('* second')));
await textarea.pressSequentially(' '); await textarea.pressSequentially(' ');
await unindent.click(); await unindent.click();
await expect(textarea).toHaveValue(initText); await expect(textarea).toHaveValue(initText);
@ -99,7 +99,7 @@ test('markdown list continuation', async ({browser}, workerInfo) => {
await textarea.fill(initText); await textarea.fill(initText);
// Test continuation of '* ' prefix // Test continuation of '* ' prefix
await textarea.evaluate((it) => it.setSelectionRange(it.value.indexOf('cond'), it.value.indexOf('cond'))); await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('cond'), it.value.indexOf('cond')));
await textarea.press('End'); await textarea.press('End');
await textarea.press('Enter'); await textarea.press('Enter');
await textarea.pressSequentially('middle'); await textarea.pressSequentially('middle');
@ -112,7 +112,7 @@ test('markdown list continuation', async ({browser}, workerInfo) => {
await expect(textarea).toHaveValue(`* first\n* second\n${tab}* middle\n${tab}* muddle\n* third\n* last`); await expect(textarea).toHaveValue(`* first\n* second\n${tab}* middle\n${tab}* muddle\n* third\n* last`);
// Test breaking in the middle of a line // Test breaking in the middle of a line
await textarea.evaluate((it) => it.setSelectionRange(it.value.lastIndexOf('ddle'), it.value.lastIndexOf('ddle'))); await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.lastIndexOf('ddle'), it.value.lastIndexOf('ddle')));
await textarea.pressSequentially('tate'); await textarea.pressSequentially('tate');
await textarea.press('Enter'); await textarea.press('Enter');
await textarea.pressSequentially('me'); await textarea.pressSequentially('me');
@ -120,7 +120,7 @@ test('markdown list continuation', async ({browser}, workerInfo) => {
// Test not triggering when Shift held // Test not triggering when Shift held
await textarea.fill(initText); await textarea.fill(initText);
await textarea.evaluate((it) => it.setSelectionRange(it.value.length, it.value.length)); await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
await textarea.press('Shift+Enter'); await textarea.press('Shift+Enter');
await textarea.press('Enter'); await textarea.press('Enter');
await textarea.pressSequentially('...but not least'); await textarea.pressSequentially('...but not least');
@ -128,28 +128,28 @@ test('markdown list continuation', async ({browser}, workerInfo) => {
// Test continuation of ordered list // Test continuation of ordered list
await textarea.fill(`1. one\n2. two`); await textarea.fill(`1. one\n2. two`);
await textarea.evaluate((it) => it.setSelectionRange(it.value.length, it.value.length)); await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
await textarea.press('Enter'); await textarea.press('Enter');
await textarea.pressSequentially('three'); await textarea.pressSequentially('three');
await expect(textarea).toHaveValue(`1. one\n2. two\n3. three`); await expect(textarea).toHaveValue(`1. one\n2. two\n3. three`);
// Test continuation of alternative ordered list syntax // Test continuation of alternative ordered list syntax
await textarea.fill(`1) one\n2) two`); await textarea.fill(`1) one\n2) two`);
await textarea.evaluate((it) => it.setSelectionRange(it.value.length, it.value.length)); await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
await textarea.press('Enter'); await textarea.press('Enter');
await textarea.pressSequentially('three'); await textarea.pressSequentially('three');
await expect(textarea).toHaveValue(`1) one\n2) two\n3) three`); await expect(textarea).toHaveValue(`1) one\n2) two\n3) three`);
// Test continuation of blockquote // Test continuation of blockquote
await textarea.fill(`> knowledge is power`); await textarea.fill(`> knowledge is power`);
await textarea.evaluate((it) => it.setSelectionRange(it.value.length, it.value.length)); await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
await textarea.press('Enter'); await textarea.press('Enter');
await textarea.pressSequentially('france is bacon'); await textarea.pressSequentially('france is bacon');
await expect(textarea).toHaveValue(`> knowledge is power\n> france is bacon`); await expect(textarea).toHaveValue(`> knowledge is power\n> france is bacon`);
// Test continuation of checklists // Test continuation of checklists
await textarea.fill(`- [ ] have a problem\n- [x] create a solution`); await textarea.fill(`- [ ] have a problem\n- [x] create a solution`);
await textarea.evaluate((it) => it.setSelectionRange(it.value.length, it.value.length)); await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
await textarea.press('Enter'); await textarea.press('Enter');
await textarea.pressSequentially('write a test'); await textarea.pressSequentially('write a test');
await expect(textarea).toHaveValue(`- [ ] have a problem\n- [x] create a solution\n- [ ] write a test`); await expect(textarea).toHaveValue(`- [ ] have a problem\n- [x] create a solution\n- [ ] write a test`);
@ -174,7 +174,7 @@ test('markdown list continuation', async ({browser}, workerInfo) => {
]; ];
for (const prefix of prefixes) { for (const prefix of prefixes) {
await textarea.fill(`${prefix}one`); await textarea.fill(`${prefix}one`);
await textarea.evaluate((it) => it.setSelectionRange(it.value.length, it.value.length)); await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
await textarea.press('Enter'); await textarea.press('Enter');
await textarea.pressSequentially('two'); await textarea.pressSequentially('two');
await expect(textarea).toHaveValue(`${prefix}one\n${prefix}two`); await expect(textarea).toHaveValue(`${prefix}one\n${prefix}two`);

View file

@ -3,14 +3,14 @@
// routers/web/repo/issue.go // routers/web/repo/issue.go
// @watch end // @watch end
import {expect} from '@playwright/test'; import {expect, type Locator} from '@playwright/test';
import {test, login_user, load_logged_in_context} from './utils_e2e.ts'; import {test, login_user, load_logged_in_context} from './utils_e2e.ts';
test.beforeAll(async ({browser}, workerInfo) => { test.beforeAll(async ({browser}, workerInfo) => {
await login_user(browser, workerInfo, 'user2'); await login_user(browser, workerInfo, 'user2');
}); });
const assertReactionCounts = (comment, counts) => const assertReactionCounts = (comment: Locator, counts: unknown) =>
expect(async () => { expect(async () => {
await expect(comment.locator('.reactions')).toBeVisible(); await expect(comment.locator('.reactions')).toBeVisible();
@ -29,7 +29,7 @@ const assertReactionCounts = (comment, counts) =>
return expect(reactions).toStrictEqual(counts); return expect(reactions).toStrictEqual(counts);
}).toPass(); }).toPass();
async function toggleReaction(menu, reaction) { async function toggleReaction(menu: Locator, reaction: string) {
await menu.evaluateAll((menus) => menus[0].focus()); await menu.evaluateAll((menus) => menus[0].focus());
await menu.locator('.add-reaction').click(); await menu.locator('.add-reaction').click();
await menu.locator(`[role=menuitem][data-reaction-content="${reaction}"]`).click(); await menu.locator(`[role=menuitem][data-reaction-content="${reaction}"]`).click();

View file

@ -4,7 +4,7 @@
// services/gitdiff/** // services/gitdiff/**
// @watch end // @watch end
import {expect} from '@playwright/test'; import {expect, type Page} from '@playwright/test';
import {test, login_user, login} from './utils_e2e.ts'; import {test, login_user, login} from './utils_e2e.ts';
import {accessibilityCheck} from './shared/accessibility.ts'; import {accessibilityCheck} from './shared/accessibility.ts';
@ -12,7 +12,7 @@ test.beforeAll(async ({browser}, workerInfo) => {
await login_user(browser, workerInfo, 'user2'); await login_user(browser, workerInfo, 'user2');
}); });
async function assertSelectedLines(page, nums) { async function assertSelectedLines(page: Page, nums: string[]) {
const pageAssertions = async () => { const pageAssertions = async () => {
expect( expect(
await Promise.all((await page.locator('tr.active [data-line-number]').all()).map((line) => line.getAttribute('data-line-number'))), await Promise.all((await page.locator('tr.active [data-line-number]').all()).map((line) => line.getAttribute('data-line-number'))),

View file

@ -3,9 +3,9 @@ import {AxeBuilder} from '@axe-core/playwright';
export async function accessibilityCheck({page}: {page: Page}, includes: string[], excludes: string[], disabledRules: string[]) { export async function accessibilityCheck({page}: {page: Page}, includes: string[], excludes: string[], disabledRules: string[]) {
// contrast of inline links is still a global issue in Forgejo // contrast of inline links is still a global issue in Forgejo
disabledRules += 'link-in-text-block'; disabledRules.push('link-in-text-block');
let accessibilityScanner = await new AxeBuilder({page}) let accessibilityScanner = new AxeBuilder({page})
.disableRules(disabledRules); .disableRules(disabledRules);
// passing the whole array seems to be not supported, // passing the whole array seems to be not supported,
// iterating has the nice side-effectof skipping this if the array is empty // iterating has the nice side-effectof skipping this if the array is empty

View file

@ -33,8 +33,8 @@ export async function login_user(browser: Browser, workerInfo: TestInfo, user: s
expect(response?.status()).toBe(200); // Status OK expect(response?.status()).toBe(200); // Status OK
// Fill out form // Fill out form
await page.type('input[name=user_name]', user); await page.fill('input[name=user_name]', user);
await page.type('input[name=password]', LOGIN_PASSWORD); await page.fill('input[name=password]', LOGIN_PASSWORD);
await page.click('form button.ui.primary.button:visible'); await page.click('form button.ui.primary.button:visible');
await page.waitForLoadState(); await page.waitForLoadState();
@ -48,15 +48,13 @@ export async function login_user(browser: Browser, workerInfo: TestInfo, user: s
} }
export async function load_logged_in_context(browser: Browser, workerInfo: TestInfo, user: string) { export async function load_logged_in_context(browser: Browser, workerInfo: TestInfo, user: string) {
let context;
try { try {
context = await test_context(browser, {storageState: `${ARTIFACTS_PATH}/state-${user}-${workerInfo.workerIndex}.json`}); return await test_context(browser, {storageState: `${ARTIFACTS_PATH}/state-${user}-${workerInfo.workerIndex}.json`});
} catch (err) { } catch (err) {
if (err.code === 'ENOENT') { if (err.code === 'ENOENT') {
throw new Error(`Could not find state for '${user}'. Did you call login_user(browser, workerInfo, '${user}') in test.beforeAll()?`); throw new Error(`Could not find state for '${user}'. Did you call login_user(browser, workerInfo, '${user}') in test.beforeAll()?`);
} }
} }
return context;
} }
export async function login({browser}: {browser: Browser}, workerInfo: TestInfo) { export async function login({browser}: {browser: Browser}, workerInfo: TestInfo) {

View file

@ -8,6 +8,8 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"os"
"regexp"
"testing" "testing"
"time" "time"
@ -17,6 +19,8 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
var rootPathRe = regexp.MustCompile("\\[repository\\]\nROOT\\s=\\s.*")
func onForgejoRunTB(t testing.TB, callback func(testing.TB, *url.URL), prepare ...bool) { func onForgejoRunTB(t testing.TB, callback func(testing.TB, *url.URL), prepare ...bool) {
if len(prepare) == 0 || prepare[0] { if len(prepare) == 0 || prepare[0] {
defer tests.PrepareTestEnv(t, 1)() defer tests.PrepareTestEnv(t, 1)()
@ -37,7 +41,13 @@ func onForgejoRunTB(t testing.TB, callback func(testing.TB, *url.URL), prepare .
require.NoError(t, err) require.NoError(t, err)
u.Host = listener.Addr().String() u.Host = listener.Addr().String()
// Override repository root in config.
conf, err := os.ReadFile(setting.CustomConf)
require.NoError(t, err)
require.NoError(t, os.WriteFile(setting.CustomConf, rootPathRe.ReplaceAll(conf, []byte("[repository]\nROOT = "+setting.RepoRootPath)), 0o644))
defer func() { defer func() {
require.NoError(t, os.WriteFile(setting.CustomConf, conf, 0o644))
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
s.Shutdown(ctx) s.Shutdown(ctx)
cancel() cancel()

View file

@ -30,7 +30,6 @@ test('WebAuthn register & login flow', async ({browser, request}, workerInfo) =>
transport: 'usb', transport: 'usb',
automaticPresenceSimulation: true, automaticPresenceSimulation: true,
isUserVerified: true, isUserVerified: true,
backupEligibility: true, // TODO: this doesn't seem to be available?!
}, },
}); });

View file

@ -109,4 +109,24 @@ func TestFeed(t *testing.T) {
}) })
}) })
}) })
t.Run("View permission", func(t *testing.T) {
t.Run("Anomynous", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", "/org3/repo3/rss/branch/master")
MakeRequest(t, req, http.StatusNotFound)
})
t.Run("No code permission", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
session := loginUser(t, "user8")
req := NewRequest(t, "GET", "/org3/repo3/rss/branch/master")
session.MakeRequest(t, req, http.StatusNotFound)
})
t.Run("With code permission", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
session := loginUser(t, "user9")
req := NewRequest(t, "GET", "/org3/repo3/rss/branch/master")
session.MakeRequest(t, req, http.StatusOK)
})
})
} }

View file

@ -17,6 +17,8 @@ import (
"code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/routers" "code.gitea.io/gitea/routers"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
) )
func TestAPIForkAsAdminIgnoringLimits(t *testing.T) { func TestAPIForkAsAdminIgnoringLimits(t *testing.T) {
@ -106,3 +108,44 @@ func TestAPIDisabledForkRepo(t *testing.T) {
session.MakeRequest(t, req, http.StatusNotFound) session.MakeRequest(t, req, http.StatusNotFound)
}) })
} }
func TestAPIForkListPrivateRepo(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user5")
token := getTokenForLoggedInUser(t, session,
auth_model.AccessTokenScopeWriteRepository,
auth_model.AccessTokenScopeWriteOrganization)
org23 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23, Visibility: api.VisibleTypePrivate})
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{
Organization: &org23.Name,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusAccepted)
t.Run("Anomynous", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks")
resp := MakeRequest(t, req, http.StatusOK)
var forks []*api.Repository
DecodeJSON(t, resp, &forks)
assert.Empty(t, forks)
assert.EqualValues(t, "0", resp.Header().Get("X-Total-Count"))
})
t.Run("Logged in", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks").AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var forks []*api.Repository
DecodeJSON(t, resp, &forks)
assert.Len(t, forks, 1)
assert.EqualValues(t, "1", resp.Header().Get("X-Total-Count"))
})
}

View file

@ -218,3 +218,57 @@ func TestAPIOrgSearchEmptyTeam(t *testing.T) {
assert.EqualValues(t, "Empty", data.Data[0].Name) assert.EqualValues(t, "Empty", data.Data[0].Name)
} }
} }
func TestAPIOrgChangeEmail(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user1")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
t.Run("Invalid", func(t *testing.T) {
newMail := "invalid"
settings := api.EditOrgOption{Email: &newMail}
resp := MakeRequest(t, NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &settings).AddTokenAuth(token), http.StatusUnprocessableEntity)
var org *api.Organization
DecodeJSON(t, resp, &org)
assert.Empty(t, org.Email)
})
t.Run("Valid", func(t *testing.T) {
newMail := "example@example.com"
settings := api.EditOrgOption{Email: &newMail}
resp := MakeRequest(t, NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &settings).AddTokenAuth(token), http.StatusOK)
var org *api.Organization
DecodeJSON(t, resp, &org)
assert.Equal(t, "example@example.com", org.Email)
})
t.Run("NoChange", func(t *testing.T) {
settings := api.EditOrgOption{}
resp := MakeRequest(t, NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &settings).AddTokenAuth(token), http.StatusOK)
var org *api.Organization
DecodeJSON(t, resp, &org)
assert.Equal(t, "example@example.com", org.Email)
})
t.Run("Empty", func(t *testing.T) {
newMail := ""
settings := api.EditOrgOption{Email: &newMail}
resp := MakeRequest(t, NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &settings).AddTokenAuth(token), http.StatusOK)
var org *api.Organization
DecodeJSON(t, resp, &org)
assert.Empty(t, org.Email)
})
}

View file

@ -36,3 +36,22 @@ func TestAPICompareBranches(t *testing.T) {
assert.Equal(t, 2, apiResp.TotalCommits) assert.Equal(t, 2, apiResp.TotalCommits)
assert.Len(t, apiResp.Commits, 2) assert.Len(t, apiResp.Commits, 2)
} }
func TestAPICompareCommits(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// Login as User2.
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/c8e31bc...8babce9").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var apiResp *api.Compare
DecodeJSON(t, resp, &apiResp)
assert.Equal(t, 2, apiResp.TotalCommits)
assert.Len(t, apiResp.Commits, 2)
}

View file

@ -4,11 +4,13 @@
package integration package integration
import ( import (
"fmt"
"net/http" "net/http"
"net/url" "net/url"
"testing" "testing"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
@ -16,7 +18,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestAPIReposGitNotes(t *testing.T) { func TestAPIReposGetGitNotes(t *testing.T) {
onGiteaRun(t, func(*testing.T, *url.URL) { onGiteaRun(t, func(*testing.T, *url.URL) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// Login as User2. // Login as User2.
@ -44,3 +46,53 @@ func TestAPIReposGitNotes(t *testing.T) {
assert.NotNil(t, apiData.Commit.RepoCommit.Verification) assert.NotNil(t, apiData.Commit.RepoCommit.Verification)
}) })
} }
func TestAPIReposSetGitNotes(t *testing.T) {
onGiteaRun(t, func(*testing.T, *url.URL) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestf(t, "GET", "/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName())
resp := MakeRequest(t, req, http.StatusOK)
var apiData api.Note
DecodeJSON(t, resp, &apiData)
assert.Equal(t, "This is a test note\n", apiData.Message)
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName()), &api.NoteOptions{
Message: "This is a new note",
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiData)
assert.Equal(t, "This is a new note\n", apiData.Message)
req = NewRequestf(t, "GET", "/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName())
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiData)
assert.Equal(t, "This is a new note\n", apiData.Message)
})
}
func TestAPIReposDeleteGitNotes(t *testing.T) {
onGiteaRun(t, func(*testing.T, *url.URL) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestf(t, "GET", "/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName())
resp := MakeRequest(t, req, http.StatusOK)
var apiData api.Note
DecodeJSON(t, resp, &apiData)
assert.Equal(t, "This is a test note\n", apiData.Message)
req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName()).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
req = NewRequestf(t, "GET", "/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName())
MakeRequest(t, req, http.StatusNotFound)
})
}

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