loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

authored by

JakobDev
JakobDev
and committed by
Earl Warren
f9092850 71ff98d6

+562 -14
+39
modules/git/notes.go
··· 6 6 import ( 7 7 "context" 8 8 "io" 9 + "os" 9 10 "strings" 10 11 11 12 "code.gitea.io/gitea/modules/log" ··· 97 98 98 99 return nil 99 100 } 101 + 102 + func SetNote(ctx context.Context, repo *Repository, commitID, notes, doerName, doerEmail string) error { 103 + _, err := repo.GetCommit(commitID) 104 + if err != nil { 105 + return err 106 + } 107 + 108 + env := append(os.Environ(), 109 + "GIT_AUTHOR_NAME="+doerName, 110 + "GIT_AUTHOR_EMAIL="+doerEmail, 111 + "GIT_COMMITTER_NAME="+doerName, 112 + "GIT_COMMITTER_EMAIL="+doerEmail, 113 + ) 114 + 115 + cmd := NewCommand(ctx, "notes", "add", "-f", "-m") 116 + cmd.AddDynamicArguments(notes, commitID) 117 + 118 + _, stderr, err := cmd.RunStdString(&RunOpts{Dir: repo.Path, Env: env}) 119 + if err != nil { 120 + log.Error("Error while running git notes add: %s", stderr) 121 + return err 122 + } 123 + 124 + return nil 125 + } 126 + 127 + func RemoveNote(ctx context.Context, repo *Repository, commitID string) error { 128 + cmd := NewCommand(ctx, "notes", "remove") 129 + cmd.AddDynamicArguments(commitID) 130 + 131 + _, stderr, err := cmd.RunStdString(&RunOpts{Dir: repo.Path}) 132 + if err != nil { 133 + log.Error("Error while running git notes remove: %s", stderr) 134 + return err 135 + } 136 + 137 + return nil 138 + }
+62 -9
modules/git/notes_test.go
··· 1 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 2 // SPDX-License-Identifier: MIT 3 3 4 - package git 4 + package git_test 5 5 6 6 import ( 7 7 "context" 8 + "os" 8 9 "path/filepath" 9 10 "testing" 10 11 12 + "code.gitea.io/gitea/models/unittest" 13 + "code.gitea.io/gitea/modules/git" 14 + 11 15 "github.com/stretchr/testify/assert" 12 16 "github.com/stretchr/testify/require" 13 17 ) 14 18 19 + const ( 20 + testReposDir = "tests/repos/" 21 + ) 22 + 23 + // openRepositoryWithDefaultContext opens the repository at the given path with DefaultContext. 24 + func openRepositoryWithDefaultContext(repoPath string) (*git.Repository, error) { 25 + return git.OpenRepository(git.DefaultContext, repoPath) 26 + } 27 + 15 28 func TestGetNotes(t *testing.T) { 16 29 bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") 17 30 bareRepo1, err := openRepositoryWithDefaultContext(bareRepo1Path) 18 31 require.NoError(t, err) 19 32 defer bareRepo1.Close() 20 33 21 - note := Note{} 22 - err = GetNote(context.Background(), bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653", &note) 34 + note := git.Note{} 35 + err = git.GetNote(context.Background(), bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653", &note) 23 36 require.NoError(t, err) 24 37 assert.Equal(t, []byte("Note contents\n"), note.Message) 25 38 assert.Equal(t, "Vladimir Panteleev", note.Commit.Author.Name) ··· 31 44 require.NoError(t, err) 32 45 defer repo.Close() 33 46 34 - note := Note{} 35 - err = GetNote(context.Background(), repo, "3e668dbfac39cbc80a9ff9c61eb565d944453ba4", &note) 47 + note := git.Note{} 48 + err = git.GetNote(context.Background(), repo, "3e668dbfac39cbc80a9ff9c61eb565d944453ba4", &note) 36 49 require.NoError(t, err) 37 50 assert.Equal(t, []byte("Note 2"), note.Message) 38 - err = GetNote(context.Background(), repo, "ba0a96fa63532d6c5087ecef070b0250ed72fa47", &note) 51 + err = git.GetNote(context.Background(), repo, "ba0a96fa63532d6c5087ecef070b0250ed72fa47", &note) 39 52 require.NoError(t, err) 40 53 assert.Equal(t, []byte("Note 1"), note.Message) 41 54 } ··· 46 59 require.NoError(t, err) 47 60 defer bareRepo1.Close() 48 61 49 - note := Note{} 50 - err = GetNote(context.Background(), bareRepo1, "non_existent_sha", &note) 62 + note := git.Note{} 63 + err = git.GetNote(context.Background(), bareRepo1, "non_existent_sha", &note) 64 + require.Error(t, err) 65 + assert.IsType(t, git.ErrNotExist{}, err) 66 + } 67 + 68 + func TestSetNote(t *testing.T) { 69 + bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") 70 + 71 + tempDir, err := os.MkdirTemp("", "") 72 + require.NoError(t, err) 73 + defer os.RemoveAll(tempDir) 74 + require.NoError(t, unittest.CopyDir(bareRepo1Path, filepath.Join(tempDir, "repo1"))) 75 + 76 + bareRepo1, err := openRepositoryWithDefaultContext(filepath.Join(tempDir, "repo1")) 77 + require.NoError(t, err) 78 + defer bareRepo1.Close() 79 + 80 + require.NoError(t, git.SetNote(context.Background(), bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653", "This is a new note", "Test", "test@test.com")) 81 + 82 + note := git.Note{} 83 + err = git.GetNote(context.Background(), bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653", &note) 84 + require.NoError(t, err) 85 + assert.Equal(t, []byte("This is a new note\n"), note.Message) 86 + assert.Equal(t, "Test", note.Commit.Author.Name) 87 + } 88 + 89 + func TestRemoveNote(t *testing.T) { 90 + bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") 91 + 92 + tempDir := t.TempDir() 93 + 94 + require.NoError(t, unittest.CopyDir(bareRepo1Path, filepath.Join(tempDir, "repo1"))) 95 + 96 + bareRepo1, err := openRepositoryWithDefaultContext(filepath.Join(tempDir, "repo1")) 97 + require.NoError(t, err) 98 + defer bareRepo1.Close() 99 + 100 + require.NoError(t, git.RemoveNote(context.Background(), bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653")) 101 + 102 + note := git.Note{} 103 + err = git.GetNote(context.Background(), bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653", &note) 51 104 require.Error(t, err) 52 - assert.IsType(t, ErrNotExist{}, err) 105 + assert.IsType(t, git.ErrNotExist{}, err) 53 106 }
+4
modules/structs/repo_note.go
··· 8 8 Message string `json:"message"` 9 9 Commit *Commit `json:"commit"` 10 10 } 11 + 12 + type NoteOptions struct { 13 + Message string `json:"message"` 14 + }
+3
options/locale/locale_en-US.ini
··· 2622 2622 diff.parent = parent 2623 2623 diff.commit = commit 2624 2624 diff.git-notes = Notes 2625 + diff.git-notes.add = Add Note 2626 + diff.git-notes.remove-header = Remove Note 2627 + diff.git-notes.remove-body = This will remove this Note 2625 2628 diff.data_not_available = Diff content is not available 2626 2629 diff.options_button = Diff options 2627 2630 diff.show_diff_stats = Show stats
+5 -1
routers/api/v1/api.go
··· 1316 1316 m.Get("/trees/{sha}", repo.GetTree) 1317 1317 m.Get("/blobs/{sha}", repo.GetBlob) 1318 1318 m.Get("/tags/{sha}", repo.GetAnnotatedTag) 1319 - m.Get("/notes/{sha}", repo.GetNote) 1319 + m.Group("/notes/{sha}", func() { 1320 + m.Get("", repo.GetNote) 1321 + m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), bind(api.NoteOptions{}), repo.SetNote) 1322 + m.Delete("", reqToken(), reqRepoWriter(unit.TypeCode), repo.RemoveNote) 1323 + }) 1320 1324 }, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode)) 1321 1325 m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.ApplyDiffPatch) 1322 1326 m.Group("/contents", func() {
+105
routers/api/v1/repo/notes.go
··· 9 9 10 10 "code.gitea.io/gitea/modules/git" 11 11 api "code.gitea.io/gitea/modules/structs" 12 + "code.gitea.io/gitea/modules/web" 12 13 "code.gitea.io/gitea/services/context" 13 14 "code.gitea.io/gitea/services/convert" 14 15 ) ··· 102 103 apiNote := api.Note{Message: string(note.Message), Commit: cmt} 103 104 ctx.JSON(http.StatusOK, apiNote) 104 105 } 106 + 107 + // SetNote Sets a note corresponding to a single commit from a repository 108 + func SetNote(ctx *context.APIContext) { 109 + // swagger:operation POST /repos/{owner}/{repo}/git/notes/{sha} repository repoSetNote 110 + // --- 111 + // summary: Set a note corresponding to a single commit from a repository 112 + // produces: 113 + // - application/json 114 + // parameters: 115 + // - name: owner 116 + // in: path 117 + // description: owner of the repo 118 + // type: string 119 + // required: true 120 + // - name: repo 121 + // in: path 122 + // description: name of the repo 123 + // type: string 124 + // required: true 125 + // - name: sha 126 + // in: path 127 + // description: a git ref or commit sha 128 + // type: string 129 + // required: true 130 + // - name: body 131 + // in: body 132 + // schema: 133 + // "$ref": "#/definitions/NoteOptions" 134 + // responses: 135 + // "200": 136 + // "$ref": "#/responses/Note" 137 + // "404": 138 + // "$ref": "#/responses/notFound" 139 + // "422": 140 + // "$ref": "#/responses/validationError" 141 + sha := ctx.Params(":sha") 142 + if !git.IsValidRefPattern(sha) { 143 + ctx.Error(http.StatusUnprocessableEntity, "no valid ref or sha", fmt.Sprintf("no valid ref or sha: %s", sha)) 144 + return 145 + } 146 + 147 + form := web.GetForm(ctx).(*api.NoteOptions) 148 + 149 + err := git.SetNote(ctx, ctx.Repo.GitRepo, sha, form.Message, ctx.Doer.Name, ctx.Doer.GetEmail()) 150 + if err != nil { 151 + if git.IsErrNotExist(err) { 152 + ctx.NotFound(sha) 153 + } else { 154 + ctx.Error(http.StatusInternalServerError, "SetNote", err) 155 + } 156 + return 157 + } 158 + 159 + getNote(ctx, sha) 160 + } 161 + 162 + // RemoveNote Removes a note corresponding to a single commit from a repository 163 + func RemoveNote(ctx *context.APIContext) { 164 + // swagger:operation DELETE /repos/{owner}/{repo}/git/notes/{sha} repository repoRemoveNote 165 + // --- 166 + // summary: Removes a note corresponding to a single commit from a repository 167 + // produces: 168 + // - application/json 169 + // parameters: 170 + // - name: owner 171 + // in: path 172 + // description: owner of the repo 173 + // type: string 174 + // required: true 175 + // - name: repo 176 + // in: path 177 + // description: name of the repo 178 + // type: string 179 + // required: true 180 + // - name: sha 181 + // in: path 182 + // description: a git ref or commit sha 183 + // type: string 184 + // required: true 185 + // responses: 186 + // "204": 187 + // "$ref": "#/responses/empty" 188 + // "404": 189 + // "$ref": "#/responses/notFound" 190 + // "422": 191 + // "$ref": "#/responses/validationError" 192 + sha := ctx.Params(":sha") 193 + if !git.IsValidRefPattern(sha) { 194 + ctx.Error(http.StatusUnprocessableEntity, "no valid ref or sha", fmt.Sprintf("no valid ref or sha: %s", sha)) 195 + return 196 + } 197 + 198 + err := git.RemoveNote(ctx, ctx.Repo.GitRepo, sha) 199 + if err != nil { 200 + if git.IsErrNotExist(err) { 201 + ctx.NotFound(sha) 202 + } else { 203 + ctx.Error(http.StatusInternalServerError, "RemoveNote", err) 204 + } 205 + return 206 + } 207 + 208 + ctx.Status(http.StatusNoContent) 209 + }
+3
routers/api/v1/swagger/options.go
··· 231 231 232 232 // in:body 233 233 SetUserQuotaGroupsOptions api.SetUserQuotaGroupsOptions 234 + 235 + // in:body 236 + NoteOptions api.NoteOptions 234 237 }
+28
routers/web/repo/commit.go
··· 27 27 "code.gitea.io/gitea/modules/markup" 28 28 "code.gitea.io/gitea/modules/setting" 29 29 "code.gitea.io/gitea/modules/util" 30 + "code.gitea.io/gitea/modules/web" 30 31 "code.gitea.io/gitea/services/context" 32 + "code.gitea.io/gitea/services/forms" 31 33 "code.gitea.io/gitea/services/gitdiff" 32 34 git_service "code.gitea.io/gitea/services/repository" 33 35 ) ··· 467 469 } 468 470 return commits 469 471 } 472 + 473 + func SetCommitNotes(ctx *context.Context) { 474 + form := web.GetForm(ctx).(*forms.CommitNotesForm) 475 + 476 + commitID := ctx.Params(":sha") 477 + 478 + err := git.SetNote(ctx, ctx.Repo.GitRepo, commitID, form.Notes, ctx.Doer.Name, ctx.Doer.GetEmail()) 479 + if err != nil { 480 + ctx.ServerError("SetNote", err) 481 + return 482 + } 483 + 484 + ctx.Redirect(fmt.Sprintf("%s/commit/%s", ctx.Repo.Repository.HTMLURL(), commitID)) 485 + } 486 + 487 + func RemoveCommitNotes(ctx *context.Context) { 488 + commitID := ctx.Params(":sha") 489 + 490 + err := git.RemoveNote(ctx, ctx.Repo.GitRepo, commitID) 491 + if err != nil { 492 + ctx.ServerError("RemoveNotes", err) 493 + return 494 + } 495 + 496 + ctx.Redirect(fmt.Sprintf("%s/commit/%s", ctx.Repo.Repository.HTMLURL(), commitID)) 497 + }
+4
routers/web/web.go
··· 1559 1559 m.Get("/graph", repo.Graph) 1560 1560 m.Get("/commit/{sha:([a-f0-9]{4,64})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff) 1561 1561 m.Get("/commit/{sha:([a-f0-9]{4,64})$}/load-branches-and-tags", repo.LoadBranchesAndTags) 1562 + m.Group("/commit/{sha:([a-f0-9]{4,64})$}/notes", func() { 1563 + m.Post("", web.Bind(forms.CommitNotesForm{}), repo.SetCommitNotes) 1564 + m.Post("/remove", repo.RemoveCommitNotes) 1565 + }, reqSignIn, reqRepoCodeWriter) 1562 1566 m.Get("/cherry-pick/{sha:([a-f0-9]{4,64})$}", repo.SetEditorconfigIfExists, repo.CherryPick) 1563 1567 }, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader) 1564 1568
+4
services/forms/repo_form.go
··· 749 749 ctx := context.GetValidateContext(req) 750 750 return middleware.Validate(errs, ctx.Data, f, ctx.Locale) 751 751 } 752 + 753 + type CommitNotesForm struct { 754 + Notes string 755 + }
+52 -1
templates/repo/commit_page.tmpl
··· 275 275 <strong>{{.NoteCommit.Author.Name}}</strong> 276 276 {{end}} 277 277 <span class="text grey" id="note-authored-time">{{DateUtils.TimeSince .NoteCommit.Author.When}}</span> 278 + {{if or ($.Permission.CanWrite $.UnitTypeCode) (not $.Repository.IsArchived) (not .IsDeleted)}} 279 + <div class="ui right"> 280 + <button id="commit-notes-edit-button" class="ui tiny button yellow" data-modal="#delete-note-modal">{{ctx.Locale.Tr "edit"}}</button> 281 + <button class="ui tiny button red show-modal" data-modal="#delete-note-modal">{{ctx.Locale.Tr "remove"}}</button> 282 + </div> 283 + <div class="ui small modal" id="delete-note-modal"> 284 + <div class="header"> 285 + {{ctx.Locale.Tr "repo.diff.git-notes.remove-header"}} 286 + </div> 287 + <p>{{ctx.Locale.Tr "repo.diff.git-notes.remove-body"}}</p> 288 + <div class="content"> 289 + <div class="text right actions"> 290 + <form action="{{.Link}}/notes/remove" method="post"> 291 + {{.CsrfTokenHtml}} 292 + <button type="button" class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button> 293 + <button type="submit" class="ui primary button red" href="{{.Link}}/notes/remove">{{ctx.Locale.Tr "remove"}}</button> 294 + </form> 295 + </div> 296 + </div> 297 + </div> 298 + {{end}} 278 299 </div> 279 - <div class="ui bottom attached info segment git-notes"> 300 + <div id="commit-notes-display-area" class="ui bottom attached info segment git-notes"> 280 301 <pre class="commit-body">{{.NoteRendered | SanitizeHTML}}</pre> 302 + </div> 303 + {{if and ($.Permission.CanWrite $.UnitTypeCode) (not $.Repository.IsArchived) (not .IsDeleted)}} 304 + <div id="commit-notes-edit-area" class="ui bottom attached info segment git-notes tw-hidden"> 305 + <form class="ui form" action="{{.Link}}/notes" method="post"> 306 + {{.CsrfTokenHtml}} 307 + 308 + <div class="field"> 309 + <textarea name="notes">{{.NoteRendered | SanitizeHTML}}</textarea> 310 + </div> 311 + 312 + <div class="field"> 313 + <button id="notes-save-button" class="ui primary button">{{ctx.Locale.Tr "save"}}</button> 314 + </div> 315 + </form> 316 + </div> 317 + {{end}} 318 + {{else if and ($.Permission.CanWrite $.UnitTypeCode) (not $.Repository.IsArchived) (not .IsDeleted)}} 319 + <button id="commit-notes-add-button" class="ui primary button green tw-mt-3">{{ctx.Locale.Tr "repo.diff.git-notes.add"}}</button> 320 + <div id="commit-notes-add-area" class="ui tw-mt-3 segment tw-hidden"> 321 + <form class="ui form" action="{{.Link}}/notes" method="post"> 322 + {{.CsrfTokenHtml}} 323 + 324 + <div class="field"> 325 + <textarea name="notes"></textarea> 326 + </div> 327 + 328 + <div class="field"> 329 + <button class="ui primary button">{{ctx.Locale.Tr "add"}}</button> 330 + </div> 331 + </form> 281 332 </div> 282 333 {{end}} 283 334 {{template "repo/diff/box" .}}
+106 -1
templates/swagger/v1_json.tmpl
··· 7375 7375 "$ref": "#/responses/validationError" 7376 7376 } 7377 7377 } 7378 + }, 7379 + "post": { 7380 + "produces": [ 7381 + "application/json" 7382 + ], 7383 + "tags": [ 7384 + "repository" 7385 + ], 7386 + "summary": "Set a note corresponding to a single commit from a repository", 7387 + "operationId": "repoSetNote", 7388 + "parameters": [ 7389 + { 7390 + "type": "string", 7391 + "description": "owner of the repo", 7392 + "name": "owner", 7393 + "in": "path", 7394 + "required": true 7395 + }, 7396 + { 7397 + "type": "string", 7398 + "description": "name of the repo", 7399 + "name": "repo", 7400 + "in": "path", 7401 + "required": true 7402 + }, 7403 + { 7404 + "type": "string", 7405 + "description": "a git ref or commit sha", 7406 + "name": "sha", 7407 + "in": "path", 7408 + "required": true 7409 + }, 7410 + { 7411 + "name": "body", 7412 + "in": "body", 7413 + "schema": { 7414 + "$ref": "#/definitions/NoteOptions" 7415 + } 7416 + } 7417 + ], 7418 + "responses": { 7419 + "200": { 7420 + "$ref": "#/responses/Note" 7421 + }, 7422 + "404": { 7423 + "$ref": "#/responses/notFound" 7424 + }, 7425 + "422": { 7426 + "$ref": "#/responses/validationError" 7427 + } 7428 + } 7429 + }, 7430 + "delete": { 7431 + "produces": [ 7432 + "application/json" 7433 + ], 7434 + "tags": [ 7435 + "repository" 7436 + ], 7437 + "summary": "Removes a note corresponding to a single commit from a repository", 7438 + "operationId": "repoRemoveNote", 7439 + "parameters": [ 7440 + { 7441 + "type": "string", 7442 + "description": "owner of the repo", 7443 + "name": "owner", 7444 + "in": "path", 7445 + "required": true 7446 + }, 7447 + { 7448 + "type": "string", 7449 + "description": "name of the repo", 7450 + "name": "repo", 7451 + "in": "path", 7452 + "required": true 7453 + }, 7454 + { 7455 + "type": "string", 7456 + "description": "a git ref or commit sha", 7457 + "name": "sha", 7458 + "in": "path", 7459 + "required": true 7460 + } 7461 + ], 7462 + "responses": { 7463 + "204": { 7464 + "$ref": "#/responses/empty" 7465 + }, 7466 + "404": { 7467 + "$ref": "#/responses/notFound" 7468 + }, 7469 + "422": { 7470 + "$ref": "#/responses/validationError" 7471 + } 7472 + } 7378 7473 } 7379 7474 }, 7380 7475 "/repos/{owner}/{repo}/git/refs": { ··· 24601 24696 }, 24602 24697 "x-go-package": "code.gitea.io/gitea/modules/structs" 24603 24698 }, 24699 + "NoteOptions": { 24700 + "type": "object", 24701 + "properties": { 24702 + "message": { 24703 + "type": "string", 24704 + "x-go-name": "Message" 24705 + } 24706 + }, 24707 + "x-go-package": "code.gitea.io/gitea/modules/structs" 24708 + }, 24604 24709 "NotificationCount": { 24605 24710 "description": "NotificationCount number of unread notifications", 24606 24711 "type": "object", ··· 28350 28455 "parameterBodies": { 28351 28456 "description": "parameterBodies", 28352 28457 "schema": { 28353 - "$ref": "#/definitions/SetUserQuotaGroupsOptions" 28458 + "$ref": "#/definitions/NoteOptions" 28354 28459 } 28355 28460 }, 28356 28461 "quotaExceeded": {
+30
tests/e2e/git-notes.test.e2e.ts
··· 1 + // @ts-check 2 + import {test, expect} from '@playwright/test'; 3 + import {login_user, load_logged_in_context} from './utils_e2e.ts'; 4 + 5 + test.beforeAll(async ({browser}, workerInfo) => { 6 + await login_user(browser, workerInfo, 'user2'); 7 + }); 8 + 9 + test('Change git note', async ({browser}, workerInfo) => { 10 + const context = await load_logged_in_context(browser, workerInfo, 'user2'); 11 + const page = await context.newPage(); 12 + let response = await page.goto('/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d'); 13 + expect(response?.status()).toBe(200); 14 + 15 + await page.locator('#commit-notes-edit-button').click(); 16 + 17 + let textarea = page.locator('textarea[name="notes"]'); 18 + await expect(textarea).toBeVisible(); 19 + await textarea.fill('This is a new note'); 20 + 21 + await page.locator('#notes-save-button').click(); 22 + 23 + expect(response?.status()).toBe(200); 24 + 25 + response = await page.goto('/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d'); 26 + expect(response?.status()).toBe(200); 27 + 28 + textarea = page.locator('textarea[name="notes"]'); 29 + await expect(textarea).toHaveText('This is a new note'); 30 + });
+53 -1
tests/integration/api_repo_git_notes_test.go
··· 4 4 package integration 5 5 6 6 import ( 7 + "fmt" 7 8 "net/http" 8 9 "net/url" 9 10 "testing" 10 11 11 12 auth_model "code.gitea.io/gitea/models/auth" 13 + repo_model "code.gitea.io/gitea/models/repo" 12 14 "code.gitea.io/gitea/models/unittest" 13 15 user_model "code.gitea.io/gitea/models/user" 14 16 api "code.gitea.io/gitea/modules/structs" ··· 16 18 "github.com/stretchr/testify/assert" 17 19 ) 18 20 19 - func TestAPIReposGitNotes(t *testing.T) { 21 + func TestAPIReposGetGitNotes(t *testing.T) { 20 22 onGiteaRun(t, func(*testing.T, *url.URL) { 21 23 user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) 22 24 // Login as User2. ··· 44 46 assert.NotNil(t, apiData.Commit.RepoCommit.Verification) 45 47 }) 46 48 } 49 + 50 + func TestAPIReposSetGitNotes(t *testing.T) { 51 + onGiteaRun(t, func(*testing.T, *url.URL) { 52 + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) 53 + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) 54 + 55 + session := loginUser(t, user.Name) 56 + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) 57 + 58 + req := NewRequestf(t, "GET", "/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName()) 59 + resp := MakeRequest(t, req, http.StatusOK) 60 + var apiData api.Note 61 + DecodeJSON(t, resp, &apiData) 62 + assert.Equal(t, "This is a test note\n", apiData.Message) 63 + 64 + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName()), &api.NoteOptions{ 65 + Message: "This is a new note", 66 + }).AddTokenAuth(token) 67 + resp = MakeRequest(t, req, http.StatusOK) 68 + DecodeJSON(t, resp, &apiData) 69 + assert.Equal(t, "This is a new note\n", apiData.Message) 70 + 71 + req = NewRequestf(t, "GET", "/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName()) 72 + resp = MakeRequest(t, req, http.StatusOK) 73 + DecodeJSON(t, resp, &apiData) 74 + assert.Equal(t, "This is a new note\n", apiData.Message) 75 + }) 76 + } 77 + 78 + func TestAPIReposDeleteGitNotes(t *testing.T) { 79 + onGiteaRun(t, func(*testing.T, *url.URL) { 80 + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) 81 + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) 82 + 83 + session := loginUser(t, user.Name) 84 + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) 85 + 86 + req := NewRequestf(t, "GET", "/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName()) 87 + resp := MakeRequest(t, req, http.StatusOK) 88 + var apiData api.Note 89 + DecodeJSON(t, resp, &apiData) 90 + assert.Equal(t, "This is a test note\n", apiData.Message) 91 + 92 + req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName()).AddTokenAuth(token) 93 + MakeRequest(t, req, http.StatusNoContent) 94 + 95 + req = NewRequestf(t, "GET", "/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName()) 96 + MakeRequest(t, req, http.StatusNotFound) 97 + }) 98 + }
+44
tests/integration/repo_git_note_test.go
··· 1 + package integration 2 + 3 + import ( 4 + "net/http" 5 + "net/url" 6 + "testing" 7 + 8 + "github.com/stretchr/testify/assert" 9 + ) 10 + 11 + func TestRepoModifyGitNotes(t *testing.T) { 12 + onGiteaRun(t, func(*testing.T, *url.URL) { 13 + session := loginUser(t, "user2") 14 + 15 + req := NewRequest(t, "GET", "/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d") 16 + resp := MakeRequest(t, req, http.StatusOK) 17 + assert.Contains(t, resp.Body.String(), "<pre class=\"commit-body\">This is a test note\n</pre>") 18 + assert.Contains(t, resp.Body.String(), "commit-notes-display-area") 19 + 20 + t.Run("Set", func(t *testing.T) { 21 + req = NewRequestWithValues(t, "POST", "/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d/notes", map[string]string{ 22 + "_csrf": GetCSRF(t, session, "/user2/repo1"), 23 + "notes": "This is a new note", 24 + }) 25 + session.MakeRequest(t, req, http.StatusSeeOther) 26 + 27 + req = NewRequest(t, "GET", "/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d") 28 + resp = MakeRequest(t, req, http.StatusOK) 29 + assert.Contains(t, resp.Body.String(), "<pre class=\"commit-body\">This is a new note\n</pre>") 30 + assert.Contains(t, resp.Body.String(), "commit-notes-display-area") 31 + }) 32 + 33 + t.Run("Delete", func(t *testing.T) { 34 + req = NewRequestWithValues(t, "POST", "/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d/notes/remove", map[string]string{ 35 + "_csrf": GetCSRF(t, session, "/user2/repo1"), 36 + }) 37 + session.MakeRequest(t, req, http.StatusSeeOther) 38 + 39 + req = NewRequest(t, "GET", "/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d") 40 + resp = MakeRequest(t, req, http.StatusOK) 41 + assert.NotContains(t, resp.Body.String(), "commit-notes-display-area") 42 + }) 43 + }) 44 + }
+18
web_src/js/features/repo-commit.js
··· 25 25 }); 26 26 } 27 27 } 28 + 29 + export function initCommitNotes() { 30 + const notesEditButton = document.getElementById('commit-notes-edit-button'); 31 + if (notesEditButton !== null) { 32 + notesEditButton.addEventListener('click', () => { 33 + document.getElementById('commit-notes-display-area').classList.add('tw-hidden'); 34 + document.getElementById('commit-notes-edit-area').classList.remove('tw-hidden'); 35 + }); 36 + } 37 + 38 + const notesAddButton = document.getElementById('commit-notes-add-button'); 39 + if (notesAddButton !== null) { 40 + notesAddButton.addEventListener('click', () => { 41 + notesAddButton.classList.add('tw-hidden'); 42 + document.getElementById('commit-notes-add-area').classList.remove('tw-hidden'); 43 + }); 44 + } 45 + }
+2 -1
web_src/js/index.js
··· 33 33 initRepoPullRequestAllowMaintainerEdit, 34 34 initRepoPullRequestReview, initRepoIssueSidebarList, initArchivedLabelHandler, 35 35 } from './features/repo-issue.js'; 36 - import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.js'; 36 + import {initRepoEllipsisButton, initCommitStatuses, initCommitNotes} from './features/repo-commit.js'; 37 37 import { 38 38 initFootLanguageMenu, 39 39 initGlobalButtonClickOnEnter, ··· 179 179 initRepoMilestoneEditor(); 180 180 181 181 initCommitStatuses(); 182 + initCommitNotes(); 182 183 initCaptcha(); 183 184 184 185 initUserAuthOauth2();