mirror of
https://codeberg.org/forgejo/forgejo
synced 2024-11-24 18:56:11 +01:00
Compare commits
32 commits
109d09de1a
...
e71a5f77c1
Author | SHA1 | Date | |
---|---|---|---|
e71a5f77c1 | |||
a5caf6a1c1 | |||
1316f4d338 | |||
f4c70a3c43 | |||
3674e90c93 | |||
0a39ee3bbe | |||
8636a8b228 | |||
f5c0570533 | |||
b7c15f7b70 | |||
298863c701 | |||
f90928507a | |||
71ff98d61d | |||
1f4be5baad | |||
cff7754735 | |||
0fb3e45ca5 | |||
1806db31d1 | |||
7c1f3a7594 | |||
3c3c9b22a9 | |||
73cb6c9204 | |||
25354c03a5 | |||
18cecf124f | |||
2f1b1d8b80 | |||
c3653e0eaa | |||
4a3f8f3004 | |||
6cfaebf043 | |||
b9697f5227 | |||
02f4d3bd2d | |||
b6869d643e | |||
7f707b2a6f | |||
c226b4d00a | |||
f9169eac96 | |||
f8cbd6301e |
|
@ -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,53 +0,0 @@
|
||||||
# Copyright 2024 The Forgejo Authors
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
#
|
|
||||||
name: issue-labels
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types:
|
|
||||||
- labeled
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
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
|
|
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,52 +0,0 @@
|
||||||
# Copyright 2024 The Forgejo Authors
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
name: issue-labels
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types:
|
|
||||||
- labeled
|
|
||||||
- edited
|
|
||||||
- opened
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
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
|
|
|
@ -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 }}
|
|
|
@ -216,11 +216,13 @@ jobs:
|
||||||
ldap:
|
ldap:
|
||||||
image: code.forgejo.org/oci/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
|
||||||
|
|
||||||
|
|
|
@ -7,66 +7,17 @@ package user
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/mail"
|
|
||||||
"net/url"
|
|
||||||
"regexp"
|
|
||||||
"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"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/validation"
|
|
||||||
|
|
||||||
"github.com/gobwas/glob"
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrEmailNotActivated e-mail address has not been activated error
|
|
||||||
var ErrEmailNotActivated = util.NewInvalidArgumentErrorf("e-mail address has not been activated")
|
|
||||||
|
|
||||||
// ErrEmailCharIsNotSupported e-mail address contains unsupported character
|
|
||||||
type ErrEmailCharIsNotSupported struct {
|
|
||||||
Email string
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsErrEmailCharIsNotSupported checks if an error is an ErrEmailCharIsNotSupported
|
|
||||||
func IsErrEmailCharIsNotSupported(err error) bool {
|
|
||||||
_, ok := err.(ErrEmailCharIsNotSupported)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err ErrEmailCharIsNotSupported) Error() string {
|
|
||||||
return fmt.Sprintf("e-mail address contains unsupported character [email: %s]", err.Email)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err ErrEmailCharIsNotSupported) Unwrap() error {
|
|
||||||
return util.ErrInvalidArgument
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrEmailInvalid represents an error where the email address does not comply with RFC 5322
|
|
||||||
// or has a leading '-' character
|
|
||||||
type ErrEmailInvalid struct {
|
|
||||||
Email string
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsErrEmailInvalid checks if an error is an ErrEmailInvalid
|
|
||||||
func IsErrEmailInvalid(err error) bool {
|
|
||||||
_, ok := err.(ErrEmailInvalid)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err ErrEmailInvalid) Error() string {
|
|
||||||
return fmt.Sprintf("e-mail invalid [email: %s]", err.Email)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err ErrEmailInvalid) Unwrap() error {
|
|
||||||
return util.ErrInvalidArgument
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrEmailAlreadyUsed represents a "EmailAlreadyUsed" kind of error.
|
// ErrEmailAlreadyUsed represents a "EmailAlreadyUsed" kind of error.
|
||||||
type ErrEmailAlreadyUsed struct {
|
type ErrEmailAlreadyUsed struct {
|
||||||
Email string
|
Email string
|
||||||
|
@ -158,22 +109,6 @@ func UpdateEmailAddress(ctx context.Context, email *EmailAddress) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
|
|
||||||
|
|
||||||
// ValidateEmail check if email is a valid & allowed address
|
|
||||||
func ValidateEmail(email string) error {
|
|
||||||
if err := validateEmailBasic(email); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return validateEmailDomain(email)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateEmailForAdmin check if email is a valid address when admins manually add or edit users
|
|
||||||
func ValidateEmailForAdmin(email string) error {
|
|
||||||
return validateEmailBasic(email)
|
|
||||||
// In this case we do not need to check the email domain
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetEmailAddressByEmail(ctx context.Context, email string) (*EmailAddress, error) {
|
func GetEmailAddressByEmail(ctx context.Context, email string) (*EmailAddress, error) {
|
||||||
ea := &EmailAddress{}
|
ea := &EmailAddress{}
|
||||||
if has, err := db.GetEngine(ctx).Where("lower_email=?", strings.ToLower(email)).Get(ea); err != nil {
|
if has, err := db.GetEngine(ctx).Where("lower_email=?", strings.ToLower(email)).Get(ea); err != nil {
|
||||||
|
@ -309,23 +244,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
|
||||||
|
|
||||||
|
@ -464,70 +382,3 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate
|
||||||
|
|
||||||
return committer.Commit()
|
return committer.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateEmailBasic checks whether the email complies with the rules
|
|
||||||
func validateEmailBasic(email string) error {
|
|
||||||
if len(email) == 0 {
|
|
||||||
return ErrEmailInvalid{email}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !emailRegexp.MatchString(email) {
|
|
||||||
return ErrEmailCharIsNotSupported{email}
|
|
||||||
}
|
|
||||||
|
|
||||||
if email[0] == '-' {
|
|
||||||
return ErrEmailInvalid{email}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := mail.ParseAddress(email); err != nil {
|
|
||||||
return ErrEmailInvalid{email}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateEmailDomain checks whether the email domain is allowed or blocked
|
|
||||||
func validateEmailDomain(email string) error {
|
|
||||||
if !IsEmailDomainAllowed(email) {
|
|
||||||
return ErrEmailInvalid{email}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsEmailDomainAllowed(email string) bool {
|
|
||||||
return IsEmailDomainAllowedInternal(
|
|
||||||
email,
|
|
||||||
setting.Service.EmailDomainAllowList,
|
|
||||||
setting.Service.EmailDomainBlockList,
|
|
||||||
setting.Federation.Enabled,
|
|
||||||
setting.AppURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsEmailDomainAllowedInternal(
|
|
||||||
email string,
|
|
||||||
emailDomainAllowList []glob.Glob,
|
|
||||||
emailDomainBlockList []glob.Glob,
|
|
||||||
isFederation bool,
|
|
||||||
fqdn string,
|
|
||||||
) bool {
|
|
||||||
var result bool
|
|
||||||
|
|
||||||
if len(emailDomainAllowList) == 0 {
|
|
||||||
result = !validation.IsEmailDomainListed(emailDomainBlockList, email)
|
|
||||||
} else if isFederation {
|
|
||||||
localFqdn, err := url.ParseRequestURI(fqdn)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
globber, err := glob.Compile(localFqdn.Hostname(), ',')
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
emailDomainAllowList = append(emailDomainAllowList, globber)
|
|
||||||
result = validation.IsEmailDomainListed(emailDomainAllowList, email)
|
|
||||||
} else {
|
|
||||||
result = validation.IsEmailDomainListed(emailDomainAllowList, email)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
|
@ -12,54 +12,10 @@ 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"
|
||||||
|
|
||||||
"github.com/gobwas/glob"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEmailDomainAllowList(t *testing.T) {
|
|
||||||
res := user_model.IsEmailDomainAllowed("someuser@localhost.localdomain")
|
|
||||||
assert.True(t, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEmailDomainAllowListInternal(t *testing.T) {
|
|
||||||
domain, _ := glob.Compile("domain.de", ',')
|
|
||||||
emailDomainAllowList := []glob.Glob{domain}
|
|
||||||
emailDomainBlockList := []glob.Glob{}
|
|
||||||
|
|
||||||
res := user_model.IsEmailDomainAllowedInternal(
|
|
||||||
"user@repo.domain.de",
|
|
||||||
emailDomainAllowList,
|
|
||||||
emailDomainBlockList,
|
|
||||||
false,
|
|
||||||
"https://repo.domain.de")
|
|
||||||
assert.False(t, res)
|
|
||||||
|
|
||||||
res = user_model.IsEmailDomainAllowedInternal(
|
|
||||||
"user@repo.domain.de",
|
|
||||||
emailDomainAllowList,
|
|
||||||
emailDomainBlockList,
|
|
||||||
true,
|
|
||||||
"xttps://repo")
|
|
||||||
assert.False(t, res)
|
|
||||||
|
|
||||||
res = user_model.IsEmailDomainAllowedInternal(
|
|
||||||
"user@repo.Domain.de",
|
|
||||||
emailDomainAllowList,
|
|
||||||
emailDomainBlockList,
|
|
||||||
true,
|
|
||||||
"https://repo.domain.de")
|
|
||||||
assert.True(t, res)
|
|
||||||
|
|
||||||
res = user_model.IsEmailDomainAllowedInternal(
|
|
||||||
"user@repo.domain.de",
|
|
||||||
emailDomainAllowList,
|
|
||||||
emailDomainBlockList,
|
|
||||||
true,
|
|
||||||
"https://repo.domain.de")
|
|
||||||
assert.True(t, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetEmailAddresses(t *testing.T) {
|
func TestGetEmailAddresses(t *testing.T) {
|
||||||
require.NoError(t, unittest.PrepareTestDatabase())
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
@ -173,64 +129,6 @@ func TestListEmails(t *testing.T) {
|
||||||
assert.Len(t, emails, 5)
|
assert.Len(t, emails, 5)
|
||||||
assert.Greater(t, count, int64(len(emails)))
|
assert.Greater(t, count, int64(len(emails)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEmailAddressValidate(t *testing.T) {
|
|
||||||
kases := map[string]error{
|
|
||||||
"abc@gmail.com": nil,
|
|
||||||
"132@hotmail.com": nil,
|
|
||||||
"1-3-2@test.org": nil,
|
|
||||||
"1.3.2@test.org": nil,
|
|
||||||
"a_123@test.org.cn": nil,
|
|
||||||
`first.last@iana.org`: nil,
|
|
||||||
`first!last@iana.org`: nil,
|
|
||||||
`first#last@iana.org`: nil,
|
|
||||||
`first$last@iana.org`: nil,
|
|
||||||
`first%last@iana.org`: nil,
|
|
||||||
`first&last@iana.org`: nil,
|
|
||||||
`first'last@iana.org`: nil,
|
|
||||||
`first*last@iana.org`: nil,
|
|
||||||
`first+last@iana.org`: nil,
|
|
||||||
`first/last@iana.org`: nil,
|
|
||||||
`first=last@iana.org`: nil,
|
|
||||||
`first?last@iana.org`: nil,
|
|
||||||
`first^last@iana.org`: nil,
|
|
||||||
"first`last@iana.org": nil,
|
|
||||||
`first{last@iana.org`: nil,
|
|
||||||
`first|last@iana.org`: nil,
|
|
||||||
`first}last@iana.org`: nil,
|
|
||||||
`first~last@iana.org`: nil,
|
|
||||||
`first;last@iana.org`: user_model.ErrEmailCharIsNotSupported{`first;last@iana.org`},
|
|
||||||
".233@qq.com": user_model.ErrEmailInvalid{".233@qq.com"},
|
|
||||||
"!233@qq.com": nil,
|
|
||||||
"#233@qq.com": nil,
|
|
||||||
"$233@qq.com": nil,
|
|
||||||
"%233@qq.com": nil,
|
|
||||||
"&233@qq.com": nil,
|
|
||||||
"'233@qq.com": nil,
|
|
||||||
"*233@qq.com": nil,
|
|
||||||
"+233@qq.com": nil,
|
|
||||||
"-233@qq.com": user_model.ErrEmailInvalid{"-233@qq.com"},
|
|
||||||
"/233@qq.com": nil,
|
|
||||||
"=233@qq.com": nil,
|
|
||||||
"?233@qq.com": nil,
|
|
||||||
"^233@qq.com": nil,
|
|
||||||
"_233@qq.com": nil,
|
|
||||||
"`233@qq.com": nil,
|
|
||||||
"{233@qq.com": nil,
|
|
||||||
"|233@qq.com": nil,
|
|
||||||
"}233@qq.com": nil,
|
|
||||||
"~233@qq.com": nil,
|
|
||||||
";233@qq.com": user_model.ErrEmailCharIsNotSupported{";233@qq.com"},
|
|
||||||
"Foo <foo@bar.com>": user_model.ErrEmailCharIsNotSupported{"Foo <foo@bar.com>"},
|
|
||||||
string([]byte{0xE2, 0x84, 0xAA}): user_model.ErrEmailCharIsNotSupported{string([]byte{0xE2, 0x84, 0xAA})},
|
|
||||||
}
|
|
||||||
for kase, err := range kases {
|
|
||||||
t.Run(kase, func(t *testing.T) {
|
|
||||||
assert.EqualValues(t, err, user_model.ValidateEmail(kase))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetActivatedEmailAddresses(t *testing.T) {
|
func TestGetActivatedEmailAddresses(t *testing.T) {
|
||||||
require.NoError(t, unittest.PrepareTestDatabase())
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
|
|
@ -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,10 @@ 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/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -321,7 +325,7 @@ func TestCreateUserInvalidEmail(t *testing.T) {
|
||||||
err := user_model.CreateUser(db.DefaultContext, user)
|
err := user_model.CreateUser(db.DefaultContext, user)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
assert.True(t, user_model.IsErrEmailCharIsNotSupported(err))
|
assert.True(t, validation.IsErrEmailCharIsNotSupported(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateUserEmailAlreadyUsed(t *testing.T) {
|
func TestCreateUserEmailAlreadyUsed(t *testing.T) {
|
||||||
|
@ -603,6 +607,69 @@ func Test_ValidateUser(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func Test_NormalizeUserFromEmail(t *testing.T) {
|
func Test_NormalizeUserFromEmail(t *testing.T) {
|
||||||
oldSetting := setting.Service.AllowDotsInUsernames
|
oldSetting := setting.Service.AllowDotsInUsernames
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
15
package-lock.json
generated
15
package-lock.json
generated
|
@ -18,7 +18,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",
|
||||||
|
@ -4807,6 +4807,12 @@
|
||||||
"integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==",
|
"integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/hammerjs": {
|
||||||
|
"version": "2.0.46",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz",
|
||||||
|
"integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
|
@ -6479,11 +6485,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chartjs-plugin-zoom": {
|
"node_modules/chartjs-plugin-zoom": {
|
||||||
"version": "2.0.1",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.1.0.tgz",
|
||||||
"integrity": "sha512-ogOmLu6e+Q7E1XWOCOz9YwybMslz9qNfGV2a+qjfmqJYpsw5ZMoRHZBUyW+NGhkpQ5PwwPA/+rikHpBZb7PZuA==",
|
"integrity": "sha512-7lMimfQCUaIJLhPJaWSAA4gw+1m8lyR3Wn+M3MxjHbM/XxRUnOxN7cM5RR9jUmxmyW0h7L2hZ8KhvUsqrFxy/Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/hammerjs": "^2.0.45",
|
||||||
"hammerjs": "^2.0.8"
|
"hammerjs": "^2.0.8"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -231,4 +231,7 @@ type swaggerParameterBodies struct {
|
||||||
|
|
||||||
// in:body
|
// in:body
|
||||||
SetUserQuotaGroupsOptions api.SetUserQuotaGroupsOptions
|
SetUserQuotaGroupsOptions api.SetUserQuotaGroupsOptions
|
||||||
|
|
||||||
|
// in:body
|
||||||
|
NoteOptions api.NoteOptions
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
|
@ -1559,6 +1559,10 @@ 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)
|
||||||
|
|
||||||
|
|
|
@ -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,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>
|
||||||
|
|
107
templates/swagger/v1_json.tmpl
generated
107
templates/swagger/v1_json.tmpl
generated
|
@ -7375,6 +7375,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 +24696,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 +28455,7 @@
|
||||||
"parameterBodies": {
|
"parameterBodies": {
|
||||||
"description": "parameterBodies",
|
"description": "parameterBodies",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/SetUserQuotaGroupsOptions"
|
"$ref": "#/definitions/NoteOptions"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"quotaExceeded": {
|
"quotaExceeded": {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
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,11 +4,13 @@
|
||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
@ -16,7 +18,7 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAPIReposGitNotes(t *testing.T) {
|
func TestAPIReposGetGitNotes(t *testing.T) {
|
||||||
onGiteaRun(t, func(*testing.T, *url.URL) {
|
onGiteaRun(t, func(*testing.T, *url.URL) {
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
// Login as User2.
|
// Login as User2.
|
||||||
|
@ -44,3 +46,53 @@ func TestAPIReposGitNotes(t *testing.T) {
|
||||||
assert.NotNil(t, apiData.Commit.RepoCommit.Verification)
|
assert.NotNil(t, apiData.Commit.RepoCommit.Verification)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAPIReposSetGitNotes(t *testing.T) {
|
||||||
|
onGiteaRun(t, func(*testing.T, *url.URL) {
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
|
||||||
|
session := loginUser(t, user.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
|
||||||
|
req := NewRequestf(t, "GET", "/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName())
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
var apiData api.Note
|
||||||
|
DecodeJSON(t, resp, &apiData)
|
||||||
|
assert.Equal(t, "This is a test note\n", apiData.Message)
|
||||||
|
|
||||||
|
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName()), &api.NoteOptions{
|
||||||
|
Message: "This is a new note",
|
||||||
|
}).AddTokenAuth(token)
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
DecodeJSON(t, resp, &apiData)
|
||||||
|
assert.Equal(t, "This is a new note\n", apiData.Message)
|
||||||
|
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName())
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
DecodeJSON(t, resp, &apiData)
|
||||||
|
assert.Equal(t, "This is a new note\n", apiData.Message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIReposDeleteGitNotes(t *testing.T) {
|
||||||
|
onGiteaRun(t, func(*testing.T, *url.URL) {
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
|
||||||
|
session := loginUser(t, user.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
|
||||||
|
req := NewRequestf(t, "GET", "/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName())
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
var apiData api.Note
|
||||||
|
DecodeJSON(t, resp, &apiData)
|
||||||
|
assert.Equal(t, "This is a test note\n", apiData.Message)
|
||||||
|
|
||||||
|
req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName()).AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName())
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
72
tests/integration/repo_git_note_test.go
Normal file
72
tests/integration/repo_git_note_test.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRepoModifyGitNotes(t *testing.T) {
|
||||||
|
onGiteaRun(t, func(*testing.T, *url.URL) {
|
||||||
|
session := loginUser(t, "user2")
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d")
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
assert.Contains(t, resp.Body.String(), "<pre class=\"commit-body\">This is a test note\n</pre>")
|
||||||
|
assert.Contains(t, resp.Body.String(), "commit-notes-display-area")
|
||||||
|
|
||||||
|
t.Run("Set", func(t *testing.T) {
|
||||||
|
req = NewRequestWithValues(t, "POST", "/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d/notes", map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, "/user2/repo1"),
|
||||||
|
"notes": "This is a new note",
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||||
|
|
||||||
|
req = NewRequest(t, "GET", "/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d")
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
assert.Contains(t, resp.Body.String(), "<pre class=\"commit-body\">This is a new note\n</pre>")
|
||||||
|
assert.Contains(t, resp.Body.String(), "commit-notes-display-area")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Delete", func(t *testing.T) {
|
||||||
|
req = NewRequestWithValues(t, "POST", "/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d/notes/remove", map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, "/user2/repo1"),
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||||
|
|
||||||
|
req = NewRequest(t, "GET", "/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d")
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
assert.NotContains(t, resp.Body.String(), "commit-notes-display-area")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepoGitNotesButtonsVisible(t *testing.T) {
|
||||||
|
onGiteaRun(t, func(*testing.T, *url.URL) {
|
||||||
|
t.Run("With Permission", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
session := loginUser(t, "user2")
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d")
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
assert.Contains(t, resp.Body.String(), "id=\"commit-notes-edit-button\"")
|
||||||
|
assert.Contains(t, resp.Body.String(), "data-modal=\"#delete-note-modal\"")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Without Permission", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d")
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
assert.NotContains(t, resp.Body.String(), "id=\"commit-notes-edit-button\"")
|
||||||
|
assert.NotContains(t, resp.Body.String(), "data-modal=\"#delete-note-modal\"")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package integration
|
package integration
|
||||||
|
@ -156,3 +157,23 @@ func TestSettingSecurityAuthSource(t *testing.T) {
|
||||||
assert.Contains(t, resp.Body.String(), `gitlab-active`)
|
assert.Contains(t, resp.Body.String(), `gitlab-active`)
|
||||||
assert.Contains(t, resp.Body.String(), `gitlab-inactive`)
|
assert.Contains(t, resp.Body.String(), `gitlab-inactive`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSettingShowUserEmailSettings(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
// user1: keep_email_private = false, user2: keep_email_private = true
|
||||||
|
|
||||||
|
// user1 can see own visible email
|
||||||
|
session := loginUser(t, "user1")
|
||||||
|
req := NewRequest(t, "GET", "/user/settings")
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
assert.Contains(t, htmlDoc.doc.Find("#signed-user-email").Text(), "user1@example.com")
|
||||||
|
|
||||||
|
// user2 cannot see own hidden email
|
||||||
|
session = loginUser(t, "user2")
|
||||||
|
req = NewRequest(t, "GET", "/user/settings")
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
htmlDoc = NewHTMLParser(t, resp.Body)
|
||||||
|
assert.NotContains(t, htmlDoc.doc.Find("#signed-user-email").Text(), "user2@example.com")
|
||||||
|
}
|
||||||
|
|
|
@ -25,3 +25,20 @@ export function initCommitStatuses() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function initCommitNotes() {
|
||||||
|
const notesEditButton = document.getElementById('commit-notes-edit-button');
|
||||||
|
if (notesEditButton !== null) {
|
||||||
|
notesEditButton.addEventListener('click', () => {
|
||||||
|
document.getElementById('commit-notes-display-area').classList.add('tw-hidden');
|
||||||
|
document.getElementById('commit-notes-edit-area').classList.remove('tw-hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const notesAddButton = document.getElementById('commit-notes-add-button');
|
||||||
|
if (notesAddButton !== null) {
|
||||||
|
notesAddButton.addEventListener('click', () => {
|
||||||
|
document.getElementById('commit-notes-add-area').classList.remove('tw-hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ import {
|
||||||
initRepoPullRequestAllowMaintainerEdit,
|
initRepoPullRequestAllowMaintainerEdit,
|
||||||
initRepoPullRequestReview, initRepoIssueSidebarList, initArchivedLabelHandler,
|
initRepoPullRequestReview, initRepoIssueSidebarList, initArchivedLabelHandler,
|
||||||
} from './features/repo-issue.js';
|
} from './features/repo-issue.js';
|
||||||
import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.js';
|
import {initRepoEllipsisButton, initCommitStatuses, initCommitNotes} from './features/repo-commit.js';
|
||||||
import {
|
import {
|
||||||
initFootLanguageMenu,
|
initFootLanguageMenu,
|
||||||
initGlobalButtonClickOnEnter,
|
initGlobalButtonClickOnEnter,
|
||||||
|
@ -179,6 +179,7 @@ onDomReady(() => {
|
||||||
initRepoMilestoneEditor();
|
initRepoMilestoneEditor();
|
||||||
|
|
||||||
initCommitStatuses();
|
initCommitStatuses();
|
||||||
|
initCommitNotes();
|
||||||
initCaptcha();
|
initCaptcha();
|
||||||
|
|
||||||
initUserAuthOauth2();
|
initUserAuthOauth2();
|
||||||
|
|
Loading…
Reference in a new issue