Skip to content

Commit 692431e

Browse files
abhinav-harnessHarness
authored andcommitted
Add pull request activities endpoint and related functionality (#6)
* Update .gitignore * Remove pagination 'page' parameter from PR activities API * Merge remote-tracking branch 'origin' into abhinav/add-pr-activities * Remove pagination 'page' parameter from PR activities API * Revert "add all comments flag" This reverts commit 4a90eac. * add all comments flag * Set fetchScope flag to false when retrieving PR scope * Update PR activities response type and add edited field to activity model * Merge remote-tracking branch 'origin' into abhinav/add-pr-activities * readd * change activity struct * Add pull request activities endpoint and related functionality
1 parent 72ab63b commit 692431e

File tree

5 files changed

+244
-17
lines changed

5 files changed

+244
-17
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Toolset Name: `pullrequests`
2525
- `get_pull_request`: Get details of a specific pull request
2626
- `list_pull_requests`: List pull requests in a repository
2727
- `get_pull_request_checks`: Get status checks for a specific pull request
28+
- `get_pull_request_activities`: Get activities and comments for a specific pull request
2829
- `create_pull_request`: Create a new pull request
2930

3031
#### Repositories Toolset

client/dto/pullrequest.go

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package dto
22

3+
import "encoding/json"
4+
35
// PullRequest represents a pull request in the system
46
type PullRequest struct {
57
Author PullRequestAuthor `json:"author,omitempty"`
@@ -112,18 +114,18 @@ type PullRequestCheckReporter struct {
112114

113115
// PullRequestCheck represents a status check for a pull request
114116
type PullRequestCheck struct {
115-
Created int64 `json:"created,omitempty"`
116-
Ended int64 `json:"ended,omitempty"`
117-
ID int `json:"id,omitempty"`
118-
Identifier string `json:"identifier,omitempty"`
119-
Link string `json:"link,omitempty"`
120-
Metadata interface{} `json:"metadata"`
121-
Payload PullRequestCheckPayload `json:"payload,omitempty"`
117+
Created int64 `json:"created,omitempty"`
118+
Ended int64 `json:"ended,omitempty"`
119+
ID int `json:"id,omitempty"`
120+
Identifier string `json:"identifier,omitempty"`
121+
Link string `json:"link,omitempty"`
122+
Metadata interface{} `json:"metadata"`
123+
Payload PullRequestCheckPayload `json:"payload,omitempty"`
122124
ReportedBy PullRequestCheckReporter `json:"reported_by,omitempty"`
123-
Started int64 `json:"started,omitempty"`
124-
Status string `json:"status,omitempty"`
125-
Summary string `json:"summary,omitempty"`
126-
Updated int64 `json:"updated,omitempty"`
125+
Started int64 `json:"started,omitempty"`
126+
Status string `json:"status,omitempty"`
127+
Summary string `json:"summary,omitempty"`
128+
Updated int64 `json:"updated,omitempty"`
127129
}
128130

129131
// PullRequestCheckInfo represents a check with additional information
@@ -136,7 +138,7 @@ type PullRequestCheckInfo struct {
136138
// PullRequestChecksResponse represents the response from the checks API
137139
type PullRequestChecksResponse struct {
138140
Checks []PullRequestCheckInfo `json:"checks,omitempty"`
139-
CommitSha string `json:"commit_sha,omitempty"`
141+
CommitSha string `json:"commit_sha,omitempty"`
140142
}
141143

142144
// PullRequestOptions represents the options for listing pull requests
@@ -158,3 +160,49 @@ type PullRequestOptions struct {
158160
AuthorID int `json:"author_id,omitempty"`
159161
IncludeChecks bool `json:"include_checks,omitempty"`
160162
}
163+
164+
// PullRequestActivity represents an activity on a pull request
165+
type PullRequestActivity struct {
166+
ID int `json:"id,omitempty"`
167+
Type string `json:"type,omitempty"`
168+
Created int64 `json:"created,omitempty"`
169+
Updated int64 `json:"updated,omitempty"`
170+
Edited int64 `json:"edited,omitempty"`
171+
ParentID int `json:"parent_id,omitempty"`
172+
RepoID int64 `json:"repo_id"`
173+
PullReqID int64 `json:"pullreq_id"`
174+
Kind string `json:"kind,omitempty"`
175+
Text string `json:"text,omitempty"`
176+
CodeComment CodeComment `json:"code_comment,omitempty"`
177+
Metadata interface{} `json:"metadata,omitempty"`
178+
Resolved int64 `json:"resolved,omitempty"`
179+
PayloadRaw json.RawMessage `json:"payload"`
180+
}
181+
182+
// PullRequestActivitiesResponse represents the response from the activities API
183+
// It's a direct slice of activities as the API returns an array
184+
type PullRequestActivitiesResponse []PullRequestActivity
185+
186+
type CodeComment struct {
187+
Outdated bool `json:"outdated,omitempty"`
188+
MergeBaseSHA string `json:"merge_base_sha,omitempty"`
189+
SourceSHA string `json:"source_sha,omitempty"`
190+
Path string `json:"path,omitempty"`
191+
LineNew int `json:"line_new,omitempty"`
192+
SpanNew int `json:"span_new,omitempty"`
193+
LineOld int `json:"line_old,omitempty"`
194+
SpanOld int `json:"span_old,omitempty"`
195+
}
196+
197+
// PullRequestActivityOptions defines options for listing PR activities
198+
type PullRequestActivityOptions struct {
199+
AccountIdentifier string `json:"accountIdentifier,omitempty"`
200+
OrgIdentifier string `json:"orgIdentifier,omitempty"`
201+
ProjectIdentifier string `json:"projectIdentifier,omitempty"`
202+
Kind []string `json:"kind,omitempty"`
203+
Type []string `json:"type,omitempty"`
204+
After int64 `json:"after,omitempty"`
205+
Before int64 `json:"before,omitempty"`
206+
Limit int `json:"limit,omitempty"`
207+
Page int `json:"page,omitempty"`
208+
}

client/pullrequest.go

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import (
99
)
1010

1111
const (
12-
pullRequestBasePath = "code/api/v1/repos"
13-
pullRequestGetPath = pullRequestBasePath + "/%s/pullreq/%d"
14-
pullRequestListPath = pullRequestBasePath + "/%s/pullreq"
15-
pullRequestCreatePath = pullRequestBasePath + "/%s/pullreq"
16-
pullRequestChecksPath = pullRequestBasePath + "/%s/pullreq/%d/checks"
12+
pullRequestBasePath = "code/api/v1/repos"
13+
pullRequestGetPath = pullRequestBasePath + "/%s/pullreq/%d"
14+
pullRequestListPath = pullRequestBasePath + "/%s/pullreq"
15+
pullRequestCreatePath = pullRequestBasePath + "/%s/pullreq"
16+
pullRequestChecksPath = pullRequestBasePath + "/%s/pullreq/%d/checks"
17+
pullRequestActivitiesPath = pullRequestBasePath + "/%s/pullreq/%d/activities"
1718
)
1819

1920
type PullRequestService struct {
@@ -156,3 +157,66 @@ func (p *PullRequestService) GetChecks(ctx context.Context, scope dto.Scope, rep
156157

157158
return checks, nil
158159
}
160+
161+
// setDefaultPaginationForPRActivities sets default pagination values for PullRequestActivityOptions
162+
func setDefaultPaginationForPRActivities(opts *dto.PullRequestActivityOptions) {
163+
if opts == nil {
164+
return
165+
}
166+
if opts.Page <= 0 {
167+
opts.Page = 1
168+
}
169+
170+
if opts.Limit <= 0 {
171+
opts.Limit = defaultPageSize
172+
} else if opts.Limit > maxPageSize {
173+
opts.Limit = maxPageSize
174+
}
175+
}
176+
177+
// GetActivities retrieves the activities (including comments) for a specific pull request
178+
func (p *PullRequestService) GetActivities(ctx context.Context, scope dto.Scope, repoID string, prNumber int, opts *dto.PullRequestActivityOptions) (dto.PullRequestActivitiesResponse, error) {
179+
path := fmt.Sprintf(pullRequestActivitiesPath, repoID, prNumber)
180+
params := make(map[string]string)
181+
addScope(scope, params)
182+
183+
// Handle nil options by creating default options
184+
if opts == nil {
185+
opts = &dto.PullRequestActivityOptions{}
186+
}
187+
188+
setDefaultPaginationForPRActivities(opts)
189+
190+
params["limit"] = fmt.Sprintf("%d", opts.Limit)
191+
192+
// Add filtering parameters
193+
if len(opts.Kind) > 0 {
194+
params["kind"] = strings.Join(opts.Kind, ",")
195+
}
196+
if len(opts.Type) > 0 {
197+
params["type"] = strings.Join(opts.Type, ",")
198+
}
199+
if opts.After > 0 {
200+
params["after"] = fmt.Sprintf("%d", opts.After)
201+
}
202+
if opts.Before > 0 {
203+
params["before"] = fmt.Sprintf("%d", opts.Before)
204+
}
205+
if opts.AccountIdentifier != "" {
206+
params["accountIdentifier"] = opts.AccountIdentifier
207+
}
208+
if opts.OrgIdentifier != "" {
209+
params["orgIdentifier"] = opts.OrgIdentifier
210+
}
211+
if opts.ProjectIdentifier != "" {
212+
params["projectIdentifier"] = opts.ProjectIdentifier
213+
}
214+
215+
var activities dto.PullRequestActivitiesResponse
216+
err := p.Client.Get(ctx, path, params, nil, &activities)
217+
if err != nil {
218+
return nil, fmt.Errorf("failed to get pull request activities: %w", err)
219+
}
220+
221+
return activities, nil
222+
}

pkg/harness/pullreq.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,3 +346,116 @@ func CreatePullRequestTool(config *config.Config, client *client.PullRequestServ
346346
return mcp.NewToolResultText(string(r)), nil
347347
}
348348
}
349+
350+
// GetPullRequestActivitiesTool creates a tool for getting activities (including comments) for a specific pull request
351+
func GetPullRequestActivitiesTool(config *config.Config, client *client.PullRequestService) (tool mcp.Tool, handler server.ToolHandlerFunc) {
352+
return mcp.NewTool("get_pull_request_activities",
353+
mcp.WithDescription("Get activities and comments for a specific pull request in a Harness repository."),
354+
mcp.WithString("repo_id",
355+
mcp.Required(),
356+
mcp.Description("The ID of the repository"),
357+
),
358+
mcp.WithNumber("pr_number",
359+
mcp.Required(),
360+
mcp.Description("The number of the pull request"),
361+
),
362+
mcp.WithString("kind",
363+
mcp.Description("Optional comma-separated kinds of the pull request activity to include in the result."),
364+
mcp.Enum("change-comment", "comment", "system"),
365+
),
366+
mcp.WithString("type",
367+
mcp.Description("Optional comma-separated types of the pull request activity to include in the result."),
368+
mcp.Enum("branch-delete", "branch-restore", "branch-update", "code-comment", "comment", "label-modify", "merge", "review-submit", "reviewer-add", "reviewer-delete", "state-change", "target-branch-change", "title-change"),
369+
),
370+
mcp.WithNumber("after",
371+
mcp.Description("The result should contain only entries created at and after this timestamp (unix millis)."),
372+
),
373+
mcp.WithNumber("before",
374+
mcp.Description("The result should contain only entries created before this timestamp (unix millis)."),
375+
),
376+
mcp.WithNumber("limit",
377+
mcp.DefaultNumber(20),
378+
mcp.Description("The maximum number of results to return (1-100)."),
379+
mcp.Max(100),
380+
),
381+
WithScope(config, false),
382+
),
383+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
384+
repoID, err := requiredParam[string](request, "repo_id")
385+
if err != nil {
386+
return mcp.NewToolResultError(err.Error()), nil
387+
}
388+
389+
prNumberFloat, err := requiredParam[float64](request, "pr_number")
390+
if err != nil {
391+
return mcp.NewToolResultError(err.Error()), nil
392+
}
393+
prNumber := int(prNumberFloat)
394+
395+
scope, err := fetchScope(config, request, false)
396+
if err != nil {
397+
return mcp.NewToolResultError(err.Error()), nil
398+
}
399+
400+
// Create the options struct
401+
opts := &dto.PullRequestActivityOptions{}
402+
403+
limit, err := OptionalParam[float64](request, "limit")
404+
if err != nil {
405+
return mcp.NewToolResultError(err.Error()), nil
406+
}
407+
if limit > 0 {
408+
opts.Limit = int(limit)
409+
}
410+
411+
// Handle filtering parameters
412+
kindStr, err := OptionalParam[string](request, "kind")
413+
if err != nil {
414+
return mcp.NewToolResultError(err.Error()), nil
415+
}
416+
if kindStr != "" {
417+
opts.Kind = parseCommaSeparatedList(kindStr)
418+
}
419+
420+
typeStr, err := OptionalParam[string](request, "type")
421+
if err != nil {
422+
return mcp.NewToolResultError(err.Error()), nil
423+
}
424+
if typeStr != "" {
425+
opts.Type = parseCommaSeparatedList(typeStr)
426+
}
427+
428+
after, err := OptionalParam[float64](request, "after")
429+
if err != nil {
430+
return mcp.NewToolResultError(err.Error()), nil
431+
}
432+
if after > 0 {
433+
opts.After = int64(after)
434+
}
435+
436+
before, err := OptionalParam[float64](request, "before")
437+
if err != nil {
438+
return mcp.NewToolResultError(err.Error()), nil
439+
}
440+
if before > 0 {
441+
opts.Before = int64(before)
442+
}
443+
444+
// Set scope identifiers
445+
opts.AccountIdentifier = scope.AccountID
446+
opts.OrgIdentifier = scope.OrgID
447+
opts.ProjectIdentifier = scope.ProjectID
448+
449+
data, err := client.GetActivities(ctx, scope, repoID, prNumber, opts)
450+
if err != nil {
451+
return mcp.NewToolResultError(fmt.Sprintf("failed to get pull request activities: %s", err.Error())), nil
452+
}
453+
454+
r, err := json.Marshal(data)
455+
if err != nil {
456+
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal pull request activities: %s", err.Error())), nil
457+
}
458+
459+
return mcp.NewToolResultText(string(r)), nil
460+
}
461+
}

pkg/harness/tools.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ func registerPullRequests(config *config.Config, tsg *toolsets.ToolsetGroup) err
135135
toolsets.NewServerTool(GetPullRequestTool(config, pullRequestClient)),
136136
toolsets.NewServerTool(ListPullRequestsTool(config, pullRequestClient)),
137137
toolsets.NewServerTool(GetPullRequestChecksTool(config, pullRequestClient)),
138+
toolsets.NewServerTool(GetPullRequestActivitiesTool(config, pullRequestClient)),
138139
).
139140
AddWriteTools(
140141
toolsets.NewServerTool(CreatePullRequestTool(config, pullRequestClient)),

0 commit comments

Comments
 (0)