Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API to get issue/pull comments and events (timeline) #17403

Merged
merged 47 commits into from
Jan 1, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
0918b4c
Add API to get issue/pull comments and events (timeline)
qwerty287 Oct 22, 2021
330ea4d
Fix swagger
qwerty287 Oct 22, 2021
59ae4d1
Merge branch 'main' into timeline-api
qwerty287 Oct 22, 2021
a96bcc7
Don't show code comments (use review api instead)
qwerty287 Oct 22, 2021
2df41c1
fmt
qwerty287 Oct 22, 2021
6811c3a
Fix comment
qwerty287 Oct 22, 2021
ea097a6
Time -> TrackedTime
qwerty287 Oct 22, 2021
7fec3c7
Use var directly
qwerty287 Oct 22, 2021
8a443f0
Add logger
qwerty287 Oct 22, 2021
f13aa96
Merge branch 'main' into timeline-api
qwerty287 Oct 22, 2021
b4bcf1f
Fix lint
qwerty287 Oct 22, 2021
f6df41e
Fix test
qwerty287 Oct 23, 2021
577b222
Merge branch 'main' into timeline-api
qwerty287 Oct 23, 2021
a6a85b7
Merge branch 'timeline-api' of github.com:qwerty287/gitea into timeli…
qwerty287 Oct 23, 2021
de9a364
Merge branch 'main' into timeline-api
qwerty287 Oct 26, 2021
3daf4ff
Merge branch 'main' into timeline-api
qwerty287 Oct 28, 2021
6315db4
Merge branch 'main' into timeline-api
zeripath Oct 28, 2021
ffcfe38
Add comments
qwerty287 Oct 29, 2021
efdb224
fmt
qwerty287 Oct 29, 2021
90439b8
Merge branch 'main' into timeline-api
qwerty287 Oct 29, 2021
fabe8eb
[test] get issue directly by ID
qwerty287 Oct 30, 2021
6f2d140
Merge branch 'main' into timeline-api
qwerty287 Oct 30, 2021
7f1df1c
Merge branch 'main' into timeline-api
qwerty287 Nov 1, 2021
e827bfd
Merge branch 'main' into timeline-api
lunny Nov 5, 2021
ce2cbf0
Merge branch 'main' into timeline-api
qwerty287 Nov 7, 2021
4a04a73
Merge branch 'main' into timeline-api
qwerty287 Nov 12, 2021
13a7019
Merge branch 'main' into timeline-api
qwerty287 Nov 15, 2021
9cbb709
Merge branch 'main' into timeline-api
qwerty287 Nov 16, 2021
c2017dc
Update test
qwerty287 Nov 16, 2021
eca8416
Merge branch 'main' into timeline-api
qwerty287 Nov 18, 2021
7e3172b
Merge branch 'main' into timeline-api
qwerty287 Nov 21, 2021
f835685
Add description for changed refs
qwerty287 Nov 22, 2021
34919d4
Merge branch 'main' into timeline-api
qwerty287 Nov 23, 2021
d2be482
Merge branch 'main' into timeline-api
qwerty287 Nov 26, 2021
7a8b1f1
Fix build issues + lint
qwerty287 Nov 27, 2021
71eecfc
Merge branch 'main' into timeline-api
qwerty287 Nov 29, 2021
bf15f47
Merge branch 'main' into timeline-api
qwerty287 Dec 5, 2021
126f9fb
Merge branch 'main' into timeline-api
qwerty287 Dec 8, 2021
648513d
Merge branch 'main' into timeline-api
qwerty287 Dec 12, 2021
df62eed
Fix build
qwerty287 Dec 12, 2021
e8b65b3
Merge branch 'main' into timeline-api
qwerty287 Dec 21, 2021
4dd46ef
Use string enums
qwerty287 Dec 22, 2021
dd3e9c4
Update swagger
qwerty287 Dec 22, 2021
06db66a
Support `page` and `limit` params
qwerty287 Dec 30, 2021
a6ac7bd
fmt + swagger
qwerty287 Jan 1, 2022
dfd0e65
Use global slices
qwerty287 Jan 1, 2022
ab221a7
Merge branch 'main' into timeline-api
qwerty287 Jan 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions integrations/api_comment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,22 @@ func TestAPIDeleteComment(t *testing.T) {

db.AssertNotExistsBean(t, &models.Comment{ID: comment.ID})
}

func TestAPIListIssueTimeline(t *testing.T) {
defer prepareTestEnv(t)()

comment := db.AssertExistsAndLoadBean(t, &models.Comment{}).(*models.Comment)
issue := db.AssertExistsAndLoadBean(t, &models.Issue{ID: comment.IssueID}).(*models.Issue)
repo := db.AssertExistsAndLoadBean(t, &models.Repository{ID: issue.RepoID}).(*models.Repository)
repoOwner := db.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)

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)

