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

Compare commits

...

22 commits

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

<details>
<summary>Screenshots</summary>

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

</details>

## Checklist

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

### Tests

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

### Documentation

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

### Release notes

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

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

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

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/4753
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: JakobDev <jakobdev@gmx.de>
Co-committed-by: JakobDev <jakobdev@gmx.de>
2024-11-18 22:56:17 +00:00
Earl Warren 71ff98d61d Merge pull request 'Update help links on page with no workflows' (#5697) from kwonunn/fix-empty-actions-page into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5697
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
2024-11-18 21:50:31 +00:00
Marco De Araujo 1f4be5baad Escaping specific markdown in commit messages on Discord-type embeds #3664 (#5811)
Co-authored-by: Marco De Araujo <marco.araujo.junior@gmail.com>
Co-committed-by: Marco De Araujo <marco.araujo.junior@gmail.com>
2024-11-18 21:47:11 +00:00
Earl Warren cff7754735 Merge pull request 'ci: disable postgresql fsync' (#5962) from viceice/ci/pg/fsync into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5962
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
2024-11-18 21:43:15 +00:00
Earl Warren 0fb3e45ca5 Merge pull request 'chore(ci): merge jobs issue label jobs in one workflow' (#6021) from earl-warren/forgejo:wip-ci-labels into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6021
Reviewed-by: Michael Kriese <michael.kriese@gmx.de>
2024-11-18 20:33:38 +00:00
Earl Warren 1806db31d1
chore(ci): merge jobs in issue-labels.yml in one workflow
Fixes: forgejo/forgejo#5999
2024-11-18 19:43:35 +01:00
Earl Warren 7c1f3a7594
chore(ci): cat all issue labels workflows in issue-labels.yml
cat cascade-setup-end-to-end.yml backport.yml merge-requirements.yml release-notes-assistant.yml > issue-labels.yml
rm cascade-setup-end-to-end.yml backport.yml merge-requirements.yml release-notes-assistant.yml
2024-11-18 19:40:15 +01:00
0ko c3653e0eaa ui: don't display email in profile settings when hidden 2024-11-18 17:06:38 +05:00
Earl Warren 02f4d3bd2d
chore(release-notes-assistant): security fix / features come first 2024-11-17 20:03:11 +01:00
Michael Kriese 7f707b2a6f
ci: disable postgresql fsync 2024-11-15 15:29:06 +01:00
Kwonunn f9169eac96
re-add the string for read-only viewers 2024-11-14 09:47:49 +01:00
Kwonunn f8cbd6301e
reword the no-workflows page 2024-11-14 09:47:48 +01:00
33 changed files with 1074 additions and 261 deletions

View file

@ -1,44 +0,0 @@
# Copyright 2024 The Forgejo Authors
# SPDX-License-Identifier: MIT
#
name: issue-labels
on:
pull_request_target:
types:
- closed
- labeled
jobs:
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

View file

@ -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

View file

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

View file

@ -1,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

View file

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

View file

@ -216,11 +216,13 @@ jobs:
ldap:
image: code.forgejo.org/oci/test-openldap:latest
pgsql:
image: 'code.forgejo.org/oci/postgres:15'
image: code.forgejo.org/oci/bitnami/postgresql:15
env:
POSTGRES_DB: test
POSTGRES_PASSWORD: postgres
options: --tmpfs /var/lib/postgresql/data
POSTGRESQL_DATABASE: test
POSTGRESQL_PASSWORD: postgres
POSTGRESQL_FSYNC: off
POSTGRESQL_EXTRA_FLAGS: -c full_page_writes=off
options: --tmpfs /bitnami/postgresql
steps:
- uses: https://code.forgejo.org/actions/checkout@v4
- uses: ./.forgejo/workflows-composite/setup-env

View file

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

View file

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

View file

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

View file

@ -2622,6 +2622,9 @@ diff.browse_source = Browse source
diff.parent = parent
diff.commit = commit
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.options_button = Diff options
diff.show_diff_stats = Show stats
@ -3837,8 +3840,8 @@ runs.actors_no_select = All actors
runs.status_no_select = All status
runs.no_results = No results matched.
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.documentation = For more information on Forgejo Actions, see <a target="_blank" rel="noopener noreferrer" href="%s">the documentation</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.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.empty_commit_message = (empty commit message)
runs.expire_log_message = Logs have been purged because they were too old.

15
package-lock.json generated
View file

@ -18,7 +18,7 @@
"asciinema-player": "3.8.0",
"chart.js": "4.4.5",
"chartjs-adapter-dayjs-4": "1.0.4",
"chartjs-plugin-zoom": "2.0.1",
"chartjs-plugin-zoom": "2.1.0",
"clippie": "4.1.1",
"css-loader": "7.0.0",
"dayjs": "1.11.12",
@ -4807,6 +4807,12 @@
"integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==",
"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": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@ -6479,11 +6485,12 @@
}
},
"node_modules/chartjs-plugin-zoom": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.0.1.tgz",
"integrity": "sha512-ogOmLu6e+Q7E1XWOCOz9YwybMslz9qNfGV2a+qjfmqJYpsw5ZMoRHZBUyW+NGhkpQ5PwwPA/+rikHpBZb7PZuA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.1.0.tgz",
"integrity": "sha512-7lMimfQCUaIJLhPJaWSAA4gw+1m8lyR3Wn+M3MxjHbM/XxRUnOxN7cM5RR9jUmxmyW0h7L2hZ8KhvUsqrFxy/Q==",
"license": "MIT",
"dependencies": {
"@types/hammerjs": "^2.0.45",
"hammerjs": "^2.0.8"
},
"peerDependencies": {

View file

@ -17,7 +17,7 @@
"asciinema-player": "3.8.0",
"chart.js": "4.4.5",
"chartjs-adapter-dayjs-4": "1.0.4",
"chartjs-plugin-zoom": "2.0.1",
"chartjs-plugin-zoom": "2.1.0",
"clippie": "4.1.1",
"css-loader": "7.0.0",
"dayjs": "1.11.12",

View file

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

View file

@ -1316,7 +1316,11 @@ func Routes() *web.Route {
m.Get("/trees/{sha}", repo.GetTree)
m.Get("/blobs/{sha}", repo.GetBlob)
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))
m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.ApplyDiffPatch)
m.Group("/contents", func() {

View file

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

View file

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

View file

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

View file

@ -1560,6 +1560,10 @@ func registerRoutes(m *web.Route) {
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})$}/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)
}, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)

View file

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

View file

@ -10,6 +10,7 @@ import (
"html/template"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"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
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
if utf8.RuneCountInString(message) > 50 {
message = fmt.Sprintf("%.47s...", message)
@ -365,3 +369,40 @@ func (d discordConvertor) createPayload(s *api.User, title, text, url string, co
},
}
}
var orderedListPattern = regexp.MustCompile(`(\d+)\.`)
var markdownPatterns = map[string]*regexp.Regexp{
"~": regexp.MustCompile(`\~(.*?)\~`),
"*": regexp.MustCompile(`\*(.*?)\*`),
"_": regexp.MustCompile(`\_(.*?)\_`),
}
var markdownToEscape = strings.NewReplacer(
"* ", "\\* ",
"`", "\\`",
"[", "\\[",
"]", "\\]",
"(", "\\(",
")", "\\)",
"#", "\\#",
"+ ", "\\+ ",
"- ", "\\- ",
"---", "\\---",
"!", "\\!",
"|", "\\|",
"<", "\\<",
">", "\\>",
)
// Escape Markdown characters
func escapeMarkdown(input string) string {
// Escaping ordered list
output := orderedListPattern.ReplaceAllString(input, "$1\\.")
for char, pattern := range markdownPatterns {
output = pattern.ReplaceAllString(output, fmt.Sprintf(`\%s$1\%s`, char, char))
}
return markdownToEscape.Replace(output)
}

View file

@ -94,6 +94,20 @@ func TestDiscordPayload(t *testing.T) {
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
})
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) {
p := issueTestPayload()
@ -346,3 +360,89 @@ func TestDiscordJSONPayload(t *testing.T) {
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)
}
var escapedMarkdownTests = map[string]struct {
input string
expected string
}{
"Escape heading level 1": {
input: "# Heading level 1",
expected: "\\# Heading level 1",
},
"Escape heading level 2": {
input: "## Heading level 2",
expected: "\\#\\# Heading level 2",
},
"Escape heading level 3": {
input: "### Heading level 3",
expected: "\\#\\#\\# Heading level 3",
},
"Escape bold text": {
input: "**bold text**",
expected: "\\*\\*bold text\\*\\*",
},
"Escape italic text": {
input: "*italic text*",
expected: "\\*italic text\\*",
},
"Escape italic text underline": {
input: "_italic text_",
expected: "\\_italic text\\_",
},
"Escape strikethrough": {
input: "~~strikethrough~~",
expected: "\\~\\~strikethrough\\~\\~",
},
"Escape Ordered list item": {
input: "1. Ordered list item\n2. Second ordered list item\n999999999999. 999999999999 ordered list item",
expected: "1\\. Ordered list item\n2\\. Second ordered list item\n999999999999\\. 999999999999 ordered list item",
},
"Escape Unordered list item": {
input: "- Unordered list\n + using plus",
expected: "\\- Unordered list\n \\+ using plus",
},
"Escape bullet list item": {
input: "* Bullet list item",
expected: "\\* Bullet list item",
},
"Escape table": {
input: "| Table | Example |\n|-|-|\n| Lorem | Ipsum |",
expected: "\\| Table \\| Example \\|\n\\|-\\|-\\|\n\\| Lorem \\| Ipsum \\|",
},
"Escape link": {
input: "[Link to Forgejo](https://forgejo.org/)",
expected: "\\[Link to Forgejo\\]\\(https://forgejo.org/\\)",
},
"Escape Alt text for an image": {
input: "![Alt text for an image](https://forgejo.org/_astro/mascot-dark.1omhhgvT_Zm0N2n.webp)",
expected: "\\!\\[Alt text for an image\\]\\(https://forgejo.org/\\_astro/mascot-dark.1omhhgvT\\_Zm0N2n.webp\\)",
},
"Escape URL if it has markdown character": {
input: "https://forgejo.org/_astro/mascot-dark.1omhhgvT_Zm0N2n.webp",
expected: "https://forgejo.org/\\_astro/mascot-dark.1omhhgvT\\_Zm0N2n.webp",
},
"Escape blockquote text": {
input: "> Blockquote text.",
expected: "\\> Blockquote text.",
},
"Escape inline code": {
input: "`Inline code`",
expected: "\\`Inline code\\`",
},
"Escape multiple code": {
input: "```\nCode block\nwith multiple lines\n```\n",
expected: "\\`\\`\\`\nCode block\nwith multiple lines\n\\`\\`\\`\n",
},
"Escape horizontal rule": {
input: "---",
expected: "\\---",
},
}
func TestEscapeMarkdownChar(t *testing.T) {
for name, test := range escapedMarkdownTests {
t.Run(name, func(t *testing.T) {
assert.Equal(t, test.expected, escapeMarkdown(test.input))
})
}
}

View file

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

View file

@ -2,7 +2,8 @@
{{svg "octicon-no-entry" 48}}
<h2>{{ctx.Locale.Tr "actions.runs.no_workflows"}}</h2>
{{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}}
<p>{{ctx.Locale.Tr "actions.runs.no_workflows.documentation" "https://forgejo.org/docs/latest/admin/actions/"}}</p>
</div>

View file

@ -128,6 +128,9 @@
</form>
</div>
</div>
<div id="commit-notes-add-button" class="item">
{{ctx.Locale.Tr "repo.diff.git-notes.add"}}
</div>
</div>
</div>
{{end}}
@ -275,10 +278,60 @@
<strong>{{.NoteCommit.Author.Name}}</strong>
{{end}}
<span class="text grey" id="note-authored-time">{{DateUtils.TimeSince .NoteCommit.Author.When}}</span>
{{if and ($.Permission.CanWrite $.UnitTypeCode) (not $.Repository.IsArchived) (not .IsDeleted)}}
<div class="ui right">
<button id="commit-notes-edit-button" class="ui tiny primary button">{{ctx.Locale.Tr "edit"}}</button>
<button class="ui tiny button red show-modal" data-modal="#delete-note-modal">{{ctx.Locale.Tr "remove"}}</button>
</div>
<div class="ui 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>
</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}}
{{template "repo/diff/box" .}}
</div>

View file

@ -7375,6 +7375,101 @@
"$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": {
@ -24601,6 +24696,16 @@
},
"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": {
"description": "NotificationCount number of unread notifications",
"type": "object",
@ -28350,7 +28455,7 @@
"parameterBodies": {
"description": "parameterBodies",
"schema": {
"$ref": "#/definitions/SetUserQuotaGroupsOptions"
"$ref": "#/definitions/NoteOptions"
}
},
"quotaExceeded": {

View file

@ -51,10 +51,12 @@
</div>
<input id="pronouns-custom" name="pronouns" value="{{.SignedUser.Pronouns}}" maxlength="50">
</div>
<div class="field {{if .Err_Email}}error{{end}}">
{{if not .SignedUser.KeepEmailPrivate}}
<div class="field">
<label>{{ctx.Locale.Tr "email"}}</label>
<p id="signed-user-email">{{.SignedUser.Email}}</p>
</div>
{{end}}
<div class="field combo-markdown-editor-init {{if .Err_Biography}}error{{end}}">
<label for="biography">{{ctx.Locale.Tr "user.user_bio"}}</label>
{{template "shared/combomarkdowneditor" (dict

View file

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

View file

@ -4,10 +4,8 @@
// templates/shared/combomarkdowneditor.tmpl
// @watch end
import {expect} from '@playwright/test';
import {expect, type Page, type Locator} from '@playwright/test';
import {test, load_logged_in_context, login_user} from './utils_e2e.ts';
// eslint-disable-next-line no-duplicate-imports
import type {Page, Locator} from '@playwright/test';
test.beforeAll(async ({browser}, workerInfo) => {
await login_user(browser, workerInfo, 'user2');
@ -229,7 +227,7 @@ async function testMarkdownEditorPage(page: Page, url: string) {
}
}
// eslint-disable-next-line playwright/expect-expect
/* eslint playwright/expect-expect: ["error", { "assertFunctionNames": ["testMarkdownEditorPage"] }] */
test('markdown editor init', async ({browser}, workerInfo) => {
const context = await load_logged_in_context(browser, workerInfo, 'user2');

View file

@ -4,11 +4,13 @@
package integration
import (
"fmt"
"net/http"
"net/url"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
@ -16,7 +18,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestAPIReposGitNotes(t *testing.T) {
func TestAPIReposGetGitNotes(t *testing.T) {
onGiteaRun(t, func(*testing.T, *url.URL) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// Login as User2.
@ -44,3 +46,53 @@ func TestAPIReposGitNotes(t *testing.T) {
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)
})
}

View 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\"")
})
})
}

View file

@ -1,4 +1,5 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
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-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")
}

View file

@ -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');
});
}
}

View file

@ -33,7 +33,7 @@ import {
initRepoPullRequestAllowMaintainerEdit,
initRepoPullRequestReview, initRepoIssueSidebarList, initArchivedLabelHandler,
} from './features/repo-issue.js';
import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.js';
import {initRepoEllipsisButton, initCommitStatuses, initCommitNotes} from './features/repo-commit.js';
import {
initFootLanguageMenu,
initGlobalButtonClickOnEnter,
@ -179,6 +179,7 @@ onDomReady(() => {
initRepoRecentCommits();
initCommitStatuses();
initCommitNotes();
initCaptcha();
initUserAuthOauth2();