[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>
This commit is contained in:
parent
71ff98d61d
commit
f90928507a
17 changed files with 562 additions and 14 deletions
|
@ -6,6 +6,7 @@ package git
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
@ -97,3 +98,41 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetNote(ctx context.Context, repo *Repository, commitID, notes, doerName, doerEmail string) error {
|
||||||
|
_, err := repo.GetCommit(commitID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
env := append(os.Environ(),
|
||||||
|
"GIT_AUTHOR_NAME="+doerName,
|
||||||
|
"GIT_AUTHOR_EMAIL="+doerEmail,
|
||||||
|
"GIT_COMMITTER_NAME="+doerName,
|
||||||
|
"GIT_COMMITTER_EMAIL="+doerEmail,
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd := NewCommand(ctx, "notes", "add", "-f", "-m")
|
||||||
|
cmd.AddDynamicArguments(notes, commitID)
|
||||||
|
|
||||||
|
_, stderr, err := cmd.RunStdString(&RunOpts{Dir: repo.Path, Env: env})
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error while running git notes add: %s", stderr)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveNote(ctx context.Context, repo *Repository, commitID string) error {
|
||||||
|
cmd := NewCommand(ctx, "notes", "remove")
|
||||||
|
cmd.AddDynamicArguments(commitID)
|
||||||
|
|
||||||
|
_, stderr, err := cmd.RunStdString(&RunOpts{Dir: repo.Path})
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error while running git notes remove: %s", stderr)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,25 +1,38 @@
|
||||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package git
|
package git_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testReposDir = "tests/repos/"
|
||||||
|
)
|
||||||
|
|
||||||
|
// openRepositoryWithDefaultContext opens the repository at the given path with DefaultContext.
|
||||||
|
func openRepositoryWithDefaultContext(repoPath string) (*git.Repository, error) {
|
||||||
|
return git.OpenRepository(git.DefaultContext, repoPath)
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetNotes(t *testing.T) {
|
func TestGetNotes(t *testing.T) {
|
||||||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
|
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
|
||||||
bareRepo1, err := openRepositoryWithDefaultContext(bareRepo1Path)
|
bareRepo1, err := openRepositoryWithDefaultContext(bareRepo1Path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer bareRepo1.Close()
|
defer bareRepo1.Close()
|
||||||
|
|
||||||
note := Note{}
|
note := git.Note{}
|
||||||
err = GetNote(context.Background(), bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653", ¬e)
|
err = git.GetNote(context.Background(), bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653", ¬e)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, []byte("Note contents\n"), note.Message)
|
assert.Equal(t, []byte("Note contents\n"), note.Message)
|
||||||
assert.Equal(t, "Vladimir Panteleev", note.Commit.Author.Name)
|
assert.Equal(t, "Vladimir Panteleev", note.Commit.Author.Name)
|
||||||
|
@ -31,11 +44,11 @@ func TestGetNestedNotes(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer repo.Close()
|
defer repo.Close()
|
||||||
|
|
||||||
note := Note{}
|
note := git.Note{}
|
||||||
err = GetNote(context.Background(), repo, "3e668dbfac39cbc80a9ff9c61eb565d944453ba4", ¬e)
|
err = git.GetNote(context.Background(), repo, "3e668dbfac39cbc80a9ff9c61eb565d944453ba4", ¬e)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, []byte("Note 2"), note.Message)
|
assert.Equal(t, []byte("Note 2"), note.Message)
|
||||||
err = GetNote(context.Background(), repo, "ba0a96fa63532d6c5087ecef070b0250ed72fa47", ¬e)
|
err = git.GetNote(context.Background(), repo, "ba0a96fa63532d6c5087ecef070b0250ed72fa47", ¬e)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, []byte("Note 1"), note.Message)
|
assert.Equal(t, []byte("Note 1"), note.Message)
|
||||||
}
|
}
|
||||||
|
@ -46,8 +59,48 @@ func TestGetNonExistentNotes(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer bareRepo1.Close()
|
defer bareRepo1.Close()
|
||||||
|
|
||||||
note := Note{}
|
note := git.Note{}
|
||||||
err = GetNote(context.Background(), bareRepo1, "non_existent_sha", ¬e)
|
err = git.GetNote(context.Background(), bareRepo1, "non_existent_sha", ¬e)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.IsType(t, ErrNotExist{}, err)
|
assert.IsType(t, git.ErrNotExist{}, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetNote(t *testing.T) {
|
||||||
|
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
|
||||||
|
|
||||||
|
tempDir, err := os.MkdirTemp("", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
require.NoError(t, unittest.CopyDir(bareRepo1Path, filepath.Join(tempDir, "repo1")))
|
||||||
|
|
||||||
|
bareRepo1, err := openRepositoryWithDefaultContext(filepath.Join(tempDir, "repo1"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer bareRepo1.Close()
|
||||||
|
|
||||||
|
require.NoError(t, git.SetNote(context.Background(), bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653", "This is a new note", "Test", "test@test.com"))
|
||||||
|
|
||||||
|
note := git.Note{}
|
||||||
|
err = git.GetNote(context.Background(), bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653", ¬e)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []byte("This is a new note\n"), note.Message)
|
||||||
|
assert.Equal(t, "Test", note.Commit.Author.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveNote(t *testing.T) {
|
||||||
|
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
require.NoError(t, unittest.CopyDir(bareRepo1Path, filepath.Join(tempDir, "repo1")))
|
||||||
|
|
||||||
|
bareRepo1, err := openRepositoryWithDefaultContext(filepath.Join(tempDir, "repo1"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer bareRepo1.Close()
|
||||||
|
|
||||||
|
require.NoError(t, git.RemoveNote(context.Background(), bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653"))
|
||||||
|
|
||||||
|
note := git.Note{}
|
||||||
|
err = git.GetNote(context.Background(), bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653", ¬e)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.IsType(t, git.ErrNotExist{}, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,3 +8,7 @@ type Note struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Commit *Commit `json:"commit"`
|
Commit *Commit `json:"commit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NoteOptions struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
|
@ -2622,6 +2622,9 @@ diff.browse_source = Browse source
|
||||||
diff.parent = parent
|
diff.parent = parent
|
||||||
diff.commit = commit
|
diff.commit = commit
|
||||||
diff.git-notes = Notes
|
diff.git-notes = Notes
|
||||||
|
diff.git-notes.add = Add Note
|
||||||
|
diff.git-notes.remove-header = Remove Note
|
||||||
|
diff.git-notes.remove-body = This will remove this Note
|
||||||
diff.data_not_available = Diff content is not available
|
diff.data_not_available = Diff content is not available
|
||||||
diff.options_button = Diff options
|
diff.options_button = Diff options
|
||||||
diff.show_diff_stats = Show stats
|
diff.show_diff_stats = Show stats
|
||||||
|
|
|
@ -1316,7 +1316,11 @@ func Routes() *web.Route {
|
||||||
m.Get("/trees/{sha}", repo.GetTree)
|
m.Get("/trees/{sha}", repo.GetTree)
|
||||||
m.Get("/blobs/{sha}", repo.GetBlob)
|
m.Get("/blobs/{sha}", repo.GetBlob)
|
||||||
m.Get("/tags/{sha}", repo.GetAnnotatedTag)
|
m.Get("/tags/{sha}", repo.GetAnnotatedTag)
|
||||||
m.Get("/notes/{sha}", repo.GetNote)
|
m.Group("/notes/{sha}", func() {
|
||||||
|
m.Get("", repo.GetNote)
|
||||||
|
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), bind(api.NoteOptions{}), repo.SetNote)
|
||||||
|
m.Delete("", reqToken(), reqRepoWriter(unit.TypeCode), repo.RemoveNote)
|
||||||
|
})
|
||||||
}, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode))
|
}, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode))
|
||||||
m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.ApplyDiffPatch)
|
m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.ApplyDiffPatch)
|
||||||
m.Group("/contents", func() {
|
m.Group("/contents", func() {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/convert"
|
"code.gitea.io/gitea/services/convert"
|
||||||
)
|
)
|
||||||
|
@ -102,3 +103,107 @@ func getNote(ctx *context.APIContext, identifier string) {
|
||||||
apiNote := api.Note{Message: string(note.Message), Commit: cmt}
|
apiNote := api.Note{Message: string(note.Message), Commit: cmt}
|
||||||
ctx.JSON(http.StatusOK, apiNote)
|
ctx.JSON(http.StatusOK, apiNote)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetNote Sets a note corresponding to a single commit from a repository
|
||||||
|
func SetNote(ctx *context.APIContext) {
|
||||||
|
// swagger:operation POST /repos/{owner}/{repo}/git/notes/{sha} repository repoSetNote
|
||||||
|
// ---
|
||||||
|
// summary: Set a note corresponding to a single commit from a repository
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: sha
|
||||||
|
// in: path
|
||||||
|
// description: a git ref or commit sha
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: body
|
||||||
|
// in: body
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/NoteOptions"
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/Note"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
sha := ctx.Params(":sha")
|
||||||
|
if !git.IsValidRefPattern(sha) {
|
||||||
|
ctx.Error(http.StatusUnprocessableEntity, "no valid ref or sha", fmt.Sprintf("no valid ref or sha: %s", sha))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form := web.GetForm(ctx).(*api.NoteOptions)
|
||||||
|
|
||||||
|
err := git.SetNote(ctx, ctx.Repo.GitRepo, sha, form.Message, ctx.Doer.Name, ctx.Doer.GetEmail())
|
||||||
|
if err != nil {
|
||||||
|
if git.IsErrNotExist(err) {
|
||||||
|
ctx.NotFound(sha)
|
||||||
|
} else {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "SetNote", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getNote(ctx, sha)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveNote Removes a note corresponding to a single commit from a repository
|
||||||
|
func RemoveNote(ctx *context.APIContext) {
|
||||||
|
// swagger:operation DELETE /repos/{owner}/{repo}/git/notes/{sha} repository repoRemoveNote
|
||||||
|
// ---
|
||||||
|
// summary: Removes a note corresponding to a single commit from a repository
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: sha
|
||||||
|
// in: path
|
||||||
|
// description: a git ref or commit sha
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "204":
|
||||||
|
// "$ref": "#/responses/empty"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
sha := ctx.Params(":sha")
|
||||||
|
if !git.IsValidRefPattern(sha) {
|
||||||
|
ctx.Error(http.StatusUnprocessableEntity, "no valid ref or sha", fmt.Sprintf("no valid ref or sha: %s", sha))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := git.RemoveNote(ctx, ctx.Repo.GitRepo, sha)
|
||||||
|
if err != nil {
|
||||||
|
if git.IsErrNotExist(err) {
|
||||||
|
ctx.NotFound(sha)
|
||||||
|
} else {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "RemoveNote", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
|
@ -231,4 +231,7 @@ type swaggerParameterBodies struct {
|
||||||
|
|
||||||
// in:body
|
// in:body
|
||||||
SetUserQuotaGroupsOptions api.SetUserQuotaGroupsOptions
|
SetUserQuotaGroupsOptions api.SetUserQuotaGroupsOptions
|
||||||
|
|
||||||
|
// in:body
|
||||||
|
NoteOptions api.NoteOptions
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,9 @@ import (
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
|
"code.gitea.io/gitea/services/forms"
|
||||||
"code.gitea.io/gitea/services/gitdiff"
|
"code.gitea.io/gitea/services/gitdiff"
|
||||||
git_service "code.gitea.io/gitea/services/repository"
|
git_service "code.gitea.io/gitea/services/repository"
|
||||||
)
|
)
|
||||||
|
@ -467,3 +469,29 @@ func processGitCommits(ctx *context.Context, gitCommits []*git.Commit) []*git_mo
|
||||||
}
|
}
|
||||||
return commits
|
return commits
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetCommitNotes(ctx *context.Context) {
|
||||||
|
form := web.GetForm(ctx).(*forms.CommitNotesForm)
|
||||||
|
|
||||||
|
commitID := ctx.Params(":sha")
|
||||||
|
|
||||||
|
err := git.SetNote(ctx, ctx.Repo.GitRepo, commitID, form.Notes, ctx.Doer.Name, ctx.Doer.GetEmail())
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("SetNote", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Redirect(fmt.Sprintf("%s/commit/%s", ctx.Repo.Repository.HTMLURL(), commitID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveCommitNotes(ctx *context.Context) {
|
||||||
|
commitID := ctx.Params(":sha")
|
||||||
|
|
||||||
|
err := git.RemoveNote(ctx, ctx.Repo.GitRepo, commitID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("RemoveNotes", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Redirect(fmt.Sprintf("%s/commit/%s", ctx.Repo.Repository.HTMLURL(), commitID))
|
||||||
|
}
|
||||||
|
|
|
@ -1559,6 +1559,10 @@ func registerRoutes(m *web.Route) {
|
||||||
m.Get("/graph", repo.Graph)
|
m.Get("/graph", repo.Graph)
|
||||||
m.Get("/commit/{sha:([a-f0-9]{4,64})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
|
m.Get("/commit/{sha:([a-f0-9]{4,64})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
|
||||||
m.Get("/commit/{sha:([a-f0-9]{4,64})$}/load-branches-and-tags", repo.LoadBranchesAndTags)
|
m.Get("/commit/{sha:([a-f0-9]{4,64})$}/load-branches-and-tags", repo.LoadBranchesAndTags)
|
||||||
|
m.Group("/commit/{sha:([a-f0-9]{4,64})$}/notes", func() {
|
||||||
|
m.Post("", web.Bind(forms.CommitNotesForm{}), repo.SetCommitNotes)
|
||||||
|
m.Post("/remove", repo.RemoveCommitNotes)
|
||||||
|
}, reqSignIn, reqRepoCodeWriter)
|
||||||
m.Get("/cherry-pick/{sha:([a-f0-9]{4,64})$}", repo.SetEditorconfigIfExists, repo.CherryPick)
|
m.Get("/cherry-pick/{sha:([a-f0-9]{4,64})$}", repo.SetEditorconfigIfExists, repo.CherryPick)
|
||||||
}, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)
|
}, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)
|
||||||
|
|
||||||
|
|
|
@ -749,3 +749,7 @@ func (f *DeadlineForm) Validate(req *http.Request, errs binding.Errors) binding.
|
||||||
ctx := context.GetValidateContext(req)
|
ctx := context.GetValidateContext(req)
|
||||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CommitNotesForm struct {
|
||||||
|
Notes string
|
||||||
|
}
|
||||||
|
|
|
@ -275,10 +275,61 @@
|
||||||
<strong>{{.NoteCommit.Author.Name}}</strong>
|
<strong>{{.NoteCommit.Author.Name}}</strong>
|
||||||
{{end}}
|
{{end}}
|
||||||
<span class="text grey" id="note-authored-time">{{DateUtils.TimeSince .NoteCommit.Author.When}}</span>
|
<span class="text grey" id="note-authored-time">{{DateUtils.TimeSince .NoteCommit.Author.When}}</span>
|
||||||
|
{{if or ($.Permission.CanWrite $.UnitTypeCode) (not $.Repository.IsArchived) (not .IsDeleted)}}
|
||||||
|
<div class="ui right">
|
||||||
|
<button id="commit-notes-edit-button" class="ui tiny button yellow" data-modal="#delete-note-modal">{{ctx.Locale.Tr "edit"}}</button>
|
||||||
|
<button class="ui tiny button red show-modal" data-modal="#delete-note-modal">{{ctx.Locale.Tr "remove"}}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui bottom attached info segment git-notes">
|
<div class="ui small modal" id="delete-note-modal">
|
||||||
|
<div class="header">
|
||||||
|
{{ctx.Locale.Tr "repo.diff.git-notes.remove-header"}}
|
||||||
|
</div>
|
||||||
|
<p>{{ctx.Locale.Tr "repo.diff.git-notes.remove-body"}}</p>
|
||||||
|
<div class="content">
|
||||||
|
<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 primary button red" href="{{.Link}}/notes/remove">{{ctx.Locale.Tr "remove"}}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div id="commit-notes-display-area" class="ui bottom attached info segment git-notes">
|
||||||
<pre class="commit-body">{{.NoteRendered | SanitizeHTML}}</pre>
|
<pre class="commit-body">{{.NoteRendered | SanitizeHTML}}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
{{if and ($.Permission.CanWrite $.UnitTypeCode) (not $.Repository.IsArchived) (not .IsDeleted)}}
|
||||||
|
<div id="commit-notes-edit-area" class="ui bottom attached info segment git-notes tw-hidden">
|
||||||
|
<form class="ui form" action="{{.Link}}/notes" method="post">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<textarea name="notes">{{.NoteRendered | SanitizeHTML}}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<button id="notes-save-button" class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{else if and ($.Permission.CanWrite $.UnitTypeCode) (not $.Repository.IsArchived) (not .IsDeleted)}}
|
||||||
|
<button id="commit-notes-add-button" class="ui primary button green tw-mt-3">{{ctx.Locale.Tr "repo.diff.git-notes.add"}}</button>
|
||||||
|
<div id="commit-notes-add-area" class="ui tw-mt-3 segment tw-hidden">
|
||||||
|
<form class="ui form" action="{{.Link}}/notes" method="post">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<textarea name="notes"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<button class="ui primary button">{{ctx.Locale.Tr "add"}}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{template "repo/diff/box" .}}
|
{{template "repo/diff/box" .}}
|
||||||
</div>
|
</div>
|
||||||
|
|
107
templates/swagger/v1_json.tmpl
generated
107
templates/swagger/v1_json.tmpl
generated
|
@ -7375,6 +7375,101 @@
|
||||||
"$ref": "#/responses/validationError"
|
"$ref": "#/responses/validationError"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Set a note corresponding to a single commit from a repository",
|
||||||
|
"operationId": "repoSetNote",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repo",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "a git ref or commit sha",
|
||||||
|
"name": "sha",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/NoteOptions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/Note"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/notFound"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"$ref": "#/responses/validationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Removes a note corresponding to a single commit from a repository",
|
||||||
|
"operationId": "repoRemoveNote",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repo",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "a git ref or commit sha",
|
||||||
|
"name": "sha",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"$ref": "#/responses/empty"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/notFound"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"$ref": "#/responses/validationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/repos/{owner}/{repo}/git/refs": {
|
"/repos/{owner}/{repo}/git/refs": {
|
||||||
|
@ -24601,6 +24696,16 @@
|
||||||
},
|
},
|
||||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
},
|
},
|
||||||
|
"NoteOptions": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Message"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
|
},
|
||||||
"NotificationCount": {
|
"NotificationCount": {
|
||||||
"description": "NotificationCount number of unread notifications",
|
"description": "NotificationCount number of unread notifications",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -28350,7 +28455,7 @@
|
||||||
"parameterBodies": {
|
"parameterBodies": {
|
||||||
"description": "parameterBodies",
|
"description": "parameterBodies",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/SetUserQuotaGroupsOptions"
|
"$ref": "#/definitions/NoteOptions"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"quotaExceeded": {
|
"quotaExceeded": {
|
||||||
|
|
30
tests/e2e/git-notes.test.e2e.ts
Normal file
30
tests/e2e/git-notes.test.e2e.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// @ts-check
|
||||||
|
import {test, expect} from '@playwright/test';
|
||||||
|
import {login_user, load_logged_in_context} from './utils_e2e.ts';
|
||||||
|
|
||||||
|
test.beforeAll(async ({browser}, workerInfo) => {
|
||||||
|
await login_user(browser, workerInfo, 'user2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Change git note', async ({browser}, workerInfo) => {
|
||||||
|
const context = await load_logged_in_context(browser, workerInfo, 'user2');
|
||||||
|
const page = await context.newPage();
|
||||||
|
let response = await page.goto('/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d');
|
||||||
|
expect(response?.status()).toBe(200);
|
||||||
|
|
||||||
|
await page.locator('#commit-notes-edit-button').click();
|
||||||
|
|
||||||
|
let textarea = page.locator('textarea[name="notes"]');
|
||||||
|
await expect(textarea).toBeVisible();
|
||||||
|
await textarea.fill('This is a new note');
|
||||||
|
|
||||||
|
await page.locator('#notes-save-button').click();
|
||||||
|
|
||||||
|
expect(response?.status()).toBe(200);
|
||||||
|
|
||||||
|
response = await page.goto('/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d');
|
||||||
|
expect(response?.status()).toBe(200);
|
||||||
|
|
||||||
|
textarea = page.locator('textarea[name="notes"]');
|
||||||
|
await expect(textarea).toHaveText('This is a new note');
|
||||||
|
});
|
|
@ -4,11 +4,13 @@
|
||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
@ -16,7 +18,7 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAPIReposGitNotes(t *testing.T) {
|
func TestAPIReposGetGitNotes(t *testing.T) {
|
||||||
onGiteaRun(t, func(*testing.T, *url.URL) {
|
onGiteaRun(t, func(*testing.T, *url.URL) {
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
// Login as User2.
|
// Login as User2.
|
||||||
|
@ -44,3 +46,53 @@ func TestAPIReposGitNotes(t *testing.T) {
|
||||||
assert.NotNil(t, apiData.Commit.RepoCommit.Verification)
|
assert.NotNil(t, apiData.Commit.RepoCommit.Verification)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAPIReposSetGitNotes(t *testing.T) {
|
||||||
|
onGiteaRun(t, func(*testing.T, *url.URL) {
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
|
||||||
|
session := loginUser(t, user.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
|
||||||
|
req := NewRequestf(t, "GET", "/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName())
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
var apiData api.Note
|
||||||
|
DecodeJSON(t, resp, &apiData)
|
||||||
|
assert.Equal(t, "This is a test note\n", apiData.Message)
|
||||||
|
|
||||||
|
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName()), &api.NoteOptions{
|
||||||
|
Message: "This is a new note",
|
||||||
|
}).AddTokenAuth(token)
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
DecodeJSON(t, resp, &apiData)
|
||||||
|
assert.Equal(t, "This is a new note\n", apiData.Message)
|
||||||
|
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName())
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
DecodeJSON(t, resp, &apiData)
|
||||||
|
assert.Equal(t, "This is a new note\n", apiData.Message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIReposDeleteGitNotes(t *testing.T) {
|
||||||
|
onGiteaRun(t, func(*testing.T, *url.URL) {
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
|
||||||
|
session := loginUser(t, user.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
|
||||||
|
req := NewRequestf(t, "GET", "/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName())
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
var apiData api.Note
|
||||||
|
DecodeJSON(t, resp, &apiData)
|
||||||
|
assert.Equal(t, "This is a test note\n", apiData.Message)
|
||||||
|
|
||||||
|
req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName()).AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d", repo.FullName())
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
44
tests/integration/repo_git_note_test.go
Normal file
44
tests/integration/repo_git_note_test.go
Normal file
|
@ -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(), "<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")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ import {
|
||||||
initRepoPullRequestAllowMaintainerEdit,
|
initRepoPullRequestAllowMaintainerEdit,
|
||||||
initRepoPullRequestReview, initRepoIssueSidebarList, initArchivedLabelHandler,
|
initRepoPullRequestReview, initRepoIssueSidebarList, initArchivedLabelHandler,
|
||||||
} from './features/repo-issue.js';
|
} from './features/repo-issue.js';
|
||||||
import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.js';
|
import {initRepoEllipsisButton, initCommitStatuses, initCommitNotes} from './features/repo-commit.js';
|
||||||
import {
|
import {
|
||||||
initFootLanguageMenu,
|
initFootLanguageMenu,
|
||||||
initGlobalButtonClickOnEnter,
|
initGlobalButtonClickOnEnter,
|
||||||
|
@ -179,6 +179,7 @@ onDomReady(() => {
|
||||||
initRepoMilestoneEditor();
|
initRepoMilestoneEditor();
|
||||||
|
|
||||||
initCommitStatuses();
|
initCommitStatuses();
|
||||||
|
initCommitNotes();
|
||||||
initCaptcha();
|
initCaptcha();
|
||||||
|
|
||||||
initUserAuthOauth2();
|
initUserAuthOauth2();
|
||||||
|
|
Loading…
Reference in a new issue