var comments []*api.TimelineComment
DecodeJSON(t, resp, &comments)
expectedCount := db.GetCount(t, &models.Comment{IssueID: issue.ID})
assert.EqualValues(t, expectedCount, len(comments))
}
129 changes: 129 additions & 0 deletions modules/convert/issue_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,132 @@ 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 *models.User) *api.TimelineComment {
err := c.LoadMilestone()
if err != nil {
return nil
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved
}

err = c.LoadAssigneeUserAndTeam()
if err != nil {
return nil
}

err = c.LoadResolveDoer()
if err != nil {
return nil
}

err = c.LoadDepIssueDetails()
if err != nil {
return nil
}

err = c.LoadTime()
if err != nil {
return nil
}

err = c.LoadLabel()
if err != nil {
return nil
}

comment := &api.TimelineComment{
ID: c.ID,
Type: int64(c.Type),
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: int64(c.RefAction),
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.Time = ToTrackedTime(c.Time)
}

if c.RefIssueID != 0 {
issue, err := models.GetIssueByID(c.RefIssueID)
if err != nil {
return nil
}
comment.RefIssue = ToAPIIssue(issue)
}

if c.RefCommentID != 0 {
com, err := models.GetCommentByID(c.RefCommentID)
if err != nil {
return nil
}
err = com.LoadPoster()
if err != nil {
return nil
}
comment.RefComment = ToComment(com)
}

if c.Label != nil {
var org *models.User
var repo *models.Repository
if c.Label.BelongsToOrg() {
var err error
org, err = models.GetUserByID(c.Label.OrgID)
if err != nil {
return nil
}
}
if c.Label.BelongsToRepo() {
var err error
repo, err = models.GetRepositoryByID(c.Label.RepoID)
if err != nil {
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
}
85 changes: 85 additions & 0 deletions modules/structs/issue_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,88 @@ 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 specifies the type of an event.
// 0 Plain comment, can be associated with a commit (CommitID > 0) and a line (LineNum > 0)
// 1 Reopen issue/pull request
// 2 Close issue/pull request
// 3 References.
// 4 Reference from a commit (not part of a pull request)
// 5 Reference from a comment
// 6 Reference from a pull request
// 7 Labels changed (if Body is "1", label was added, if not it was removed)
// 8 Milestone changed
// 9 Assignees changed
// 10 Change Title
// 11 Delete Branch
// 12 Start a stopwatch for time tracking
// 13 Stop a stopwatch for time tracking
// 14 Add time manual for time tracking
// 15 Cancel a stopwatch for time tracking
// 16 Added a due date
// 17 Modified the due date
// 18 Removed a due date
// 19 Dependency added
// 20 Dependency removed
// 21 Not returned; use review API to get more information
// 22 Reviews a pull request by giving general feedback; use review API to get more information
// 23 Lock an issue, giving only collaborators access
// 24 Unlocks a previously locked issue
// 25 Change pull request's target branch
// 26 Delete time manual for time tracking
// 27 add or remove Request from one
// 28 merge pull request
// 29 push to PR head branch (information about the push is included in Body)
// 30 Project changed
// 31 Project board changed
// 32 Dismiss Review
Type int64 `json:"type"`
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved

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"`
Time *TrackedTime `json:"time"`
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved
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"`
// action that was used to reference issue/PR
// 0 means the cross-reference is simply a comment
// 1 means the cross-reference should close an issue if it is resolved
// 2 means the cross-reference should reopen an issue if it is resolved
// 3 means the cross-reference will no longer affect the source
RefAction int64 `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 @@ -816,6 +816,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
100 changes: 100 additions & 0 deletions routers/api/v1/repo/issue_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,106 @@ 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: before
// in: query
// description: if provided, only comments updated before the provided time are returned.
// type: string
// format: date-time
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved
// 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{
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 i, comment := range comments {
if comment.Type != models.CommentTypeCode && isXRefCommentAccessible(ctx.User, comment, issue.RepoID) {
comment.Issue = issue
apiComments = append(apiComments, convert.ToTimelineComment(comments[i], ctx.User))
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved
}
}

ctx.SetTotalCountHeader(int64(len(apiComments)))
ctx.JSON(http.StatusOK, &apiComments)
}

func isXRefCommentAccessible(user *models.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 = models.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
Expand Down
7 changes: 7 additions & 0 deletions routers/api/v1/swagger/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading