Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
50 changes: 50 additions & 0 deletions git_mcp/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,56 @@ async def list_my_merge_requests(
)


@mcp.tool()
async def get_merge_request_diff(
platform: str, project_id: str, mr_id: str, **options
) -> Dict[str, Any]:
"""Get diff/changes for a merge request

Args:
platform: The platform name (e.g., 'gitlab', 'github')
project_id: The project identifier
mr_id: The merge request/pull request ID
**options: Optional parameters:
- format: Response format ('json', 'unified') - default: 'json'
- include_diff: Include actual diff content (bool) - default: True

Returns:
Dict containing:
- mr_id: The merge request ID
- total_changes: Summary of additions, deletions, files changed
- files: List of changed files with details
- diff_format: Format of the response
- truncated: Whether response was truncated
"""
return await PlatformService.get_merge_request_diff(
platform, project_id, mr_id, **options
)


@mcp.tool()
async def get_merge_request_commits(
platform: str, project_id: str, mr_id: str, **filters
) -> Dict[str, Any]:
"""Get commits for a merge request

Args:
platform: The platform name (e.g., 'gitlab', 'github')
project_id: The project identifier
mr_id: The merge request/pull request ID
**filters: Optional filters for commit selection

Returns:
Dict containing:
- mr_id: The merge request ID
- total_commits: Number of commits
- commits: List of commit details with sha, message, author, dates, etc.
"""
return await PlatformService.get_merge_request_commits(
platform, project_id, mr_id, **filters
)


# Fork operations
@mcp.tool()
async def create_fork(platform: str, project_id: str, **kwargs) -> Dict[str, Any]:
Expand Down
14 changes: 14 additions & 0 deletions git_mcp/platforms/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,20 @@ async def merge_merge_request(
"""Merge a merge request."""
pass

@abstractmethod
async def get_merge_request_diff(
self, project_id: str, mr_id: str, **options
) -> Dict[str, Any]:
"""Get diff/changes for a merge request."""
pass

@abstractmethod
async def get_merge_request_commits(
self, project_id: str, mr_id: str, **filters
) -> Dict[str, Any]:
"""Get commits for a merge request."""
pass

# Repository operations
@abstractmethod
async def list_branches(self, project_id: str, **filters) -> List[Dict[str, Any]]:
Expand Down
117 changes: 117 additions & 0 deletions git_mcp/platforms/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,123 @@ async def get_fork_parent(self, project_id: str) -> Optional[str]:
f"Failed to get fork parent for {project_id}: {e}", self.platform_name
)

async def get_merge_request_diff(
self, project_id: str, mr_id: str, **options
) -> Dict[str, Any]:
"""Get diff/changes for a GitHub pull request."""
if not self.client:
await self.authenticate()

try:
repo = self.client.get_repo(project_id)
pr = repo.get_pull(int(mr_id))

# Get diff format option (default: json)
diff_format = options.get("format", "json")
include_diff = options.get("include_diff", True)

# Get files changed in the PR
files = list(pr.get_files())

# Initialize response structure
response = {
"mr_id": str(mr_id),
"total_changes": {
"additions": pr.additions,
"deletions": pr.deletions,
"files_changed": len(files),
},
"files": [],
"diff_format": diff_format,
"truncated": False,
}

# Process file changes
for file in files:
file_info = {
"path": file.filename,
"status": file.status, # GitHub provides: added, removed, modified, renamed
"additions": file.additions,
"deletions": file.deletions,
"binary": file.filename.endswith(
(".png", ".jpg", ".jpeg", ".gif", ".pdf", ".zip", ".exe")
),
}

# Include diff content if requested and file is not too large
if (
include_diff
and not file_info["binary"]
and hasattr(file, "patch")
and file.patch
):
file_info["diff"] = file.patch

response["files"].append(file_info)

return response

except GithubException as e:
if e.status == 404:
raise ResourceNotFoundError("pull_request", mr_id, self.platform_name)
raise PlatformError(
f"Failed to get pull request diff {mr_id}: {e}", self.platform_name
)

async def get_merge_request_commits(
self, project_id: str, mr_id: str, **filters
) -> Dict[str, Any]:
"""Get commits for a GitHub pull request."""
if not self.client:
await self.authenticate()

try:
repo = self.client.get_repo(project_id)
pr = repo.get_pull(int(mr_id))

# Get commits from the pull request
commits = list(pr.get_commits())

response: Dict[str, Any] = {
"mr_id": str(mr_id),
"total_commits": len(commits),
"commits": [],
}

# Process each commit
for commit in commits:
commit_info = {
"sha": commit.sha,
"message": commit.commit.message,
"author": commit.commit.author.name if commit.commit.author else "",
"authored_date": commit.commit.author.date.isoformat()
if commit.commit.author and commit.commit.author.date
else "",
"committer": commit.commit.committer.name
if commit.commit.committer
else "",
"committed_date": commit.commit.committer.date.isoformat()
if commit.commit.committer and commit.commit.committer.date
else "",
"url": commit.html_url,
}

# Add stats if available (GitHub provides these)
if hasattr(commit, "stats") and commit.stats:
commit_info["additions"] = commit.stats.additions
commit_info["deletions"] = commit.stats.deletions

