Skip to content

Commit 7db2f11

Browse files
qwerty287zeripathlunny
authored
Add API to get issue/pull comments and events (timeline) (#17403)
* 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 #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 <[email protected]> Co-authored-by: Lunny Xiao <[email protected]>
1 parent 549fd03 commit 7db2f11

File tree

9 files changed

+577
-0
lines changed

9 files changed

+577
-0
lines changed

integrations/api_comment_test.go

+22
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,25 @@ func TestAPIDeleteComment(t *testing.T) {
180180

181181
unittest.AssertNotExistsBean(t, &models.Comment{ID: comment.ID})
182182
}
183+
184+
func TestAPIListIssueTimeline(t *testing.T) {
185+
defer prepareTestEnv(t)()
186+
187+
// load comment
188+
issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 1}).(*models.Issue)
189+
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository)
190+
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
191+
192+
// make request
193+
session := loginUser(t, repoOwner.Name)
194+
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/%d/timeline",
195+
repoOwner.Name, repo.Name, issue.Index)
196+
resp := session.MakeRequest(t, req, http.StatusOK)
197+
198+
// check if lens of list returned by API and
199+
// lists extracted directly from DB are the same
200+
var comments []*api.TimelineComment
201+
DecodeJSON(t, resp, &comments)
202+
expectedCount := unittest.GetCount(t, &models.Comment{IssueID: issue.ID})
203+
assert.EqualValues(t, expectedCount, len(comments))
204+
}

models/issue_comment.go

+41
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,47 @@ const (
110110
CommentTypeChangeIssueRef
111111
)
112112

113+
var commentStrings = []string{
114+
"comment",
115+
"reopen",
116+
"close",
117+
"issue_ref",
118+
"commit_ref",
119+
"comment_ref",
120+
"pull_ref",
121+
"label",
122+
"milestone",
123+
"assignees",
124+
"change_title",
125+
"delete_branch",
126+
"start_tracking",
127+
"stop_tracking",
128+
"add_time_manual",
129+
"cancel_tracking",
130+
"added_deadline",
131+
"modified_deadline",
132+
"removed_deadline",
133+
"add_dependency",
134+
"remove_dependency",
135+
"code",
136+
"review",
137+
"lock",
138+
"unlock",
139+
"change_target_branch",
140+
"delete_time_manual",
141+
"review_request",
142+
"merge_pull",
143+
"pull_push",
144+
"project",
145+
"project_board",
146+
"dismiss_review",
147+
"change_issue_ref",
148+
}
149+
150+
func (t CommentType) String() string {
151+
return commentStrings[t]
152+
}
153+
113154
// RoleDescriptor defines comment tag type
114155
type RoleDescriptor int
115156

modules/convert/issue_comment.go

+143
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ package convert
66

77
import (
88
"code.gitea.io/gitea/models"
9+
repo_model "code.gitea.io/gitea/models/repo"
10+
user_model "code.gitea.io/gitea/models/user"
11+
"code.gitea.io/gitea/modules/log"
912
api "code.gitea.io/gitea/modules/structs"
1013
)
1114

@@ -22,3 +25,143 @@ func ToComment(c *models.Comment) *api.Comment {
2225
Updated: c.UpdatedUnix.AsTime(),
2326
}
2427
}
28+
29+
// ToTimelineComment converts a models.Comment to the api.TimelineComment format
30+
func ToTimelineComment(c *models.Comment, doer *user_model.User) *api.TimelineComment {
31+
err := c.LoadMilestone()
32+
if err != nil {
33+
log.Error("LoadMilestone: %v", err)
34+
return nil
35+
}
36+
37+
err = c.LoadAssigneeUserAndTeam()
38+
if err != nil {
39+
log.Error("LoadAssigneeUserAndTeam: %v", err)
40+
return nil
41+
}
42+
43+
err = c.LoadResolveDoer()
44+
if err != nil {
45+
log.Error("LoadResolveDoer: %v", err)
46+
return nil
47+
}
48+
49+
err = c.LoadDepIssueDetails()
50+
if err != nil {
51+
log.Error("LoadDepIssueDetails: %v", err)
52+
return nil
53+
}
54+
55+
err = c.LoadTime()
56+
if err != nil {
57+
log.Error("LoadTime: %v", err)
58+
return nil
59+
}
60+
61+
err = c.LoadLabel()
62+
if err != nil {
63+
log.Error("LoadLabel: %v", err)
64+
return nil
65+
}
66+
67+
comment := &api.TimelineComment{
68+
ID: c.ID,
69+
Type: c.Type.String(),
70+
Poster: ToUser(c.Poster, nil),
71+
HTMLURL: c.HTMLURL(),
72+
IssueURL: c.IssueURL(),
73+
PRURL: c.PRURL(),
74+
Body: c.Content,
75+
Created: c.CreatedUnix.AsTime(),
76+
Updated: c.UpdatedUnix.AsTime(),
77+
78+
OldProjectID: c.OldProjectID,
79+
ProjectID: c.ProjectID,
80+
81+
OldTitle: c.OldTitle,
82+
NewTitle: c.NewTitle,
83+
84+
OldRef: c.OldRef,
85+
NewRef: c.NewRef,
86+
87+
RefAction: c.RefAction.String(),
88+
RefCommitSHA: c.CommitSHA,
89+
90+
ReviewID: c.ReviewID,
91+
92+
RemovedAssignee: c.RemovedAssignee,
93+
}
94+
95+
if c.OldMilestone != nil {
96+
comment.OldMilestone = ToAPIMilestone(c.OldMilestone)
97+
}
98+
if c.Milestone != nil {
99+
comment.Milestone = ToAPIMilestone(c.Milestone)
100+
}
101+
102+
if c.Time != nil {
103+
comment.TrackedTime = ToTrackedTime(c.Time)
104+
}
105+
106+
if c.RefIssueID != 0 {
107+
issue, err := models.GetIssueByID(c.RefIssueID)
108+
if err != nil {
109+
log.Error("GetIssueByID(%d): %v", c.RefIssueID, err)
110+
return nil
111+
}
112+
comment.RefIssue = ToAPIIssue(issue)
113+
}
114+
115+
if c.RefCommentID != 0 {
116+
com, err := models.GetCommentByID(c.RefCommentID)
117+
if err != nil {
118+
log.Error("GetCommentByID(%d): %v", c.RefCommentID, err)
119+
return nil
120+
}
121+
err = com.LoadPoster()
122+
if err != nil {
123+
log.Error("LoadPoster: %v", err)
124+
return nil
125+
}
126+
comment.RefComment = ToComment(com)
127+
}
128+
129+
if c.Label != nil {
130+
var org *user_model.User
131+
var repo *repo_model.Repository
132+
if c.Label.BelongsToOrg() {
133+
var err error
134+
org, err = user_model.GetUserByID(c.Label.OrgID)
135+
if err != nil {
136+
log.Error("GetUserByID(%d): %v", c.Label.OrgID, err)
137+
return nil
138+
}
139+
}
140+
if c.Label.BelongsToRepo() {
141+
var err error
142+
repo, err = repo_model.GetRepositoryByID(c.Label.RepoID)
143+
if err != nil {
144+
log.Error("GetRepositoryByID(%d): %v", c.Label.RepoID, err)
145+
return nil
146+
}
147+
}
148+
comment.Label = ToLabel(c.Label, repo, org)
149+
}
150+
151+
if c.Assignee != nil {
152+
comment.Assignee = ToUser(c.Assignee, nil)
153+
}
154+
if c.AssigneeTeam != nil {
155+
comment.AssigneeTeam = ToTeam(c.AssigneeTeam)
156+
}
157+
158+
if c.ResolveDoer != nil {
159+
comment.ResolveDoer = ToUser(c.ResolveDoer, nil)
160+
}
161+
162+
if c.DependentIssue != nil {
163+
comment.DependentIssue = ToAPIIssue(c.DependentIssue)
164+
}
165+
166+
return comment
167+
}

