From a0ab910137d91fb7381c61a373f2816d3a398979 Mon Sep 17 00:00:00 2001 From: Nicolas FRADIN Date: Wed, 20 May 2026 19:04:08 +0200 Subject: [PATCH] feat: add PullRequestClient with read and write methods --- src/clients/pull-request-client.ts | 390 +++++++++++++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 src/clients/pull-request-client.ts diff --git a/src/clients/pull-request-client.ts b/src/clients/pull-request-client.ts new file mode 100644 index 0000000..ddbd5c2 --- /dev/null +++ b/src/clients/pull-request-client.ts @@ -0,0 +1,390 @@ +import { BaseClient, ClientOptions } from './base-client.js'; + +export interface CreatePROptions { + title: string; + source_branch: string; + destination_branch: string; + description?: string; + reviewers?: string[]; + close_source_branch?: boolean; +} + +export interface UpdatePROptions { + title?: string; + description?: string; + reviewers?: string[]; + destination_branch?: string; +} + +export interface MergePROptions { + merge_strategy?: 'merge_commit' | 'squash' | 'fast_forward'; + commit_message?: string; + close_source_branch?: boolean; +} + +export class PullRequestClient extends BaseClient { + constructor(options: ClientOptions = {}) { + super(options); + } + + async listPullRequests( + workspace: string, + repoSlug: string, + options?: { state?: string; author?: string; reviewer?: string; since?: string }, + ): Promise { + await this.ensureInitialized(); + try { + const params: Record = {}; + if (options?.state) params.state = options.state; + if (options?.author) params.author = options.author; + if (options?.reviewer) params.reviewer = options.reviewer; + if (options?.since) params.since = options.since; + const response = await this.axiosInstance.get( + `/repositories/${workspace}/${repoSlug}/pullrequests`, + { params }, + ); + return response.data.values || []; + } catch (error) { + throw new Error(`Failed to list pull requests: ${this.formatError(error)}`); + } + } + + async getPullRequest(workspace: string, repoSlug: string, prId: number): Promise { + await this.ensureInitialized(); + try { + const response = await this.axiosInstance.get( + `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}`, + ); + return response.data; + } catch (error) { + throw new Error(`Failed to get pull request: ${this.formatError(error)}`); + } + } + + async getPullRequestActivities( + workspace: string, repoSlug: string, prId: number, + options?: { limit?: number; start?: number }, + ): Promise { + await this.ensureInitialized(); + try { + const params: Record = {}; + if (options?.limit) params.limit = options.limit; + if (options?.start) params.start = options.start; + const response = await this.axiosInstance.get( + `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/activity`, + { params }, + ); + return response.data; + } catch (error) { + throw new Error(`Failed to get activities: ${this.formatError(error)}`); + } + } + + async getPullRequestChanges( + workspace: string, repoSlug: string, prId: number, + options?: { limit?: number; start?: number }, + ): Promise { + await this.ensureInitialized(); + try { + const params: Record = {}; + if (options?.limit) params.limit = options.limit; + if (options?.start) params.start = options.start; + const prResponse = await this.axiosInstance.get( + `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}`, + ); + const sourceHash = prResponse.data.source?.commit?.hash; + const destHash = prResponse.data.destination?.commit?.hash; + if (!sourceHash || !destHash) { + throw new Error('Could not determine source and destination commits'); + } + const response = await this.axiosInstance.get( + `/repositories/${workspace}/${repoSlug}/diffstat/${sourceHash}..${destHash}`, + { params }, + ); + return response.data; + } catch (error) { + throw new Error(`Failed to get changes: ${this.formatError(error)}`); + } + } + + async getPullRequestCommits( + workspace: string, repoSlug: string, prId: number, + options?: { limit?: number; start?: number }, + ): Promise { + await this.ensureInitialized(); + try { + const params: Record = {}; + if (options?.limit) params.limit = options.limit; + if (options?.start) params.start = options.start; + const response = await this.axiosInstance.get( + `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/commits`, + { params }, + ); + return response.data; + } catch (error) { + throw new Error(`Failed to get commits: ${this.formatError(error)}`); + } + } + + async getPullRequestDiff( + workspace: string, repoSlug: string, prId: number, + options?: { context?: number; path?: string; whitespace?: string }, + ): Promise { + await this.ensureInitialized(); + try { + const params: Record = {}; + if (options?.context) params.context = options.context; + if (options?.whitespace) params.whitespace = options.whitespace; + if (options?.path) { + const response = await this.axiosInstance.get( + `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/diff/${options.path}`, + { params, responseType: 'text' }, + ); + return response.data; + } + const prResponse = await this.axiosInstance.get( + `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}`, + ); + const sourceHash = prResponse.data.source?.commit?.hash; + const destHash = prResponse.data.destination?.commit?.hash; + if (!sourceHash || !destHash) { + throw new Error('Could not determine source and destination commits'); + } + const response = await this.axiosInstance.get( + `/repositories/${workspace}/${repoSlug}/diff/${sourceHash}..${destHash}`, + { params, responseType: 'text' }, + ); + return response.data; + } catch (error) { + throw new Error(`Failed to get diff: ${this.formatError(error)}`); + } + } + + async getPullRequestPatch(workspace: string, repoSlug: string, prId: number): Promise { + await this.ensureInitialized(); + try { + const prResponse = await this.axiosInstance.get( + `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}`, + ); + const sourceHash = prResponse.data.source?.commit?.hash; + const destHash = prResponse.data.destination?.commit?.hash; + if (!sourceHash || !destHash) { + throw new Error('Could not determine source and destination commits'); + } + const response = await this.axiosInstance.get( + `/repositories/${workspace}/${repoSlug}/diff/${sourceHash}..${destHash}`, + { responseType: 'text' }, + ); + return { patch: response.data }; + } catch (error) { + throw new Error(`Failed to get patch: ${this.formatError(error)}`); + } + } + + async getPullRequestParticipants(workspace: string, repoSlug: string, prId: number): Promise { + await this.ensureInitialized(); + try { + const response = await this.axiosInstance.get( + `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}`, + ); + return response.data.participants || []; + } catch (error) { + throw new Error(`Failed to get participants: ${this.formatError(error)}`); + } + } + + async getPullRequestReviewers(workspace: string, repoSlug: string, prId: number): Promise { + await this.ensureInitialized(); + try { + const response = await this.axiosInstance.get( + `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}`, + ); + return response.data.reviewers || []; + } catch (error) { + throw new Error(`Failed to get reviewers: ${this.formatError(error)}`); + } + } + + async getPullRequestStatus(workspace: string, repoSlug: string, prId: number): Promise { + await this.ensureInitialized(); + try { + const response = await this.axiosInstance.get( + `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}`, + ); + const pr = response.data; + return { + id: pr.id, title: pr.title, state: pr.state, status: pr.status, + author: pr.author, + source_branch: pr.source?.branch?.name, + destination_branch: pr.destination?.branch?.name, + created_on: pr.created_on, updated_on: pr.updated_on, + closed_on: pr.closed_on, merge_commit: pr.merge_commit, + }; + } catch (error) { + throw new Error(`Failed to get status: ${this.formatError(error)}`); + } + } + + async getPullRequestTasks(workspace: string, repoSlug: string, prId: number): Promise { + await this.ensureInitialized(); + try { + const response = await this.axiosInstance.get( + `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/tasks`, + ); + return response.data; + } catch (error) { + throw new Error(`Failed to get tasks: ${this.formatError(error)}`); + } + } + + async getPullRequestTaskCount(workspace: string, repoSlug: string, prId: number): Promise { + await this.ensureInitialized(); + try { + const response = await this.axiosInstance.get( + `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/tasks`, + ); + return { count: response.data.values?.length || 0 }; + } catch (error) { + throw new Error(`Failed to get task count: ${this.formatError(error)}`); + } + } + + async getFullPullRequest(workspace: string, repoSlug: string, prId: number): Promise { + await this.ensureInitialized(); + try { + const response = await this.axiosInstance.get( + `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}`, + { params: { fields: '+*' } }, + ); + return response.data; + } catch (error) { + throw new Error(`Failed to get full PR: ${this.formatError(error)}`); + } + } + + async createPullRequest(workspace: string, repoSlug: string, opts: CreatePROptions): Promise { + await this.ensureInitialized(); + try { + const body: Record = { + title: opts.title, + source: { branch: { name: opts.source_branch } }, + destination: { branch: { name: opts.destination_branch } }, + }; + if (opts.description !== undefined) body.description = opts.description; + if (opts.close_source_branch !== undefined) body.close_source_branch = opts.close_source_branch; + if (opts.reviewers?.length) { + body.reviewers = opts.reviewers.map(r => ({ uuid: r })); + } + const response = await this.axiosInstance.post( + `/repositories/${workspace}/${repoSlug}/pullrequests`, + body, + ); + return response.data; + } catch (error) { + throw new Error(`Failed to create pull request: ${this.formatError(error)}`); + } + } + + async updatePullRequest( + workspace: string, repoSlug: string, prId: number, opts: UpdatePROptions, + ): Promise { + await this.ensureInitialized(); + try { + const body: Record = {}; + if (opts.title !== undefined) body.title = opts.title; + if (opts.description !== undefined) body.description = opts.description; + if (opts.destination_branch !== undefined) { + body.destination = { branch: { name: opts.destination_branch } }; + } + if (opts.reviewers !== undefined) { + body.reviewers = opts.reviewers.map(r => ({ uuid: r })); + } + const response = await this.axiosInstance.put( + `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}`, + body, + ); + return response.data; + } catch (error) { + throw new Error(`Failed to update pull request: ${this.formatError(error)}`); + } + } + + async mergePullRequest( + workspace: string, repoSlug: string, prId: number, opts: MergePROptions = {}, + ): Promise { + await this.ensureInitialized(); + try { + const body: Record = {}; + if (opts.merge_strategy) body.merge_strategy = opts.merge_strategy; + if (opts.commit_message) body.message = opts.commit_message; + if (opts.close_source_branch !== undefined) body.close_source_branch = opts.close_source_branch; + const response = await this.axiosInstance.post( + `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/merge`, + body, + ); + return response.data; + } catch (error) { + throw new Error(`Failed to merge pull request: ${this.formatError(error)}`); + } + } + + async declinePullRequest(workspace: string, repoSlug: string, prId: number): Promise { + await this.ensureInitialized(); + try { + const response = await this.axiosInstance.post( + `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/decline`, + ); + return response.data; + } catch (error) { + throw new Error(`Failed to decline pull request: ${this.formatError(error)}`); + } + } + + async approvePullRequest(workspace: string, repoSlug: string, prId: number): Promise { + await this.ensureInitialized(); + try { + const response = await this.axiosInstance.post( + `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/approve`, + ); + return response.data; + } catch (error) { + throw new Error(`Failed to approve pull request: ${this.formatError(error)}`); + } + } + + async unapprovePullRequest(workspace: string, repoSlug: string, prId: number): Promise { + await this.ensureInitialized(); + try { + await this.axiosInstance.delete( + `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/approve`, + ); + return { success: true }; + } catch (error) { + throw new Error(`Failed to unapprove pull request: ${this.formatError(error)}`); + } + } + + async requestChangesPullRequest(workspace: string, repoSlug: string, prId: number): Promise { + await this.ensureInitialized(); + try { + const response = await this.axiosInstance.post( + `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/request-changes`, + ); + return response.data; + } catch (error) { + throw new Error(`Failed to request changes: ${this.formatError(error)}`); + } + } + + async removeRequestChangesPullRequest(workspace: string, repoSlug: string, prId: number): Promise { + await this.ensureInitialized(); + try { + await this.axiosInstance.delete( + `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/request-changes`, + ); + return { success: true }; + } catch (error) { + throw new Error(`Failed to remove request-changes: ${this.formatError(error)}`); + } + } +}