response["commits"].append(commit_info)

return response

except GithubException as e:
if e.status == 404:
raise ResourceNotFoundError("pull_request", mr_id, self.platform_name)
raise PlatformError(
f"Failed to get pull request commits {mr_id}: {e}", self.platform_name
)

def parse_branch_reference(self, branch_ref: str) -> Dict[str, Any]:
"""Parse GitHub branch reference into components.

Expand Down
123 changes: 123 additions & 0 deletions git_mcp/platforms/gitlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,129 @@ async def get_fork_parent(self, project_id: str) -> Optional[str]:
f"Failed to get fork parent for {project_id}: {e}", self.platform_name
)

async def get_merge_request_diff(
self, project_id: str, mr_id: str, **options
) -> Dict[str, Any]:
"""Get diff/changes for a GitLab merge request."""
if not self.client:
await self.authenticate()

try:
project = self.client.projects.get(project_id)
mr = project.mergerequests.get(mr_id)

# Get diff format option (default: json)
diff_format = options.get("format", "json")
include_diff = options.get("include_diff", True)

# Get changes using GitLab changes API
changes = mr.changes()

# Initialize response structure
response = {
"mr_id": str(mr_id),
"total_changes": {
"additions": 0,
"deletions": 0,
"files_changed": len(changes.get("changes", [])),
},
"files": [],
"diff_format": diff_format,
"truncated": False,
}

# Process file changes
for change in changes.get("changes", []):
file_info = {
"path": change.get("new_path") or change.get("old_path", ""),
"status": self._get_file_status(change),
"additions": 0, # GitLab doesn't provide line counts in changes
"deletions": 0,
"binary": change.get("diff", "").startswith("Binary files"),
}

# Include diff content if requested
if include_diff and not file_info["binary"]:
file_info["diff"] = change.get("diff", "")

response["files"].append(file_info)

# Try to get overall stats from MR
if hasattr(mr, "changes_count"):
response["total_changes"]["files_changed"] = mr.changes_count

return response

except GitlabError as e:
if e.response_code == 404:
raise ResourceNotFoundError("merge_request", mr_id, self.platform_name)
raise PlatformError(
f"Failed to get merge request diff {mr_id}: {e}", self.platform_name
)

async def get_merge_request_commits(
self, project_id: str, mr_id: str, **filters
) -> Dict[str, Any]:
"""Get commits for a GitLab merge request."""
if not self.client:
await self.authenticate()

try:
project = self.client.projects.get(project_id)
mr = project.mergerequests.get(mr_id)

# Get commits from the merge request
commits = mr.commits()

response: Dict[str, Any] = {
"mr_id": str(mr_id),
"total_commits": len(commits),
"commits": [],
}

# Process each commit
for commit in commits:
commit_info = {
"sha": commit.id,
"message": commit.message,
"author": commit.author_name,
"authored_date": commit.authored_date,
"committer": commit.committer_name,
"committed_date": commit.committed_date,
"url": commit.web_url if hasattr(commit, "web_url") else "",
}

# Add stats if available
if hasattr(commit, "stats"):
commit_info["additions"] = getattr(commit.stats, "additions", 0)
commit_info["deletions"] = getattr(commit.stats, "deletions", 0)

response["commits"].append(commit_info)

return response

except GitlabError as e:
if e.response_code == 404:
raise ResourceNotFoundError("merge_request", mr_id, self.platform_name)
raise PlatformError(
f"Failed to get merge request commits {mr_id}: {e}", self.platform_name
)

def _get_file_status(self, change: Dict) -> str:
"""Determine file status from GitLab change object."""
new_file = change.get("new_file", False)
deleted_file = change.get("deleted_file", False)
renamed_file = change.get("renamed_file", False)

if new_file:
return "added"
elif deleted_file:
return "deleted"
elif renamed_file:
return "renamed"
else:
return "modified"

def parse_branch_reference(self, branch_ref: str) -> Dict[str, Any]:
"""Parse GitLab branch reference into components.

Expand Down
30 changes: 30 additions & 0 deletions git_mcp/services/platform_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -737,3 +737,33 @@ async def list_forks(
"note": "Use GitHub/GitLab web interface to view forks for now",
}
]

@staticmethod
async def get_merge_request_diff(
platform_name: str, project_id: str, mr_id: str, **options
) -> Dict[str, Any]:
"""Get diff/changes for a merge request."""
adapter = PlatformService.get_adapter(platform_name)
diff_data = await adapter.get_merge_request_diff(project_id, mr_id, **options)

diff_data.update(
{"platform": platform_name, "project_id": project_id, "mr_id": mr_id}
)

return diff_data

@staticmethod
async def get_merge_request_commits(
platform_name: str, project_id: str, mr_id: str, **filters
) -> Dict[str, Any]:
"""Get commits for a merge request."""
adapter = PlatformService.get_adapter(platform_name)
commits_data = await adapter.get_merge_request_commits(
project_id, mr_id, **filters
)

commits_data.update(
{"platform": platform_name, "project_id": project_id, "mr_id": mr_id}
)

return commits_data
Loading