aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--integrations/api_comment_test.go22
-rw-r--r--models/issue_comment.go41
-rw-r--r--modules/convert/issue_comment.go143
-rw-r--r--modules/references/references.go11
-rw-r--r--modules/structs/issue_comment.go45
-rw-r--r--routers/api/v1/api.go1
-rw-r--r--routers/api/v1/repo/issue_comment.go111
-rw-r--r--routers/api/v1/swagger/issue.go7
-rw-r--r--templates/swagger/v1_json.tmpl196
9 files changed, 577 insertions, 0 deletions
diff --git a/integrations/api_comment_test.go b/integrations/api_comment_test.go
index 0c3ac2ae5b..4c4c6308ee 100644
--- a/integrations/api_comment_test.go
+++ b/integrations/api_comment_test.go
@@ -180,3 +180,25 @@ func TestAPIDeleteComment(t *testing.T) {
unittest.AssertNotExistsBean(t, &models.Comment{ID: comment.ID})
}
+
+func TestAPIListIssueTimeline(t *testing.T) {
+ defer prepareTestEnv(t)()
+
+ // load comment
+ issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 1}).(*models.Issue)
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository)
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
+
+ // make request
+ session := loginUser(t, repoOwner.Name)
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/%d/timeline",
+ repoOwner.Name, repo.Name, issue.Index)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ // check if lens of list returned by API and
+ // lists extracted directly from DB are the same
+ var comments []*api.TimelineComment
+ DecodeJSON(t, resp, &comments)
+ expectedCount := unittest.GetCount(t, &models.Comment{IssueID: issue.ID})
+ assert.EqualValues(t, expectedCount, len(comments))
+}
diff --git a/models/issue_comment.go b/models/issue_comment.go
index 360a212a23..9a6f4247b0 100644
--- a/models/issue_comment.go
+++ b/models/issue_comment.go
@@ -110,6 +110,47 @@ const (
CommentTypeChangeIssueRef
)
+var commentStrings = []string{
+ "comment",
+ "reopen",
+ "close",
+ "issue_ref",
+ "commit_ref",
+ "comment_ref",
+ "pull_ref",
+ "label",
+ "milestone",
+ "assignees",
+ "change_title",
+ "delete_branch",
+ "start_tracking",
+ "stop_tracking",
+ "add_time_manual",
+ "cancel_tracking",
+ "added_deadline",
+ "modified_deadline",
+ "removed_deadline",
+ "add_dependency",
+ "remove_dependency",
+ "code",
+ "review",
+ "lock",
+ "unlock",
+ "change_target_branch",
+ "delete_time_manual",
+ "review_request",
+ "merge_pull",
+ "pull_push",
+ "project",
+ "project_board",
+ "dismiss_review",
+ "change_issue_ref",
+}
+
+func (t CommentType) String() string {
+ return commentStrings[t]
+}
+
// RoleDescriptor defines comment tag type
type RoleDescriptor int
diff --git a/modules/convert/issue_comment.go b/modules/convert/issue_comment.go
index 1610b9f0d8..caba2b506e 100644
--- a/modules/convert/issue_comment.go
+++ b/modules/convert/issue_comment.go
@@ -6,6 +6,9 @@ package convert
import (
"code.gitea.io/gitea/models"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
)
@@ -22,3 +25,143 @@ func ToComment(c *models.Comment) *api.Comment {
Updated: c.UpdatedUnix.AsTime(),
}
}
+
+// ToTimelineComment converts a models.Comment to the api.TimelineComment format
+func ToTimelineComment(c *models.Comment, doer *user_model.User) *api.TimelineComment {
+ err := c.LoadMilestone()
+ if err != nil {
+ log.Error("LoadMilestone: %v", err)
+ return nil
+ }
+
+ err = c.LoadAssigneeUserAndTeam()
+ if err != nil {
+ log.Error("LoadAssigneeUserAndTeam: %v", err)
+ return nil
+ }
+
+ err = c.LoadResolveDoer()
+ if err != nil {
+ log.Error("LoadResolveDoer: %v", err)
+ return nil
+ }
+
+ err = c.LoadDepIssueDetails()
+ if err != nil {
+ log.Error("LoadDepIssueDetails: %v", err)
+ return nil
+ }
+
+ err = c.LoadTime()
+ if err != nil {
+ log.Error("LoadTime: %v", err)
+ return nil
+ }
+
+ err = c.LoadLabel()
+ if err != nil {
+ log.Error("LoadLabel: %v", err)
+ return nil
+ }
+
+ comment := &api.TimelineComment{
+ ID: c.ID,
+ Type: c.Type.String(),
+ Poster: ToUser(c.Poster, nil),
+ HTMLURL: c.HTMLURL(),
+ IssueURL: c.IssueURL(),
+ PRURL: c.PRURL(),
+ Body: c.Content,
+ Created: c.CreatedUnix.AsTime(),
+ Updated: c.UpdatedUnix.AsTime(),
+
+ OldProjectID: c.OldProjectID,
+ ProjectID: c.ProjectID,
+
+ OldTitle: c.OldTitle,
+ NewTitle: c.NewTitle,
+
+ OldRef: c.OldRef,
+ NewRef: c.NewRef,
+
+ RefAction: c.RefAction.String(),
+ RefCommitSHA: c.CommitSHA,
+
+ ReviewID: c.ReviewID,
+
+ RemovedAssignee: c.RemovedAssignee,
+ }
+
+ if c.OldMilestone != nil {
+ comment.OldMilestone = ToAPIMilestone(c.OldMilestone)
+ }
+ if c.Milestone != nil {
+ comment.Milestone = ToAPIMilestone(c.Milestone)
+ }
+
+ if c.Time != nil {
+ comment.TrackedTime = ToTrackedTime(c.Time)
+ }
+
+ if c.RefIssueID != 0 {
+ issue, err := models.GetIssueByID(c.RefIssueID)
+ if err != nil {
+ log.Error("GetIssueByID(%d): %v", c.RefIssueID, err)
+ return nil
+ }
+ comment.RefIssue = ToAPIIssue(issue)
+ }
+
+ if c.RefCommentID != 0 {
+ com, err := models.GetCommentByID(c.RefCommentID)
+ if err != nil {
+ log.Error("GetCommentByID(%d): %v", c.RefCommentID, err)
+ return nil
+ }
+ err = com.LoadPoster()
+ if err != nil {
+ log.Error("LoadPoster: %v", err)
+ return nil
+ }
+ comment.RefComment = ToComment(com)
+ }
+
+ if c.Label != nil {
+ var org *user_model.User
+ var repo *repo_model.Repository
+ if c.Label.BelongsToOrg() {
+ var err error
+ org, err = user_model.GetUserByID(c.Label.OrgID)
+ if err != nil {
+ log.Error("GetUserByID(%d): %v", c.Label.OrgID, err)
+ return nil
+ }
+ }
+ if c.Label.BelongsToRepo() {
+ var err error
+ repo, err = repo_model.GetRepositoryByID(c.Label.RepoID)
+ if err != nil {
+ log.Error("GetRepositoryByID(%d): %v", c.Label.RepoID, err)
+ return nil
+ }
+ }
+ comment.Label = ToLabel(c.Label, repo, org)
+ }
+
+ if c.Assignee != nil {
+ comment.Assignee = ToUser(c.Assignee, nil)
+ }
+ if c.AssigneeTeam != nil {
+ comment.AssigneeTeam = ToTeam(c.AssigneeTeam)
+ }
+
+ if c.ResolveDoer != nil {
+ comment.ResolveDoer = ToUser(c.ResolveDoer, nil)
+ }
+
+ if c.DependentIssue != nil {
+ comment.DependentIssue = ToAPIIssue(c.DependentIssue)
+ }
+
+ return comment
+}
diff --git a/modules/references/references.go b/modules/references/references.go
index cfc01cd4c0..74837b8553 100644
--- a/modules/references/references.go
+++ b/modules/references/references.go
@@ -49,6 +49,13 @@ var (
giteaHostInit sync.Once
giteaHost string
giteaIssuePullPattern *regexp.Regexp
+
+ actionStrings = []string{
+ "none",
+ "closes",
+ "reopens",
+ "neutered",
+ }
)
// XRefAction represents the kind of effect a cross reference has once is resolved
@@ -65,6 +72,10 @@ const (
XRefActionNeutered // 3
)
+func (a XRefAction) String() string {
+ return actionStrings[a]
+}
+
// IssueReference contains an unverified cross-reference to a local issue or pull request
type IssueReference struct {
Index int64
diff --git a/modules/structs/issue_comment.go b/modules/structs/issue_comment.go
index 0c8ac20017..e13ec05d01 100644
--- a/modules/structs/issue_comment.go
+++ b/modules/structs/issue_comment.go
@@ -35,3 +35,48 @@ type EditIssueCommentOption struct {
// required: true
Body string `json:"body" binding:"Required"`
}
+
+// TimelineComment represents a timeline comment (comment of any type) on a commit or issue
+type TimelineComment struct {
+ ID int64 `json:"id"`
+ Type string `json:"type"`
+
+ HTMLURL string `json:"html_url"`
+ PRURL string `json:"pull_request_url"`
+ IssueURL string `json:"issue_url"`
+ Poster *User `json:"user"`
+ Body string `json:"body"`
+ // swagger:strfmt date-time
+ Created time.Time `json:"created_at"`
+ // swagger:strfmt date-time
+ Updated time.Time `json:"updated_at"`
+
+ OldProjectID int64 `json:"old_project_id"`
+ ProjectID int64 `json:"project_id"`
+ OldMilestone *Milestone `json:"old_milestone"`
+ Milestone *Milestone `json:"milestone"`
+ TrackedTime *TrackedTime `json:"tracked_time"`
+ OldTitle string `json:"old_title"`
+ NewTitle string `json:"new_title"`
+ OldRef string `json:"old_ref"`
+ NewRef string `json:"new_ref"`
+
+ RefIssue *Issue `json:"ref_issue"`
+ RefComment *Comment `json:"ref_comment"`
+ RefAction string `json:"ref_action"`
+ // commit SHA where issue/PR was referenced
+ RefCommitSHA string `json:"ref_commit_sha"`
+
+ ReviewID int64 `json:"review_id"`
+
+ Label *Label `json:"label"`
+
+ Assignee *User `json:"assignee"`
+ AssigneeTeam *Team `json:"assignee_team"`
+ // whether the assignees were removed or added
+ RemovedAssignee bool `json:"removed_assignee"`
+
+ ResolveDoer *User `json:"resolve_doer"`
+
+ DependentIssue *Issue `json:"dependent_issue"`
+}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index c587907d4b..7a2347650a 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -842,6 +842,7 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
m.Combo("/{id}", reqToken()).Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueCommentDeprecated).
Delete(repo.DeleteIssueCommentDeprecated)
})
+ m.Get("/timeline", repo.ListIssueCommentsAndTimeline)
m.Group("/labels", func() {
m.Combo("").Get(repo.ListIssueLabels).
Post(reqToken(), bind(api.IssueLabelsOption{}), repo.AddIssueLabels).
diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go
index 13e7de46b1..b929cec373 100644
--- a/routers/api/v1/repo/issue_comment.go
+++ b/routers/api/v1/repo/issue_comment.go
@@ -10,6 +10,8 @@ import (
"net/http"
"code.gitea.io/gitea/models"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
@@ -102,6 +104,115 @@ func ListIssueComments(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, &apiComments)
}
+// ListIssueCommentsAndTimeline list all the comments and events of an issue
+func ListIssueCommentsAndTimeline(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/issues/{index}/timeline issue issueGetCommentsAndTimeline
+ // ---
+ // summary: List all comments and events on an issue
+ // 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: index
+ // in: path
+ // description: index of the issue
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: since
+ // in: query
+ // description: if provided, only comments updated since the specified time are returned.
+ // type: string
+ // format: date-time
+ // - name: page
+ // in: query
+ // description: page number of results to return (1-based)
+ // type: integer
+ // - name: limit
+ // in: query
+ // description: page size of results
+ // type: integer
+ // - name: before
+ // in: query
+ // description: if provided, only comments updated before the provided time are returned.
+ // type: string
+ // format: date-time
+ // responses:
+ // "200":
+ // "$ref": "#/responses/TimelineList"
+
+ before, since, err := utils.GetQueryBeforeSince(ctx)
+ if err != nil {
+ ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
+ return
+ }
+ issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err)
+ return
+ }
+ issue.Repo = ctx.Repo.Repository
+
+ opts := &models.FindCommentsOptions{
+ ListOptions: utils.GetListOptions(ctx),
+ IssueID: issue.ID,
+ Since: since,
+ Before: before,
+ Type: models.CommentTypeUnknown,
+ }
+
+ comments, err := models.FindComments(opts)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "FindComments", err)
+ return
+ }
+
+ if err := models.CommentList(comments).LoadPosters(); err != nil {
+ ctx.Error(http.StatusInternalServerError, "LoadPosters", err)
+ return
+ }
+
+ var apiComments []*api.TimelineComment
+ for _, comment := range comments {
+ if comment.Type != models.CommentTypeCode && isXRefCommentAccessible(ctx.User, comment, issue.RepoID) {
+ comment.Issue = issue
+ apiComments = append(apiComments, convert.ToTimelineComment(comment, ctx.User))
+ }
+ }
+
+ ctx.SetTotalCountHeader(int64(len(apiComments)))
+ ctx.JSON(http.StatusOK, &apiComments)
+}
+
+func isXRefCommentAccessible(user *user_model.User, c *models.Comment, issueRepoID int64) bool {
+ // Remove comments that the user has no permissions to see
+ if models.CommentTypeIsRef(c.Type) && c.RefRepoID != issueRepoID && c.RefRepoID != 0 {
+ var err error
+ // Set RefRepo for description in template
+ c.RefRepo, err = repo_model.GetRepositoryByID(c.RefRepoID)
+ if err != nil {
+ return false
+ }
+ perm, err := models.GetUserRepoPermission(c.RefRepo, user)
+ if err != nil {
+ return false
+ }
+ if !perm.CanReadIssuesOrPulls(c.RefIsPull) {
+ return false
+ }
+ }
+ return true
+}
+
// ListRepoIssueComments returns all issue-comments for a repo
func ListRepoIssueComments(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues/comments issue issueGetRepoComments
diff --git a/routers/api/v1/swagger/issue.go b/routers/api/v1/swagger/issue.go
index 0f2f572020..09e7077b20 100644
--- a/routers/api/v1/swagger/issue.go
+++ b/routers/api/v1/swagger/issue.go
@@ -36,6 +36,13 @@ type swaggerResponseCommentList struct {
Body []api.Comment `json:"body"`
}
+// TimelineList
+// swagger:response TimelineList
+type swaggerResponseTimelineList struct {
+ // in:body
+ Body []api.TimelineComment `json:"body"`
+}
+
// Label
// swagger:response Label
type swaggerResponseLabel struct {
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 992cdf5bda..9438c41a29 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -6057,6 +6057,73 @@
}
}
},
+ "/repos/{owner}/{repo}/issues/{index}/timeline": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "issue"
+ ],
+ "summary": "List all comments and events on an issue",
+ "operationId": "issueGetCommentsAndTimeline",
+ "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": "integer",
+ "format": "int64",
+ "description": "index of the issue",
+ "name": "index",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "format": "date-time",
+ "description": "if provided, only comments updated since the specified time are returned.",
+ "name": "since",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "page number of results to return (1-based)",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "page size of results",
+ "name": "limit",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "format": "date-time",
+ "description": "if provided, only comments updated before the provided time are returned.",
+ "name": "before",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/TimelineList"
+ }
+ }
+ }
+ },
"/repos/{owner}/{repo}/issues/{index}/times": {
"get": {
"produces": [
@@ -17396,6 +17463,126 @@
"format": "int64",
"x-go-package": "code.gitea.io/gitea/modules/timeutil"
},
+ "TimelineComment": {
+ "description": "TimelineComment represents a timeline comment (comment of any type) on a commit or issue",
+ "type": "object",
+ "properties": {
+ "assignee": {
+ "$ref": "#/definitions/User"
+ },
+ "assignee_team": {
+ "$ref": "#/definitions/Team"
+ },
+ "body": {
+ "type": "string",
+ "x-go-name": "Body"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time",
+ "x-go-name": "Created"
+ },
+ "dependent_issue": {
+ "$ref": "#/definitions/Issue"
+ },
+ "html_url": {
+ "type": "string",
+ "x-go-name": "HTMLURL"
+ },
+ "id": {
+ "type": "integer",
+ "format": "int64",
+ "x-go-name": "ID"
+ },
+ "issue_url": {
+ "type": "string",
+ "x-go-name": "IssueURL"
+ },
+ "label": {
+ "$ref": "#/definitions/Label"
+ },
+ "milestone": {
+ "$ref": "#/definitions/Milestone"
+ },
+ "new_ref": {
+ "type": "string",
+ "x-go-name": "NewRef"
+ },
+ "new_title": {
+ "type": "string",
+ "x-go-name": "NewTitle"
+ },
+ "old_milestone": {
+ "$ref": "#/definitions/Milestone"
+ },
+ "old_project_id": {
+ "type": "integer",
+ "format": "int64",
+ "x-go-name": "OldProjectID"
+ },
+ "old_ref": {
+ "type": "string",
+ "x-go-name": "OldRef"
+ },
+ "old_title": {
+ "type": "string",
+ "x-go-name": "OldTitle"
+ },
+ "project_id": {
+ "type": "integer",
+ "format": "int64",
+ "x-go-name": "ProjectID"
+ },
+ "pull_request_url": {
+ "type": "string",
+ "x-go-name": "PRURL"
+ },
+ "ref_action": {
+ "type": "string",
+ "x-go-name": "RefAction"
+ },
+ "ref_comment": {
+ "$ref": "#/definitions/Comment"
+ },
+ "ref_commit_sha": {
+ "description": "commit SHA where issue/PR was referenced",
+ "type": "string",
+ "x-go-name": "RefCommitSHA"
+ },
+ "ref_issue": {
+ "$ref": "#/definitions/Issue"
+ },
+ "removed_assignee": {
+ "description": "whether the assignees were removed or added",
+ "type": "boolean",
+ "x-go-name": "RemovedAssignee"
+ },
+ "resolve_doer": {
+ "$ref": "#/definitions/User"
+ },
+ "review_id": {
+ "type": "integer",
+ "format": "int64",
+ "x-go-name": "ReviewID"
+ },
+ "tracked_time": {
+ "$ref": "#/definitions/TrackedTime"
+ },
+ "type": {
+ "type": "string",
+ "x-go-name": "Type"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time",
+ "x-go-name": "Updated"
+ },
+ "user": {
+ "$ref": "#/definitions/User"
+ }
+ },
+ "x-go-package": "code.gitea.io/gitea/modules/structs"
+ },
"TopicName": {
"description": "TopicName a list of repo topic names",
"type": "object",
@@ -18525,6 +18712,15 @@
}
}
},
+ "TimelineList": {
+ "description": "TimelineList",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/TimelineComment"
+ }
+ }
+ },
"TopicListResponse": {
"description": "TopicListResponse",
"schema": {