modules/references/references.go

+11
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ var (
4949
giteaHostInit sync.Once
5050
giteaHost string
5151
giteaIssuePullPattern *regexp.Regexp
52+
53+
actionStrings = []string{
54+
"none",
55+
"closes",
56+
"reopens",
57+
"neutered",
58+
}
5259
)
5360

5461
// XRefAction represents the kind of effect a cross reference has once is resolved
@@ -65,6 +72,10 @@ const (
6572
XRefActionNeutered // 3
6673
)
6774

75+
func (a XRefAction) String() string {
76+
return actionStrings[a]
77+
}
78+
6879
// IssueReference contains an unverified cross-reference to a local issue or pull request
6980
type IssueReference struct {
7081
Index int64

modules/structs/issue_comment.go

+45
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,48 @@ type EditIssueCommentOption struct {
3535
// required: true
3636
Body string `json:"body" binding:"Required"`
3737
}
38+
39+
// TimelineComment represents a timeline comment (comment of any type) on a commit or issue
40+
type TimelineComment struct {
41+
ID int64 `json:"id"`
42+
Type string `json:"type"`
43+
44+
HTMLURL string `json:"html_url"`
45+
PRURL string `json:"pull_request_url"`
46+
IssueURL string `json:"issue_url"`
47+
Poster *User `json:"user"`
48+
Body string `json:"body"`
49+
// swagger:strfmt date-time
50+
Created time.Time `json:"created_at"`
51+
// swagger:strfmt date-time
52+
Updated time.Time `json:"updated_at"`
53+
54+
OldProjectID int64 `json:"old_project_id"`
55+
ProjectID int64 `json:"project_id"`
56+
OldMilestone *Milestone `json:"old_milestone"`
57+
Milestone *Milestone `json:"milestone"`
58+
TrackedTime *TrackedTime `json:"tracked_time"`
59+
OldTitle string `json:"old_title"`
60+
NewTitle string `json:"new_title"`
61+
OldRef string `json:"old_ref"`
62+
NewRef string `json:"new_ref"`
63+
64+
RefIssue *Issue `json:"ref_issue"`
65+
RefComment *Comment `json:"ref_comment"`
66+
RefAction string `json:"ref_action"`
67+
// commit SHA where issue/PR was referenced
68+
RefCommitSHA string `json:"ref_commit_sha"`
69+
70+
ReviewID int64 `json:"review_id"`
71+
72+
Label *Label `json:"label"`
73+
74+
Assignee *User `json:"assignee"`
75+
AssigneeTeam *Team `json:"assignee_team"`
76+
// whether the assignees were removed or added
77+
RemovedAssignee bool `json:"removed_assignee"`
78+
79+
ResolveDoer *User `json:"resolve_doer"`
80+
81+
DependentIssue *Issue `json:"dependent_issue"`
82+
}

routers/api/v1/api.go

+1
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,7 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
842842
m.Combo("/{id}", reqToken()).Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueCommentDeprecated).
843843
Delete(repo.DeleteIssueCommentDeprecated)
844844
})
845+
m.Get("/timeline", repo.ListIssueCommentsAndTimeline)
845846
m.Group("/labels", func() {
846847
m.Combo("").Get(repo.ListIssueLabels).
847848
Post(reqToken(), bind(api.IssueLabelsOption{}), repo.AddIssueLabels).

0 commit comments

Comments
 (0)