Skip to content

Commit

Permalink
Add API to get issue/pull comments and events (timeline) (go-gitea#17403
Browse files Browse the repository at this point in the history
)

* Add API to get issue/pull comments and events (timeline)
Adds an API to get both comments and events in one endpoint with all required data.
Closes go-gitea#13250

* Fix swagger

* Don't show code comments (use review api instead)

* fmt

* Fix comment

* Time -> TrackedTime

* Use var directly

* Add logger

* Fix lint

* Fix test

* Add comments

* fmt

* [test] get issue directly by ID

* Update test

* Add description for changed refs

* Fix build issues + lint

* Fix build

* Use string enums

* Update swagger

* Support `page` and `limit` params

* fmt + swagger

* Use global slices

Co-authored-by: zeripath <art27@cantab.net>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
  • Loading branch information
3 people authored and Stelios Malathouras committed Mar 28, 2022
1 parent a718b99 commit dd021ba
Show file tree
Hide file tree
Showing 9 changed files with 577 additions and 0 deletions.
22 changes: 22 additions & 0 deletions integrations/api_comment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
41 changes: 41 additions & 0 deletions models/issue_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
143 changes: 143 additions & 0 deletions modules/convert/issue_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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
}
11 changes: 11 additions & 0 deletions modules/references/references.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
45 changes: 45 additions & 0 deletions modules/structs/issue_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
1 change: 1 addition & 0 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Loading

0 comments on commit dd021ba

Please sign in to comment.