diff --git a/integrations/api_repo_git_notes_test.go b/integrations/api_repo_git_notes_test.go new file mode 100644 index 0000000000000..6eae5e970d63b --- /dev/null +++ b/integrations/api_repo_git_notes_test.go @@ -0,0 +1,39 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integrations + +import ( + "net/http" + "net/url" + "testing" + + "code.gitea.io/gitea/models" + api "code.gitea.io/gitea/modules/structs" + "github.com/stretchr/testify/assert" +) + +func TestAPIReposGitNotes(t *testing.T) { + onGiteaRun(t, func(*testing.T, *url.URL) { + user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) + // Login as User2. + session := loginUser(t, user.Name) + token := getTokenForLoggedInUser(t, session) + + // check invalid requests + req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/notes/12345?token=%s", user.Name, token) + session.MakeRequest(t, req, http.StatusNotFound) + + req = NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/notes/..?token=%s", user.Name, token) + session.MakeRequest(t, req, http.StatusUnprocessableEntity) + + // check valid request + req = NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d?token=%s", user.Name, token) + resp := session.MakeRequest(t, req, http.StatusOK) + + var apiData api.Note + DecodeJSON(t, resp, &apiData) + assert.Equal(t, "This is a test note\n", apiData.Message) + }) +} diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/objects/3f/a2f829675543ecfc16b2891aebe8bf0608a8f4 b/integrations/gitea-repositories-meta/user2/repo1.git/objects/3f/a2f829675543ecfc16b2891aebe8bf0608a8f4 new file mode 100644 index 0000000000000..892c6bffe9e2b Binary files /dev/null and b/integrations/gitea-repositories-meta/user2/repo1.git/objects/3f/a2f829675543ecfc16b2891aebe8bf0608a8f4 differ diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/objects/d4/a1a6dcf7bd42891f264d484e80dac7e66b5410 b/integrations/gitea-repositories-meta/user2/repo1.git/objects/d4/a1a6dcf7bd42891f264d484e80dac7e66b5410 new file mode 100644 index 0000000000000..d7ef93c616fe7 Binary files /dev/null and b/integrations/gitea-repositories-meta/user2/repo1.git/objects/d4/a1a6dcf7bd42891f264d484e80dac7e66b5410 differ diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/objects/d7/bd5b8cfb680f460e37b6fd7cf74c284e059118 b/integrations/gitea-repositories-meta/user2/repo1.git/objects/d7/bd5b8cfb680f460e37b6fd7cf74c284e059118 new file mode 100644 index 0000000000000..6039ff661955c Binary files /dev/null and b/integrations/gitea-repositories-meta/user2/repo1.git/objects/d7/bd5b8cfb680f460e37b6fd7cf74c284e059118 differ diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/refs/notes/commits b/integrations/gitea-repositories-meta/user2/repo1.git/refs/notes/commits new file mode 100644 index 0000000000000..6f837536fc995 --- /dev/null +++ b/integrations/gitea-repositories-meta/user2/repo1.git/refs/notes/commits @@ -0,0 +1 @@ +3fa2f829675543ecfc16b2891aebe8bf0608a8f4 diff --git a/modules/git/notes_gogit.go b/modules/git/notes_gogit.go index 702754069bd3a..9da45ca65c25d 100644 --- a/modules/git/notes_gogit.go +++ b/modules/git/notes_gogit.go @@ -10,19 +10,24 @@ import ( "context" "io/ioutil" + "code.gitea.io/gitea/modules/log" + "github.com/go-git/go-git/v5/plumbing/object" ) // GetNote retrieves the git-notes data for a given commit. func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) error { + log.Trace("Searching for git note corresponding to the commit %q in the repository %q", commitID, repo.Path) notes, err := repo.GetCommit(NotesRef) if err != nil { + log.Error("Unable to get commit from ref %q. Error: %v", NotesRef, err) return err } remainingCommitID := commitID path := "" currentTree := notes.Tree.gogitTree + log.Trace("Found tree with ID %q while searching for git note corresponding to the commit %q", currentTree.Entries[0].Name, commitID) var file *object.File for len(remainingCommitID) > 2 { file, err = currentTree.File(remainingCommitID) @@ -39,6 +44,7 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) if err == object.ErrDirectoryNotFound { return ErrNotExist{ID: remainingCommitID, RelPath: path} } + log.Error("Unable to find git note corresponding to the commit %q. Error: %v", commitID, err) return err } } @@ -46,12 +52,14 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) blob := file.Blob dataRc, err := blob.Reader() if err != nil { + log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err) return err } defer dataRc.Close() d, err := ioutil.ReadAll(dataRc) if err != nil { + log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err) return err } note.Message = d @@ -68,6 +76,7 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) lastCommits, err := GetLastCommitForPaths(ctx, commitNode, "", []string{path}) if err != nil { + log.Error("Unable to get the commit for the path %q. Error: %v", path, err) return err } note.Commit = convertCommit(lastCommits[path]) diff --git a/modules/git/notes_nogogit.go b/modules/git/notes_nogogit.go index 267087a86fafa..697f998288f5d 100644 --- a/modules/git/notes_nogogit.go +++ b/modules/git/notes_nogogit.go @@ -10,20 +10,26 @@ import ( "context" "io/ioutil" "strings" + + "code.gitea.io/gitea/modules/log" ) // GetNote retrieves the git-notes data for a given commit. func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) error { + log.Trace("Searching for git note corresponding to the commit %q in the repository %q", commitID, repo.Path) notes, err := repo.GetCommit(NotesRef) if err != nil { + log.Error("Unable to get commit from ref %q. Error: %v", NotesRef, err) return err } path := "" tree := ¬es.Tree + log.Trace("Found tree with ID %q while searching for git note corresponding to the commit %q", tree.ID, commitID) var entry *TreeEntry + originalCommitID := commitID for len(commitID) > 2 { entry, err = tree.GetTreeEntryByPath(commitID) if err == nil { @@ -36,12 +42,15 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) commitID = commitID[2:] } if err != nil { + log.Error("Unable to find git note corresponding to the commit %q. Error: %v", originalCommitID, err) return err } } - dataRc, err := entry.Blob().DataAsync() + blob := entry.Blob() + dataRc, err := blob.DataAsync() if err != nil { + log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err) return err } closed := false @@ -52,6 +61,7 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) }() d, err := ioutil.ReadAll(dataRc) if err != nil { + log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err) return err } _ = dataRc.Close() @@ -66,6 +76,7 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) lastCommits, err := GetLastCommitForPaths(ctx, notes, treePath, []string{path}) if err != nil { + log.Error("Unable to get the commit for the path %q. Error: %v", treePath, err) return err } note.Commit = lastCommits[path] diff --git a/modules/structs/repo_note.go b/modules/structs/repo_note.go new file mode 100644 index 0000000000000..bddc945a5c709 --- /dev/null +++ b/modules/structs/repo_note.go @@ -0,0 +1,11 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package structs + +// Note contains information related to a git note +type Note struct { + Message string `json:"message"` + Commit *Commit `json:"commit"` +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index b2202254daa97..f05647134637e 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -953,6 +953,7 @@ func Routes() *web.Route { m.Get("/trees/{sha}", context.RepoRefForAPI, repo.GetTree) m.Get("/blobs/{sha}", context.RepoRefForAPI, repo.GetBlob) m.Get("/tags/{sha}", context.RepoRefForAPI, repo.GetAnnotatedTag) + m.Get("/notes/{sha}", repo.GetNote) }, reqRepoReader(models.UnitTypeCode)) m.Group("/contents", func() { m.Get("", repo.GetContentsList) diff --git a/routers/api/v1/repo/notes.go b/routers/api/v1/repo/notes.go new file mode 100644 index 0000000000000..a5f9512983f9e --- /dev/null +++ b/routers/api/v1/repo/notes.go @@ -0,0 +1,82 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package repo + +import ( + "fmt" + "net/http" + + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" + "code.gitea.io/gitea/modules/git" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/validation" +) + +// GetNote Get a note corresponding to a single commit from a repository +func GetNote(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/git/notes/{sha} repository repoGetNote + // --- + // summary: Get 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: + // "200": + // "$ref": "#/responses/Note" + // "422": + // "$ref": "#/responses/validationError" + // "404": + // "$ref": "#/responses/notFound" + + sha := ctx.Params(":sha") + if (validation.GitRefNamePatternInvalid.MatchString(sha) || !validation.CheckGitRefAdditionalRulesValid(sha)) && !git.SHAPattern.MatchString(sha) { + ctx.Error(http.StatusUnprocessableEntity, "no valid ref or sha", fmt.Sprintf("no valid ref or sha: %s", sha)) + return + } + getNote(ctx, sha) +} + +func getNote(ctx *context.APIContext, identifier string) { + gitRepo, err := git.OpenRepository(ctx.Repo.Repository.RepoPath()) + if err != nil { + ctx.Error(http.StatusInternalServerError, "OpenRepository", err) + return + } + defer gitRepo.Close() + var note git.Note + err = git.GetNote(ctx, gitRepo, identifier, ¬e) + if err != nil { + if git.IsErrNotExist(err) { + ctx.NotFound(identifier) + return + } + ctx.Error(http.StatusInternalServerError, "GetNote", err) + return + } + + cmt, err := convert.ToCommit(ctx.Repo.Repository, note.Commit, nil) + if err != nil { + ctx.Error(http.StatusInternalServerError, "ToCommit", err) + return + } + apiNote := api.Note{Message: string(note.Message), Commit: cmt} + ctx.JSON(http.StatusOK, apiNote) +} diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index d539bcb9feabb..ed5fe5169ee31 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -254,6 +254,13 @@ type swaggerCommitList struct { Body []api.Commit `json:"body"` } +// Note +// swagger:response Note +type swaggerNote struct { + // in: body + Body api.Note `json:"body"` +} + // EmptyRepository // swagger:response EmptyRepository type swaggerEmptyRepository struct { diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index a1d92abec7a86..d23d09bcfdad0 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -3569,6 +3569,52 @@ } } }, + "/repos/{owner}/{repo}/git/notes/{sha}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get a note corresponding to a single commit from a repository", + "operationId": "repoGetNote", + "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": { + "200": { + "$ref": "#/responses/Note" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, "/repos/{owner}/{repo}/git/refs": { "get": { "produces": [ @@ -15453,6 +15499,20 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "Note": { + "description": "Note contains information related to a git note", + "type": "object", + "properties": { + "commit": { + "$ref": "#/definitions/Commit" + }, + "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", @@ -17412,6 +17472,12 @@ } } }, + "Note": { + "description": "Note", + "schema": { + "$ref": "#/definitions/Note" + } + }, "NotificationCount": { "description": "Number of unread notifications", "schema": {