From e5da36e51549141c4446654536720f608d0994c2 Mon Sep 17 00:00:00 2001 From: pdogra1299 Date: Mon, 7 Jul 2025 11:58:53 +0530 Subject: [PATCH] feat: add list_branch_commits and list_pr_commits tools - Add list_branch_commits tool with advanced filtering options: - Filter by date range (since/until) - Filter by author email/username - Include/exclude merge commits - Search in commit messages - Pagination support - Add list_pr_commits tool to list all commits in a pull request - Returns PR title along with commit list - Pagination support - Detailed commit information - Fix author filtering for Bitbucket Server with client-side filtering - Add TypeScript interfaces for commit types - Add formatter functions for consistent commit representation - Update documentation with comprehensive usage examples - Bump version to 0.10.0 --- CHANGELOG.md | 30 ++++ README.md | 180 +++++++++++++++++++++++ package.json | 2 +- src/handlers/branch-handlers.ts | 196 +++++++++++++++++++++++++- src/handlers/pull-request-handlers.ts | 99 ++++++++++++- src/index.ts | 6 +- src/tools/definitions.ts | 80 +++++++++++ src/types/bitbucket.ts | 59 ++++++++ src/types/guards.ts | 44 ++++++ src/utils/formatters.ts | 44 +++++- 10 files changed, 732 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee758d5..60c0653 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.10.0] - 2025-07-03 + +### Added +- **New `list_branch_commits` tool for retrieving commit history**: + - List all commits in a specific branch with detailed information + - Advanced filtering options: + - `since` and `until` parameters for date range filtering (ISO date strings) + - `author` parameter to filter by author email/username + - `include_merge_commits` parameter to include/exclude merge commits (default: true) + - `search` parameter to search in commit messages + - Returns branch head information and paginated commit list + - Each commit includes hash, message, author details, date, parents, and merge status + - Supports both Bitbucket Server and Cloud APIs with appropriate parameter mapping + - Useful for reviewing commit history, tracking changes, and analyzing branch activity + +- **New `list_pr_commits` tool for pull request commits**: + - List all commits that are part of a specific pull request + - Returns PR title and paginated commit list + - Simpler than branch commits - focused specifically on PR changes + - Each commit includes same detailed information as branch commits + - Supports pagination with `limit` and `start` parameters + - Useful for reviewing all changes in a PR before merging + +### Changed +- Added new TypeScript interfaces for commit types: + - `BitbucketServerCommit` and `BitbucketCloudCommit` for API responses + - `FormattedCommit` for consistent commit representation +- Added formatter functions `formatServerCommit` and `formatCloudCommit` for unified output +- Enhanced type guards with `isListBranchCommitsArgs` and `isListPrCommitsArgs` + ## [0.9.1] - 2025-01-27 ### Fixed diff --git a/README.md b/README.md index 0e4ab2d..eb17a2d 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,14 @@ An MCP (Model Context Protocol) server that provides tools for interacting with - `update_pull_request` - Update PR details (title, description, reviewers, destination branch) - `add_comment` - Add comments to pull requests (supports replies) - `merge_pull_request` - Merge pull requests with various strategies +- `list_pr_commits` - List all commits that are part of a pull request - `delete_branch` - Delete branches after merge #### Branch Management Tools - `list_branches` - List branches with filtering and pagination - `delete_branch` - Delete branches (with protection checks) - `get_branch` - Get detailed branch information including associated PRs +- `list_branch_commits` - List commits in a branch with advanced filtering #### File and Directory Tools - `list_directory_content` - List files and directories in a repository path @@ -596,6 +598,184 @@ This tool is particularly useful for: - Understanding PR review status - Identifying stale branches +### List Branch Commits + +Get all commits in a specific branch with advanced filtering options: + +```typescript +// Basic usage - get recent commits +{ + "tool": "list_branch_commits", + "arguments": { + "workspace": "PROJ", + "repository": "my-repo", + "branch_name": "feature/new-feature", + "limit": 50 // Optional (default: 25) + } +} + +// Filter by date range +{ + "tool": "list_branch_commits", + "arguments": { + "workspace": "PROJ", + "repository": "my-repo", + "branch_name": "main", + "since": "2025-01-01T00:00:00Z", // ISO date string + "until": "2025-01-15T23:59:59Z" // ISO date string + } +} + +// Filter by author +{ + "tool": "list_branch_commits", + "arguments": { + "workspace": "PROJ", + "repository": "my-repo", + "branch_name": "develop", + "author": "john.doe@company.com", // Email or username + "limit": 100 + } +} + +// Exclude merge commits +{ + "tool": "list_branch_commits", + "arguments": { + "workspace": "PROJ", + "repository": "my-repo", + "branch_name": "release/v2.0", + "include_merge_commits": false + } +} + +// Search in commit messages +{ + "tool": "list_branch_commits", + "arguments": { + "workspace": "PROJ", + "repository": "my-repo", + "branch_name": "main", + "search": "bugfix", // Search in commit messages + "limit": 50 + } +} + +// Combine multiple filters +{ + "tool": "list_branch_commits", + "arguments": { + "workspace": "PROJ", + "repository": "my-repo", + "branch_name": "develop", + "author": "jane.smith@company.com", + "since": "2025-01-01T00:00:00Z", + "include_merge_commits": false, + "search": "feature", + "limit": 100, + "start": 0 // For pagination + } +} +``` + +**Filter Parameters:** +- `since`: ISO date string - only show commits after this date +- `until`: ISO date string - only show commits before this date +- `author`: Filter by author email/username +- `include_merge_commits`: Boolean to include/exclude merge commits (default: true) +- `search`: Search for text in commit messages + +Returns detailed commit information: +```json +{ + "branch_name": "feature/new-feature", + "branch_head": "abc123def456", // Latest commit hash + "commits": [ + { + "hash": "abc123def456", + "abbreviated_hash": "abc123d", + "message": "Add new feature implementation", + "author": { + "name": "John Doe", + "email": "john.doe@example.com" + }, + "date": "2025-01-03T10:30:00Z", + "parents": ["parent1hash", "parent2hash"], + "is_merge_commit": false + } + // ... more commits + ], + "total_count": 150, + "start": 0, + "limit": 25, + "has_more": true, + "next_start": 25, + "filters_applied": { + "author": "john.doe@example.com", + "since": "2025-01-01", + "include_merge_commits": false + } +} +``` + +This tool is particularly useful for: +- Reviewing commit history before releases +- Finding commits by specific authors +- Tracking changes within date ranges +- Searching for specific features or fixes +- Analyzing branch activity patterns + +### List PR Commits + +Get all commits that are part of a pull request: + +```typescript +{ + "tool": "list_pr_commits", + "arguments": { + "workspace": "PROJ", + "repository": "my-repo", + "pull_request_id": 123, + "limit": 50, // Optional (default: 25) + "start": 0 // Optional: for pagination + } +} +``` + +Returns commit information for the PR: +```json +{ + "pull_request_id": 123, + "pull_request_title": "Add awesome feature", + "commits": [ + { + "hash": "def456ghi789", + "abbreviated_hash": "def456g", + "message": "Initial implementation", + "author": { + "name": "Jane Smith", + "email": "jane.smith@example.com" + }, + "date": "2025-01-02T14:20:00Z", + "parents": ["parent1hash"], + "is_merge_commit": false + } + // ... more commits + ], + "total_count": 5, + "start": 0, + "limit": 25, + "has_more": false +} +``` + +This tool is particularly useful for: +- Reviewing all changes in a PR before merging +- Understanding the development history of a PR +- Checking commit messages for quality +- Verifying authorship of changes +- Analyzing PR complexity by commit count + ### Get Pull Request Diff Get the diff/changes for a pull request with optional filtering capabilities: diff --git a/package.json b/package.json index 627fb14..97c554f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nexus2520/bitbucket-mcp-server", - "version": "0.9.1", + "version": "0.10.0", "description": "MCP server for Bitbucket API integration - supports both Cloud and Server", "type": "module", "main": "./build/index.js", diff --git a/src/handlers/branch-handlers.ts b/src/handlers/branch-handlers.ts index d50bf1c..5af98c0 100644 --- a/src/handlers/branch-handlers.ts +++ b/src/handlers/branch-handlers.ts @@ -3,9 +3,17 @@ import { BitbucketApiClient } from '../utils/api-client.js'; import { isListBranchesArgs, isDeleteBranchArgs, - isGetBranchArgs + isGetBranchArgs, + isListBranchCommitsArgs } from '../types/guards.js'; -import { BitbucketServerBranch, BitbucketCloudBranch } from '../types/bitbucket.js'; +import { + BitbucketServerBranch, + BitbucketCloudBranch, + BitbucketServerCommit, + BitbucketCloudCommit, + FormattedCommit +} from '../types/bitbucket.js'; +import { formatServerCommit, formatCloudCommit } from '../utils/formatters.js'; export class BranchHandlers { constructor( @@ -392,4 +400,188 @@ export class BranchHandlers { return this.apiClient.handleApiError(error, `getting branch '${branch_name}' in ${workspace}/${repository}`); } } + + async handleListBranchCommits(args: any) { + if (!isListBranchCommitsArgs(args)) { + throw new McpError( + ErrorCode.InvalidParams, + 'Invalid arguments for list_branch_commits' + ); + } + + const { + workspace, + repository, + branch_name, + limit = 25, + start = 0, + since, + until, + author, + include_merge_commits = true, + search + } = args; + + try { + let apiPath: string; + let params: any = {}; + let commits: FormattedCommit[] = []; + let totalCount = 0; + let nextPageStart: number | null = null; + + if (this.apiClient.getIsServer()) { + // Bitbucket Server API + apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/commits`; + params = { + until: `refs/heads/${branch_name}`, + limit, + start, + withCounts: true + }; + + // Add filters + if (since) { + params.since = since; + } + if (!include_merge_commits) { + params.merges = 'exclude'; + } + + const response = await this.apiClient.makeRequest('get', apiPath, undefined, { params }); + + // Format commits + commits = (response.values || []).map((commit: BitbucketServerCommit) => formatServerCommit(commit)); + + // Apply client-side filters for Server API + if (author) { + // Filter by author email or name + commits = commits.filter(c => + c.author.email === author || + c.author.name === author || + c.author.email.toLowerCase() === author.toLowerCase() || + c.author.name.toLowerCase() === author.toLowerCase() + ); + } + + // Filter by date if 'until' is provided (Server API doesn't support 'until' param directly) + if (until) { + const untilDate = new Date(until).getTime(); + commits = commits.filter(c => new Date(c.date).getTime() <= untilDate); + } + + // Filter by message search if provided + if (search) { + const searchLower = search.toLowerCase(); + commits = commits.filter(c => c.message.toLowerCase().includes(searchLower)); + } + + // If we applied client-side filters, update the total count + if (author || until || search) { + totalCount = commits.length; + // Can't determine if there are more results when filtering client-side + nextPageStart = null; + } else { + totalCount = response.size || commits.length; + if (!response.isLastPage && response.nextPageStart !== undefined) { + nextPageStart = response.nextPageStart; + } + } + } else { + // Bitbucket Cloud API + apiPath = `/repositories/${workspace}/${repository}/commits/${encodeURIComponent(branch_name)}`; + params = { + pagelen: limit, + page: Math.floor(start / limit) + 1 + }; + + // Build query string for filters + const queryParts: string[] = []; + if (author) { + queryParts.push(`author.raw ~ "${author}"`); + } + if (!include_merge_commits) { + // Cloud API doesn't have direct merge exclusion, we'll filter client-side + } + if (queryParts.length > 0) { + params.q = queryParts.join(' AND '); + } + + const response = await this.apiClient.makeRequest('get', apiPath, undefined, { params }); + + // Format commits + let cloudCommits = (response.values || []).map((commit: BitbucketCloudCommit) => formatCloudCommit(commit)); + + // Apply client-side filters + if (!include_merge_commits) { + cloudCommits = cloudCommits.filter((c: FormattedCommit) => !c.is_merge_commit); + } + if (since) { + const sinceDate = new Date(since).getTime(); + cloudCommits = cloudCommits.filter((c: FormattedCommit) => new Date(c.date).getTime() >= sinceDate); + } + if (until) { + const untilDate = new Date(until).getTime(); + cloudCommits = cloudCommits.filter((c: FormattedCommit) => new Date(c.date).getTime() <= untilDate); + } + if (search) { + const searchLower = search.toLowerCase(); + cloudCommits = cloudCommits.filter((c: FormattedCommit) => c.message.toLowerCase().includes(searchLower)); + } + + commits = cloudCommits; + totalCount = response.size || commits.length; + if (response.next) { + nextPageStart = start + limit; + } + } + + // Get branch head info + let branchHead: string | null = null; + try { + if (this.apiClient.getIsServer()) { + const branchesPath = `/rest/api/latest/projects/${workspace}/repos/${repository}/branches`; + const branchesResponse = await this.apiClient.makeRequest('get', branchesPath, undefined, { + params: { filterText: branch_name, limit: 1 } + }); + const branch = branchesResponse.values?.find((b: any) => b.displayId === branch_name); + branchHead = branch?.latestCommit || null; + } else { + const branchPath = `/repositories/${workspace}/${repository}/refs/branches/${encodeURIComponent(branch_name)}`; + const branch = await this.apiClient.makeRequest('get', branchPath); + branchHead = branch.target?.hash || null; + } + } catch (e) { + // Ignore error, branch head is optional + } + + // Build filters applied summary + const filtersApplied: any = {}; + if (author) filtersApplied.author = author; + if (since) filtersApplied.since = since; + if (until) filtersApplied.until = until; + if (include_merge_commits !== undefined) filtersApplied.include_merge_commits = include_merge_commits; + if (search) filtersApplied.search = search; + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + branch_name, + branch_head: branchHead, + commits, + total_count: totalCount, + start, + limit, + has_more: nextPageStart !== null, + next_start: nextPageStart, + filters_applied: filtersApplied + }, null, 2), + }, + ], + }; + } catch (error) { + return this.apiClient.handleApiError(error, `listing commits for branch '${branch_name}' in ${workspace}/${repository}`); + } + } } diff --git a/src/handlers/pull-request-handlers.ts b/src/handlers/pull-request-handlers.ts index ba761f5..70d4a03 100644 --- a/src/handlers/pull-request-handlers.ts +++ b/src/handlers/pull-request-handlers.ts @@ -1,6 +1,6 @@ import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { BitbucketApiClient } from '../utils/api-client.js'; -import { formatServerResponse, formatCloudResponse } from '../utils/formatters.js'; +import { formatServerResponse, formatCloudResponse, formatServerCommit, formatCloudCommit } from '../utils/formatters.js'; import { formatSuggestionComment } from '../utils/suggestion-formatter.js'; import { DiffParser } from '../utils/diff-parser.js'; import { @@ -13,7 +13,10 @@ import { FormattedComment, FormattedFileChange, CodeMatch, - MultipleMatchesError + MultipleMatchesError, + BitbucketServerCommit, + BitbucketCloudCommit, + FormattedCommit } from '../types/bitbucket.js'; import { isGetPullRequestArgs, @@ -21,7 +24,8 @@ import { isCreatePullRequestArgs, isUpdatePullRequestArgs, isAddCommentArgs, - isMergePullRequestArgs + isMergePullRequestArgs, + isListPrCommitsArgs } from '../types/guards.js'; export class PullRequestHandlers { @@ -1113,4 +1117,93 @@ export class PullRequestHandlers { private selectBestMatch(matches: CodeMatch[]): CodeMatch { return matches.sort((a, b) => b.confidence - a.confidence)[0]; } + + async handleListPrCommits(args: any) { + if (!isListPrCommitsArgs(args)) { + throw new McpError( + ErrorCode.InvalidParams, + 'Invalid arguments for list_pr_commits' + ); + } + + const { workspace, repository, pull_request_id, limit = 25, start = 0 } = args; + + try { + // First get the PR details to include in response + const prPath = this.apiClient.getIsServer() + ? `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}` + : `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}`; + + let prTitle = ''; + try { + const pr = await this.apiClient.makeRequest('get', prPath); + prTitle = pr.title; + } catch (e) { + // Ignore error, PR title is optional + } + + let apiPath: string; + let params: any = {}; + let commits: FormattedCommit[] = []; + let totalCount = 0; + let nextPageStart: number | null = null; + + if (this.apiClient.getIsServer()) { + // Bitbucket Server API + apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/commits`; + params = { + limit, + start, + withCounts: true + }; + + const response = await this.apiClient.makeRequest('get', apiPath, undefined, { params }); + + // Format commits + commits = (response.values || []).map((commit: BitbucketServerCommit) => formatServerCommit(commit)); + + totalCount = response.size || commits.length; + if (!response.isLastPage && response.nextPageStart !== undefined) { + nextPageStart = response.nextPageStart; + } + } else { + // Bitbucket Cloud API + apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/commits`; + params = { + pagelen: limit, + page: Math.floor(start / limit) + 1 + }; + + const response = await this.apiClient.makeRequest('get', apiPath, undefined, { params }); + + // Format commits + commits = (response.values || []).map((commit: BitbucketCloudCommit) => formatCloudCommit(commit)); + + totalCount = response.size || commits.length; + if (response.next) { + nextPageStart = start + limit; + } + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + pull_request_id, + pull_request_title: prTitle, + commits, + total_count: totalCount, + start, + limit, + has_more: nextPageStart !== null, + next_start: nextPageStart + }, null, 2), + }, + ], + }; + } catch (error) { + return this.apiClient.handleApiError(error, `listing commits for pull request ${pull_request_id} in ${workspace}/${repository}`); + } + } } diff --git a/src/index.ts b/src/index.ts index bc773a0..d3362cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,7 +40,7 @@ class BitbucketMCPServer { this.server = new Server( { name: 'bitbucket-mcp-server', - version: '0.9.1', + version: '0.10.0', }, { capabilities: { @@ -99,6 +99,8 @@ class BitbucketMCPServer { return this.pullRequestHandlers.handleAddComment(request.params.arguments); case 'merge_pull_request': return this.pullRequestHandlers.handleMergePullRequest(request.params.arguments); + case 'list_pr_commits': + return this.pullRequestHandlers.handleListPrCommits(request.params.arguments); // Branch tools case 'list_branches': @@ -107,6 +109,8 @@ class BitbucketMCPServer { return this.branchHandlers.handleDeleteBranch(request.params.arguments); case 'get_branch': return this.branchHandlers.handleGetBranch(request.params.arguments); + case 'list_branch_commits': + return this.branchHandlers.handleListBranchCommits(request.params.arguments); // Code Review tools case 'get_pull_request_diff': diff --git a/src/tools/definitions.ts b/src/tools/definitions.ts index 0f25f51..f2f9c99 100644 --- a/src/tools/definitions.ts +++ b/src/tools/definitions.ts @@ -527,4 +527,84 @@ export const toolDefinitions = [ required: ['workspace', 'repository', 'file_path'], }, }, + { + name: 'list_branch_commits', + description: 'List commits in a branch with detailed information and filtering options', + inputSchema: { + type: 'object', + properties: { + workspace: { + type: 'string', + description: 'Bitbucket workspace/project key (e.g., "PROJ")', + }, + repository: { + type: 'string', + description: 'Repository slug (e.g., "my-repo")', + }, + branch_name: { + type: 'string', + description: 'Branch name to get commits from', + }, + limit: { + type: 'number', + description: 'Maximum number of commits to return (default: 25)', + }, + start: { + type: 'number', + description: 'Start index for pagination (default: 0)', + }, + since: { + type: 'string', + description: 'ISO date string - only show commits after this date (optional)', + }, + until: { + type: 'string', + description: 'ISO date string - only show commits before this date (optional)', + }, + author: { + type: 'string', + description: 'Filter by author email/username (optional)', + }, + include_merge_commits: { + type: 'boolean', + description: 'Include merge commits in results (default: true)', + }, + search: { + type: 'string', + description: 'Search for text in commit messages (optional)', + }, + }, + required: ['workspace', 'repository', 'branch_name'], + }, + }, + { + name: 'list_pr_commits', + description: 'List all commits that are part of a pull request', + inputSchema: { + type: 'object', + properties: { + workspace: { + type: 'string', + description: 'Bitbucket workspace/project key (e.g., "PROJ")', + }, + repository: { + type: 'string', + description: 'Repository slug (e.g., "my-repo")', + }, + pull_request_id: { + type: 'number', + description: 'Pull request ID', + }, + limit: { + type: 'number', + description: 'Maximum number of commits to return (default: 25)', + }, + start: { + type: 'number', + description: 'Start index for pagination (default: 0)', + }, + }, + required: ['workspace', 'repository', 'pull_request_id'], + }, + }, ]; diff --git a/src/types/bitbucket.ts b/src/types/bitbucket.ts index 2ebb27d..3bd098b 100644 --- a/src/types/bitbucket.ts +++ b/src/types/bitbucket.ts @@ -384,3 +384,62 @@ export interface MultipleMatchesError { }>; suggestion: string; } + +// Commit types +export interface BitbucketServerCommit { + id: string; + displayId: string; + message: string; + author: { + name: string; + emailAddress: string; + }; + authorTimestamp: number; + committer?: { + name: string; + emailAddress: string; + }; + committerTimestamp?: number; + parents: Array<{ + id: string; + displayId: string; + }>; +} + +export interface BitbucketCloudCommit { + hash: string; + message: string; + author: { + raw: string; + user?: { + display_name: string; + account_id: string; + }; + }; + date: string; + parents: Array<{ + hash: string; + type: string; + }>; + links?: { + self: { + href: string; + }; + html: { + href: string; + }; + }; +} + +export interface FormattedCommit { + hash: string; + abbreviated_hash: string; + message: string; + author: { + name: string; + email: string; + }; + date: string; + parents: string[]; + is_merge_commit: boolean; +} diff --git a/src/types/guards.ts b/src/types/guards.ts index 94fb60d..0127081 100644 --- a/src/types/guards.ts +++ b/src/types/guards.ts @@ -261,3 +261,47 @@ export const isGetFileContentArgs = ( (args.start_line === undefined || typeof args.start_line === 'number') && (args.line_count === undefined || typeof args.line_count === 'number') && (args.full_content === undefined || typeof args.full_content === 'boolean'); + +export const isListBranchCommitsArgs = ( + args: any +): args is { + workspace: string; + repository: string; + branch_name: string; + limit?: number; + start?: number; + since?: string; + until?: string; + author?: string; + include_merge_commits?: boolean; + search?: string; +} => + typeof args === 'object' && + args !== null && + typeof args.workspace === 'string' && + typeof args.repository === 'string' && + typeof args.branch_name === 'string' && + (args.limit === undefined || typeof args.limit === 'number') && + (args.start === undefined || typeof args.start === 'number') && + (args.since === undefined || typeof args.since === 'string') && + (args.until === undefined || typeof args.until === 'string') && + (args.author === undefined || typeof args.author === 'string') && + (args.include_merge_commits === undefined || typeof args.include_merge_commits === 'boolean') && + (args.search === undefined || typeof args.search === 'string'); + +export const isListPrCommitsArgs = ( + args: any +): args is { + workspace: string; + repository: string; + pull_request_id: number; + limit?: number; + start?: number; +} => + typeof args === 'object' && + args !== null && + typeof args.workspace === 'string' && + typeof args.repository === 'string' && + typeof args.pull_request_id === 'number' && + (args.limit === undefined || typeof args.limit === 'number') && + (args.start === undefined || typeof args.start === 'number'); diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts index e83bbc8..15ec431 100644 --- a/src/utils/formatters.ts +++ b/src/utils/formatters.ts @@ -1,4 +1,11 @@ -import { BitbucketServerPullRequest, BitbucketCloudPullRequest, MergeInfo } from '../types/bitbucket.js'; +import { + BitbucketServerPullRequest, + BitbucketCloudPullRequest, + MergeInfo, + BitbucketServerCommit, + BitbucketCloudCommit, + FormattedCommit +} from '../types/bitbucket.js'; export function formatServerResponse( pr: BitbucketServerPullRequest, @@ -74,3 +81,38 @@ export function formatCloudResponse(pr: BitbucketCloudPullRequest): any { close_source_branch: pr.close_source_branch, }; } + +export function formatServerCommit(commit: BitbucketServerCommit): FormattedCommit { + return { + hash: commit.id, + abbreviated_hash: commit.displayId, + message: commit.message, + author: { + name: commit.author.name, + email: commit.author.emailAddress, + }, + date: new Date(commit.authorTimestamp).toISOString(), + parents: commit.parents.map(p => p.id), + is_merge_commit: commit.parents.length > 1, + }; +} + +export function formatCloudCommit(commit: BitbucketCloudCommit): FormattedCommit { + // Parse the author raw string which is in format "Name " + const authorMatch = commit.author.raw.match(/^(.+?)\s*<(.+?)>$/); + const authorName = authorMatch ? authorMatch[1] : (commit.author.user?.display_name || commit.author.raw); + const authorEmail = authorMatch ? authorMatch[2] : ''; + + return { + hash: commit.hash, + abbreviated_hash: commit.hash.substring(0, 7), + message: commit.message, + author: { + name: authorName, + email: authorEmail, + }, + date: commit.date, + parents: commit.parents.map(p => p.hash), + is_merge_commit: commit.parents.length > 1, + }; +}