mirror of
https://codeberg.org/forgejo/forgejo
synced 2024-11-22 01:44:24 +01:00
Compare commits
102 commits
735c2c8ada
...
16d06705b3
Author | SHA1 | Date | |
---|---|---|---|
16d06705b3 | |||
7015bdfa48 | |||
a69943085a | |||
45fa9e5ae9 | |||
1316f4d338 | |||
f4c70a3c43 | |||
3674e90c93 | |||
0a39ee3bbe | |||
8636a8b228 | |||
f5c0570533 | |||
b7c15f7b70 | |||
298863c701 | |||
f90928507a | |||
71ff98d61d | |||
1f4be5baad | |||
cff7754735 | |||
0fb3e45ca5 | |||
1806db31d1 | |||
7c1f3a7594 | |||
3c3c9b22a9 | |||
73cb6c9204 | |||
25354c03a5 | |||
18cecf124f | |||
2f1b1d8b80 | |||
c3653e0eaa | |||
4a3f8f3004 | |||
6cfaebf043 | |||
e0b37253c8 | |||
b9697f5227 | |||
6553148de9 | |||
387ed7e072 | |||
289c22cc4a | |||
4163402f5e | |||
662e9c53d9 | |||
e31090cf4b | |||
2cc3278791 | |||
693f7731f9 | |||
02f4d3bd2d | |||
b6869d643e | |||
abec2442b7 | |||
64a89c8d33 | |||
76f172b080 | |||
a5363a539b | |||
b5161325ef | |||
9701e5e0ff | |||
da40383cf4 | |||
8e94947ed9 | |||
c01a03e93d | |||
ca0cd42d7a | |||
01c9c19536 | |||
1b9d1240eb | |||
d2dc4fae3a | |||
e434ecdaca | |||
569a67327c | |||
146824badc | |||
eaa66f85f6 | |||
e4eb82b738 | |||
969a6ab24a | |||
7d59060dc6 | |||
308812a82e | |||
fc26becba4 | |||
02a2dbef69 | |||
013cc1dee4 | |||
6d0f2c1b82 | |||
2cccc02e76 | |||
356aa6521b | |||
cf323a3d55 | |||
6bab3c374c | |||
570e8cec9e | |||
bf810fa8d3 | |||
66dfb2813c | |||
95a8987844 | |||
9fd2df6e30 | |||
7f707b2a6f | |||
5406310f3e | |||
b21cc70dd7 | |||
4a5d9d4b78 | |||
1e1b162cbe | |||
b1bc294955 | |||
01ab0583f5 | |||
786dfc7fb8 | |||
061abe6004 | |||
3e3ef76808 | |||
7067cc7da4 | |||
e6bbecb02d | |||
b70196653f | |||
9508aa7713 | |||
1ce33aa38d | |||
0fa436c373 | |||
296935b0d7 | |||
1c25bbe773 | |||
c8d97e5594 | |||
e426a52a87 | |||
faa796feb9 | |||
cdc38ace39 | |||
4043377377 | |||
c226b4d00a | |||
19c9e0a0c2 | |||
d1520cf08d | |||
f9169eac96 | |||
f8cbd6301e | |||
a5ba7cadf7 |
|
@ -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
|
|
|
@ -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
|
|
205
.forgejo/workflows/issue-labels.yml
Normal file
205
.forgejo/workflows/issue-labels.yml
Normal 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 }}
|
|
@ -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
|
|
|
@ -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 }}
|
|
||||||
|
|
|
@ -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 }}
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
17
Makefile
17
Makefile
|
@ -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
|
||||||
|
|
|
@ -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).
|
||||||
|
|
203
build/generate-disposable-email.go
Normal file
203
build/generate-disposable-email.go
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
// Copyright 2024 James Hatfield
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
//go:build ignore
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"go/format"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const disposableEmailListURL string = "https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/%s/disposable_email_blocklist.conf"
|
||||||
|
|
||||||
|
var (
|
||||||
|
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")
|
||||||
|
check *bool = flag.Bool("check", false, "Check if the current output file matches the current upstream list")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *check {
|
||||||
|
// read in the local copy of the domain list
|
||||||
|
local, err := get_local_file()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("File Read Error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate the remote copy of the domain list
|
||||||
|
remote, err := generate()
|
||||||
|
if err != nil {
|
||||||
|
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 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 url string = fmt.Sprintf(disposableEmailListURL, *gitRef)
|
||||||
|
|
||||||
|
// download the domain list
|
||||||
|
res, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// go through all entries (1 domain per line)
|
||||||
|
scanner := bufio.NewScanner(bytes.NewReader(body))
|
||||||
|
|
||||||
|
var arrDomains []string
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
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
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
_, err = sb.WriteString("[]string{\n")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range arrDomains {
|
||||||
|
_, err = sb.WriteString(fmt.Sprintf("\t%q,\n", item))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = sb.WriteString("}")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert the values into file
|
||||||
|
final := fmt.Sprintf(hdr, url, sb.String())
|
||||||
|
|
||||||
|
return format.Source([]byte(final))
|
||||||
|
}
|
||||||
|
|
||||||
|
const hdr = `
|
||||||
|
// Copyright 2024 James Hatfield
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Code generated by build/generate-disposable-email.go. DO NOT EDIT
|
||||||
|
// Sourced from %s
|
||||||
|
package setting
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
var DisposableEmailDomains = sync.OnceValue(func() []string {
|
||||||
|
return %s
|
||||||
|
})
|
||||||
|
`
|
6
go.mod
6
go.mod
|
@ -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
12
go.sum
|
@ -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=
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
19
models/forgejo_migrations/v24.go
Normal file
19
models/forgejo_migrations/v24.go
Normal 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
|
||||||
|
}
|
|
@ -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,28 +307,10 @@ func GetLatestCommitStatusForPairs(ctx context.Context, repoSHAs []RepoSHA) (map
|
||||||
|
|
||||||
repoStatuses := make(map[int64][]*CommitStatus)
|
repoStatuses := make(map[int64][]*CommitStatus)
|
||||||
|
|
||||||
if len(results) > 0 {
|
|
||||||
statuses := make([]*CommitStatus, 0, len(results))
|
|
||||||
|
|
||||||
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
|
// Group the statuses by repo ID
|
||||||
for _, status := range statuses {
|
for _, status := range results {
|
||||||
repoStatuses[status.RepoID] = append(repoStatuses[status.RepoID], status)
|
repoStatuses[status.RepoID] = append(repoStatuses[status.RepoID], status)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return repoStatuses, nil
|
return repoStatuses, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
11
models/issues/TestGetUIDsAndStopwatch/stopwatch.yml
Normal file
11
models/issues/TestGetUIDsAndStopwatch/stopwatch.yml
Normal 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
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -75,6 +75,10 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uniqueUserIDs := make(container.Set[int64])
|
||||||
|
uniqueUserIDs.AddMultiple(userIDs...)
|
||||||
|
|
||||||
|
if repo.Owner.IsOrganization() {
|
||||||
additionalUserIDs := make([]int64, 0, 10)
|
additionalUserIDs := make([]int64, 0, 10)
|
||||||
if err = e.Table("team_user").
|
if err = e.Table("team_user").
|
||||||
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
|
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
|
||||||
|
@ -86,15 +90,13 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us
|
||||||
Find(&additionalUserIDs); err != nil {
|
Find(&additionalUserIDs); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
uniqueUserIDs := make(container.Set[int64])
|
|
||||||
uniqueUserIDs.AddMultiple(userIDs...)
|
|
||||||
uniqueUserIDs.AddMultiple(additionalUserIDs...)
|
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()).
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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),
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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", ¬e)
|
err = git.GetNote(context.Background(), bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653", ¬e)
|
||||||
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", ¬e)
|
err = git.GetNote(context.Background(), repo, "3e668dbfac39cbc80a9ff9c61eb565d944453ba4", ¬e)
|
||||||
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", ¬e)
|
err = git.GetNote(context.Background(), repo, "ba0a96fa63532d6c5087ecef070b0250ed72fa47", ¬e)
|
||||||
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", ¬e)
|
err = git.GetNote(context.Background(), bareRepo1, "non_existent_sha", ¬e)
|
||||||
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", ¬e)
|
||||||
|
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", ¬e)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.IsType(t, git.ErrNotExist{}, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
3811
modules/setting/disposable_email_domain_data.go
Normal file
3811
modules/setting/disposable_email_domain_data.go
Normal file
File diff suppressed because it is too large
Load diff
|
@ -5,6 +5,7 @@ package setting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -37,6 +38,7 @@ var Service = struct {
|
||||||
RegisterManualConfirm bool
|
RegisterManualConfirm bool
|
||||||
EmailDomainAllowList []glob.Glob
|
EmailDomainAllowList []glob.Glob
|
||||||
EmailDomainBlockList []glob.Glob
|
EmailDomainBlockList []glob.Glob
|
||||||
|
EmailDomainBlockDisposable bool
|
||||||
DisableRegistration bool
|
DisableRegistration bool
|
||||||
AllowOnlyInternalRegistration bool
|
AllowOnlyInternalRegistration bool
|
||||||
AllowOnlyExternalRegistration bool
|
AllowOnlyExternalRegistration bool
|
||||||
|
@ -156,6 +158,22 @@ func loadServiceFrom(rootCfg ConfigProvider) {
|
||||||
}
|
}
|
||||||
Service.EmailDomainAllowList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_WHITELIST", "EMAIL_DOMAIN_ALLOWLIST")
|
Service.EmailDomainAllowList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_WHITELIST", "EMAIL_DOMAIN_ALLOWLIST")
|
||||||
Service.EmailDomainBlockList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_BLOCKLIST")
|
Service.EmailDomainBlockList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_BLOCKLIST")
|
||||||
|
Service.EmailDomainBlockDisposable = sec.Key("EMAIL_DOMAIN_BLOCK_DISPOSABLE").MustBool(false)
|
||||||
|
if Service.EmailDomainBlockDisposable {
|
||||||
|
toAdd := make([]glob.Glob, 0, len(DisposableEmailDomains()))
|
||||||
|
for _, domain := range DisposableEmailDomains() {
|
||||||
|
domain = strings.ToLower(domain)
|
||||||
|
// Only add domains that aren't blocked yet.
|
||||||
|
if !slices.ContainsFunc(Service.EmailDomainBlockList, func(g glob.Glob) bool { return g.Match(domain) }) {
|
||||||
|
if g, err := glob.Compile(domain); err != nil {
|
||||||
|
log.Error("Error in disposable domain %s: %v", domain, err)
|
||||||
|
} else {
|
||||||
|
toAdd = append(toAdd, g)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Service.EmailDomainBlockList = append(Service.EmailDomainBlockList, toAdd...)
|
||||||
|
}
|
||||||
Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration))
|
Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration))
|
||||||
Service.ShowMilestonesDashboardPage = sec.Key("SHOW_MILESTONES_DASHBOARD_PAGE").MustBool(true)
|
Service.ShowMilestonesDashboardPage = sec.Key("SHOW_MILESTONES_DASHBOARD_PAGE").MustBool(true)
|
||||||
Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool()
|
Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool()
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
package setting
|
package setting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/structs"
|
"code.gitea.io/gitea/modules/structs"
|
||||||
|
@ -11,8 +14,18 @@ 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 {
|
||||||
|
for _, g := range globs {
|
||||||
|
if g.Match(s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoadServices(t *testing.T) {
|
func TestLoadServices(t *testing.T) {
|
||||||
oldService := Service
|
oldService := Service
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -28,15 +41,6 @@ EMAIL_DOMAIN_BLOCKLIST = d3, *.b
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
loadServiceFrom(cfg)
|
loadServiceFrom(cfg)
|
||||||
|
|
||||||
match := func(globs []glob.Glob, s string) bool {
|
|
||||||
for _, g := range globs {
|
|
||||||
if g.Match(s) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.True(t, match(Service.EmailDomainAllowList, "d1"))
|
assert.True(t, match(Service.EmailDomainAllowList, "d1"))
|
||||||
assert.True(t, match(Service.EmailDomainAllowList, "foo.w"))
|
assert.True(t, match(Service.EmailDomainAllowList, "foo.w"))
|
||||||
assert.True(t, match(Service.EmailDomainAllowList, "d2"))
|
assert.True(t, match(Service.EmailDomainAllowList, "d2"))
|
||||||
|
@ -48,6 +52,121 @@ EMAIL_DOMAIN_BLOCKLIST = d3, *.b
|
||||||
assert.False(t, match(Service.EmailDomainBlockList, "d1"))
|
assert.False(t, match(Service.EmailDomainBlockList, "d1"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadServiceBlockDisposable(t *testing.T) {
|
||||||
|
oldService := Service
|
||||||
|
defer func() {
|
||||||
|
Service = oldService
|
||||||
|
}()
|
||||||
|
|
||||||
|
cfg, err := NewConfigProviderFromData(`
|
||||||
|
[service]
|
||||||
|
EMAIL_DOMAIN_BLOCK_DISPOSABLE = true
|
||||||
|
`)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
loadServiceFrom(cfg)
|
||||||
|
|
||||||
|
for _, domain := range DisposableEmailDomains() {
|
||||||
|
require.True(t, match(Service.EmailDomainBlockList, domain))
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Len(t, Service.EmailDomainBlockList, len(DisposableEmailDomains()))
|
||||||
|
|
||||||
|
knownGood := [...]string{
|
||||||
|
"aol.com",
|
||||||
|
"gmx.com",
|
||||||
|
"mail.com",
|
||||||
|
"zoho.com",
|
||||||
|
"proton.me",
|
||||||
|
"gmail.com",
|
||||||
|
"yahoo.com",
|
||||||
|
"icloud.com",
|
||||||
|
"outlook.com",
|
||||||
|
"protonmail.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, domain := range knownGood {
|
||||||
|
require.False(t, match(Service.EmailDomainBlockList, domain))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadServiceBlockDisposableWithExistingGlobs(t *testing.T) {
|
||||||
|
oldService := Service
|
||||||
|
defer func() {
|
||||||
|
Service = oldService
|
||||||
|
}()
|
||||||
|
|
||||||
|
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]
|
||||||
|
EMAIL_DOMAIN_BLOCKLIST = %s
|
||||||
|
EMAIL_DOMAIN_BLOCK_DISPOSABLE = true
|
||||||
|
`, blockString))
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
loadServiceFrom(cfg)
|
||||||
|
|
||||||
|
for _, domain := range DisposableEmailDomains() {
|
||||||
|
require.True(t, match(Service.EmailDomainBlockList, domain))
|
||||||
|
}
|
||||||
|
|
||||||
|
redundant := 0
|
||||||
|
for _, val := range DisposableEmailDomains() {
|
||||||
|
if strings.HasSuffix(val, sortedTldCounts[0].Tld) ||
|
||||||
|
strings.HasSuffix(val, sortedTldCounts[1].Tld) {
|
||||||
|
redundant++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := len(DisposableEmailDomains()) - redundant + 2
|
||||||
|
require.Len(t, Service.EmailDomainBlockList, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadServiceBlockDisposableWithComplementGlobs(t *testing.T) {
|
||||||
|
oldService := Service
|
||||||
|
defer func() {
|
||||||
|
Service = oldService
|
||||||
|
}()
|
||||||
|
|
||||||
|
cfg, err := NewConfigProviderFromData(`
|
||||||
|
[service]
|
||||||
|
EMAIL_DOMAIN_BLOCKLIST = *.random
|
||||||
|
EMAIL_DOMAIN_BLOCK_DISPOSABLE = true
|
||||||
|
`)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
loadServiceFrom(cfg)
|
||||||
|
|
||||||
|
for _, domain := range DisposableEmailDomains() {
|
||||||
|
require.True(t, match(Service.EmailDomainBlockList, domain))
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := len(DisposableEmailDomains()) + 1
|
||||||
|
require.Len(t, Service.EmailDomainBlockList, expected)
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoadServiceVisibilityModes(t *testing.T) {
|
func TestLoadServiceVisibilityModes(t *testing.T) {
|
||||||
oldService := Service
|
oldService := Service
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|
|
@ -48,7 +48,7 @@ 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)"`
|
||||||
|
|
|
@ -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"`
|
||||||
|
}
|
||||||
|
|
|
@ -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
896
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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
214
poetry.lock
generated
|
@ -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"]
|
||||||
|
|
|
@ -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_security; then
|
||||||
|
if $is_bug; then
|
||||||
if $is_breaking; then
|
if $is_breaking; then
|
||||||
if $is_feature; then
|
echo -n AA Breaking security bug fixes
|
||||||
echo -n AA Breaking features
|
|
||||||
elif $is_bug; then
|
|
||||||
echo -n AB Breaking bug fixes
|
|
||||||
else
|
else
|
||||||
echo -n ZC Breaking changes without a feature or bug label
|
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
|
||||||
|
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
8
release-notes/5974.md
Normal 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
1
release-notes/5988.md
Normal 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.
|
|
@ -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() {
|
||||||
|
|
|
@ -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,14 +341,29 @@ 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)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "DeletePrimaryEmailAddressOfUser", err)
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := &user_service.UpdateOptions{
|
opts := &user_service.UpdateOptions{
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -1110,11 +1110,20 @@ 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)
|
||||||
|
baseIsBranch := ctx.Repo.GitRepo.IsBranchExist(baseBranch)
|
||||||
|
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")
|
ctx.NotFound("BaseNotExist")
|
||||||
return nil, nil, nil, "", ""
|
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.
|
||||||
headRepo := repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID)
|
headRepo := repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID)
|
||||||
|
@ -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)
|
||||||
|
headIsBranch := headGitRepo.IsTagExist(headBranch)
|
||||||
|
headIsTag := headGitRepo.IsCommitExist(baseBranch)
|
||||||
|
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()
|
headGitRepo.Close()
|
||||||
ctx.NotFound()
|
ctx.NotFound("IsRefExist", nil)
|
||||||
return nil, nil, 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)
|
||||||
|
|
|
@ -231,4 +231,7 @@ type swaggerParameterBodies struct {
|
||||||
|
|
||||||
// in:body
|
// in:body
|
||||||
SetUserQuotaGroupsOptions api.SetUserQuotaGroupsOptions
|
SetUserQuotaGroupsOptions api.SetUserQuotaGroupsOptions
|
||||||
|
|
||||||
|
// in:body
|
||||||
|
NoteOptions api.NoteOptions
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 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)
|
|
||||||
}
|
}
|
||||||
|
if u == nil {
|
||||||
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,8 +822,22 @@ 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 {
|
||||||
|
ctx.ServerError("VerifyUserAuthorizationToken", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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 {
|
if err := user_model.ActivateEmail(ctx, email); err != nil {
|
||||||
ctx.ServerError("ActivateEmail", err)
|
ctx.ServerError("ActivateEmail", err)
|
||||||
return
|
return
|
||||||
|
@ -845,13 +846,8 @@ func ActivateEmail(ctx *context.Context) {
|
||||||
log.Trace("Email activated: %s", email.Email)
|
log.Trace("Email activated: %s", email.Email)
|
||||||
ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
|
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
|
// Allow user to validate more emails
|
||||||
_ = ctx.Cache.Delete("MailResendLimit_" + u.LowerName)
|
_ = 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.
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
116
services/auth/source/oauth2/jwtsigningkey_test.go
Normal file
116
services/auth/source/oauth2/jwtsigningkey_test.go
Normal 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))
|
||||||
|
})
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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]
|
||||||
}
|
}
|
||||||
|
|
|
@ -309,6 +309,7 @@ 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,
|
||||||
|
IncludeDrafts: true,
|
||||||
IncludeTags: true,
|
IncludeTags: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -394,14 +395,18 @@ 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.Title = parts[0]
|
||||||
|
rel.Note = note
|
||||||
|
if author != nil {
|
||||||
rel.PublisherID = author.ID
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
<div class="ui bottom attached info segment git-notes">
|
<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 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>
|
||||||
|
|
110
templates/swagger/v1_json.tmpl
generated
110
templates/swagger/v1_json.tmpl
generated
|
@ -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": {
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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}}
|
||||||
|
<div class="field">
|
||||||
<label>{{ctx.Locale.Tr "email"}}</label>
|
<label>{{ctx.Locale.Tr "email"}}</label>
|
||||||
<p id="signed-user-email">{{.SignedUser.Email}}</p>
|
<p id="signed-user-email">{{.SignedUser.Email}}</p>
|
||||||
</div>
|
</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>
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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}/`);
|
||||||
|
|
30
tests/e2e/git-notes.test.e2e.ts
Normal file
30
tests/e2e/git-notes.test.e2e.ts
Normal 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');
|
||||||
|
});
|
|
@ -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();
|
||||||
|
|
|
@ -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`);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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'))),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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?!
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue