From f90928507a8173a79847b8b5d81fcce93ac2da31 Mon Sep 17 00:00:00 2001 From: JakobDev Date: Mon, 18 Nov 2024 22:56:17 +0000 Subject: [PATCH] [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.
Screenshots ![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)
## 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/.md` to be be used for the release notes instead of the title. ## Release notes - Features - [PR](https://codeberg.org/forgejo/forgejo/pulls/4753): Allow changing git notes Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/4753 Reviewed-by: Gusted Co-authored-by: JakobDev Co-committed-by: JakobDev --- modules/git/notes.go | 39 +++++++ modules/git/notes_test.go | 71 ++++++++++-- modules/structs/repo_note.go | 4 + options/locale/locale_en-US.ini | 3 + routers/api/v1/api.go | 6 +- routers/api/v1/repo/notes.go | 105 ++++++++++++++++++ routers/api/v1/swagger/options.go | 3 + routers/web/repo/commit.go | 28 +++++ routers/web/web.go | 4 + services/forms/repo_form.go | 4 + templates/repo/commit_page.tmpl | 53 ++++++++- templates/swagger/v1_json.tmpl | 107 ++++++++++++++++++- tests/e2e/git-notes.test.e2e.ts | 30 ++++++ tests/integration/api_repo_git_notes_test.go | 54 +++++++++- tests/integration/repo_git_note_test.go | 44 ++++++++ web_src/js/features/repo-commit.js | 18 ++++ web_src/js/index.js | 3 +- 17 files changed, 562 insertions(+), 14 deletions(-) create mode 100644 tests/e2e/git-notes.test.e2e.ts create mode 100644 tests/integration/repo_git_note_test.go diff --git a/modules/git/notes.go b/modules/git/notes.go index ee628c0436..54f4d714e2 100644 --- a/modules/git/notes.go +++ b/modules/git/notes.go @@ -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 +} diff --git a/modules/git/notes_test.go b/modules/git/notes_test.go index bbb16ccb14..cb9f39b93a 100644 --- a/modules/git/notes_test.go +++ b/modules/git/notes_test.go @@ -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", ¬e) + note := git.Note{} + err = git.GetNote(context.Background(), bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653", ¬e) 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", ¬e) + note := git.Note{} + err = git.GetNote(context.Background(), repo, "3e668dbfac39cbc80a9ff9c61eb565d944453ba4", ¬e) require.NoError(t, err) 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) 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", ¬e) + note := git.Note{} + err = git.GetNote(context.Background(), bareRepo1, "non_existent_sha", ¬e) 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) } diff --git a/modules/structs/repo_note.go b/modules/structs/repo_note.go index 4eaf5a255d..76c6c17898 100644 --- a/modules/structs/repo_note.go +++ b/modules/structs/repo_note.go @@ -8,3 +8,7 @@ type Note struct { Message string `json:"message"` Commit *Commit `json:"commit"` } + +type NoteOptions struct { + Message string `json:"message"` +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 4423832d2a..856a835b32 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -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 will remove this Note diff.data_not_available = Diff content is not available diff.options_button = Diff options diff.show_diff_stats = Show stats diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 718b27aeef..4fe10d8a00 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -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() { diff --git a/routers/api/v1/repo/notes.go b/routers/api/v1/repo/notes.go index a4a1d4eab7..9ed78ce80f 100644 --- a/routers/api/v1/repo/notes.go +++ b/routers/api/v1/repo/notes.go @@ -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) +} diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 3034b09ce3..1dccf92d82 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -231,4 +231,7 @@ type swaggerParameterBodies struct { // in:body SetUserQuotaGroupsOptions api.SetUserQuotaGroupsOptions + + // in:body + NoteOptions api.NoteOptions } diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 1428238074..a06da71429 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -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)) +} diff --git a/routers/web/web.go b/routers/web/web.go index ecdd5d8d92..d3b50b873c 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1559,6 +1559,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) diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 58c34473aa..f6e184fcb6 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -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 +} diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index a25b450dbe..aaec11385d 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -275,10 +275,61 @@ {{.NoteCommit.Author.Name}} {{end}} {{DateUtils.TimeSince .NoteCommit.Author.When}} + {{if or ($.Permission.CanWrite $.UnitTypeCode) (not $.Repository.IsArchived) (not .IsDeleted)}} +
+ + +
+ + {{end}} -
+
{{.NoteRendered | SanitizeHTML}}
+ {{if and ($.Permission.CanWrite $.UnitTypeCode) (not $.Repository.IsArchived) (not .IsDeleted)}} +
+
+ {{.CsrfTokenHtml}} + +
+ +
+ +
+ +
+
+
+ {{end}} + {{else if and ($.Permission.CanWrite $.UnitTypeCode) (not $.Repository.IsArchived) (not .IsDeleted)}} + +
+
+ {{.CsrfTokenHtml}} + +
+ +
+ +
+ +
+
+
{{end}} {{template "repo/diff/box" .}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 8e64c68f89..3e3838ccc2 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -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": { diff --git a/tests/e2e/git-notes.test.e2e.ts b/tests/e2e/git-notes.test.e2e.ts new file mode 100644 index 0000000000..c0dc1618db --- /dev/null +++ b/tests/e2e/git-notes.test.e2e.ts @@ -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'); +}); diff --git a/tests/integration/api_repo_git_notes_test.go b/tests/integration/api_repo_git_notes_test.go index 9f3e927077..1b5e5d652c 100644 --- a/tests/integration/api_repo_git_notes_test.go +++ b/tests/integration/api_repo_git_notes_test.go @@ -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) + }) +} diff --git a/tests/integration/repo_git_note_test.go b/tests/integration/repo_git_note_test.go new file mode 100644 index 0000000000..9c2423c892 --- /dev/null +++ b/tests/integration/repo_git_note_test.go @@ -0,0 +1,44 @@ +package integration + +import ( + "net/http" + "net/url" + "testing" + + "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(), "
This is a test note\n
") + 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(), "
This is a new note\n
") + 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") + }) + }) +} diff --git a/web_src/js/features/repo-commit.js b/web_src/js/features/repo-commit.js index f61ea08a42..88887d1110 100644 --- a/web_src/js/features/repo-commit.js +++ b/web_src/js/features/repo-commit.js @@ -25,3 +25,21 @@ 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', () => { + notesAddButton.classList.add('tw-hidden'); + document.getElementById('commit-notes-add-area').classList.remove('tw-hidden'); + }); + } +} diff --git a/web_src/js/index.js b/web_src/js/index.js index 80aff9e59e..bab1abfa36 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -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(() => { initRepoMilestoneEditor(); initCommitStatuses(); + initCommitNotes(); initCaptcha(); initUserAuthOauth2();