Skip to content

Commit 7fa2852

Browse files
authored
Merge pull request #20 from yumeminami/feature/mr-pr-detail-support
feat: Add MR/PR detail functionality - get diff and commits
2 parents eb2e587 + 5c3a977 commit 7fa2852

File tree

5 files changed

+334
-0
lines changed

5 files changed

+334
-0
lines changed

git_mcp/mcp_server.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,56 @@ async def list_my_merge_requests(
316316
)
317317

318318

319+
@mcp.tool()
320+
async def get_merge_request_diff(
321+
platform: str, project_id: str, mr_id: str, **options
322+
) -> Dict[str, Any]:
323+
"""Get diff/changes for a merge request
324+
325+
Args:
326+
platform: The platform name (e.g., 'gitlab', 'github')
327+
project_id: The project identifier
328+
mr_id: The merge request/pull request ID
329+
**options: Optional parameters:
330+
- format: Response format ('json', 'unified') - default: 'json'
331+
- include_diff: Include actual diff content (bool) - default: True
332+
333+
Returns:
334+
Dict containing:
335+
- mr_id: The merge request ID
336+
- total_changes: Summary of additions, deletions, files changed
337+
- files: List of changed files with details
338+
- diff_format: Format of the response
339+
- truncated: Whether response was truncated
340+
"""
341+
return await PlatformService.get_merge_request_diff(
342+
platform, project_id, mr_id, **options
343+
)
344+
345+
346+
@mcp.tool()
347+
async def get_merge_request_commits(
348+
platform: str, project_id: str, mr_id: str, **filters
349+
) -> Dict[str, Any]:
350+
"""Get commits for a merge request
351+
352+
Args:
353+
platform: The platform name (e.g., 'gitlab', 'github')
354+
project_id: The project identifier
355+
mr_id: The merge request/pull request ID
356+
**filters: Optional filters for commit selection
357+
358+
Returns:
359+
Dict containing:
360+
- mr_id: The merge request ID
361+
- total_commits: Number of commits
362+
- commits: List of commit details with sha, message, author, dates, etc.
363+
"""
364+
return await PlatformService.get_merge_request_commits(
365+
platform, project_id, mr_id, **filters
366+
)
367+
368+
319369
# Fork operations
320370
@mcp.tool()
321371
async def create_fork(platform: str, project_id: str, **kwargs) -> Dict[str, Any]:

git_mcp/platforms/base.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,20 @@ async def merge_merge_request(
236236
"""Merge a merge request."""
237237
pass
238238

239+
@abstractmethod
240+
async def get_merge_request_diff(
241+
self, project_id: str, mr_id: str, **options
242+
) -> Dict[str, Any]:
243+
"""Get diff/changes for a merge request."""
244+
pass
245+
246+
@abstractmethod
247+
async def get_merge_request_commits(
248+
self, project_id: str, mr_id: str, **filters
249+
) -> Dict[str, Any]:
250+
"""Get commits for a merge request."""
251+
pass
252+
239253
# Repository operations
240254
@abstractmethod
241255
async def list_branches(self, project_id: str, **filters) -> List[Dict[str, Any]]:

git_mcp/platforms/github.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,123 @@ async def get_fork_parent(self, project_id: str) -> Optional[str]:
731731
f"Failed to get fork parent for {project_id}: {e}", self.platform_name
732732
)
733733

734+
async def get_merge_request_diff(
735+
self, project_id: str, mr_id: str, **options
736+
) -> Dict[str, Any]:
737+
"""Get diff/changes for a GitHub pull request."""
738+
if not self.client:
739+
await self.authenticate()
740+
741+
try:
742+
repo = self.client.get_repo(project_id)
743+
pr = repo.get_pull(int(mr_id))
744+
745+
# Get diff format option (default: json)
746+
diff_format = options.get("format", "json")
747+
include_diff = options.get("include_diff", True)
748+
749+
# Get files changed in the PR
750+
files = list(pr.get_files())
751+
752+
# Initialize response structure
753+
response = {
754+
"mr_id": str(mr_id),
755+
"total_changes": {
756+
"additions": pr.additions,
757+
"deletions": pr.deletions,
758+
"files_changed": len(files),
759+
},
760+
"files": [],
761+
"diff_format": diff_format,
762+
"truncated": False,
763+
}
764+
765+
# Process file changes
766+
for file in files:
767+
file_info = {
768+
"path": file.filename,
769+
"status": file.status, # GitHub provides: added, removed, modified, renamed
770+
"additions": file.additions,
771+
"deletions": file.deletions,
772+
"binary": file.filename.endswith(
773+
(".png", ".jpg", ".jpeg", ".gif", ".pdf", ".zip", ".exe")
774+
),
775+
}
776+
777+
# Include diff content if requested and file is not too large
778+
if (
779+
include_diff
780+
and not file_info["binary"]
781+
and hasattr(file, "patch")
782+
and file.patch
783+
):
784+
file_info["diff"] = file.patch
785+
786+
response["files"].append(file_info)
787+
788+
return response
789+
790+
except GithubException as e:
791+
if e.status == 404:
792+
raise ResourceNotFoundError("pull_request", mr_id, self.platform_name)
793+
raise PlatformError(
794+
f"Failed to get pull request diff {mr_id}: {e}", self.platform_name
795+
)
796+
797+
async def get_merge_request_commits(
798+
self, project_id: str, mr_id: str, **filters
799+
) -> Dict[str, Any]:
800+
"""Get commits for a GitHub pull request."""
801+
if not self.client:
802+
await self.authenticate()
803+
804+
try:
805+
repo = self.client.get_repo(project_id)
806+
pr = repo.get_pull(int(mr_id))
807+
808+
# Get commits from the pull request
809+
commits = list(pr.get_commits())
810+
811+
response: Dict[str, Any] = {
812+
"mr_id": str(mr_id),
813+
"total_commits": len(commits),
814+
"commits": [],
815+
}
816+
817+
# Process each commit
818+
for commit in commits:
819+
commit_info = {
820+
"sha": commit.sha,
821+
"message": commit.commit.message,
822+
"author": commit.commit.author.name if commit.commit.author else "",
823+
"authored_date": commit.commit.author.date.isoformat()
824+
if commit.commit.author and commit.commit.author.date
825+
else "",
826+
"committer": commit.commit.committer.name
827+
if commit.commit.committer
828+
else "",
829+
"committed_date": commit.commit.committer.date.isoformat()
830+
if commit.commit.committer and commit.commit.committer.date
831+
else "",
832+
"url": commit.html_url,
833+
}
834+
835+
# Add stats if available (GitHub provides these)
836+
if hasattr(commit, "stats") and commit.stats:
837+
commit_info["additions"] = commit.stats.additions
838+
commit_info["deletions"] = commit.stats.deletions
839+
840+
response["commits"].append(commit_info)
841+
842+
return response
843+
844+
except GithubException as e:
845+
if e.status == 404:
846+
raise ResourceNotFoundError("pull_request", mr_id, self.platform_name)
847+
raise PlatformError(
848+
f"Failed to get pull request commits {mr_id}: {e}", self.platform_name
849+
)
850+
734851
def parse_branch_reference(self, branch_ref: str) -> Dict[str, Any]:
735852
"""Parse GitHub branch reference into components.
736853

git_mcp/platforms/gitlab.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,129 @@ async def get_fork_parent(self, project_id: str) -> Optional[str]:
628628
f"Failed to get fork parent for {project_id}: {e}", self.platform_name
629629
)
630630

631+
async def get_merge_request_diff(
632+
self, project_id: str, mr_id: str, **options
633+
) -> Dict[str, Any]:
634+
"""Get diff/changes for a GitLab merge request."""
635+
if not self.client:
636+
await self.authenticate()
637+
638+
try:
639+
project = self.client.projects.get(project_id)
640+
mr = project.mergerequests.get(mr_id)
641+
642+
# Get diff format option (default: json)
643+
diff_format = options.get("format", "json")
644+
include_diff = options.get("include_diff", True)
645+
646+
# Get changes using GitLab changes API
647+
changes = mr.changes()
648+
649+
# Initialize response structure
650+
response = {
651+
"mr_id": str(mr_id),
652+
"total_changes": {
653+
"additions": 0,
654+
"deletions": 0,
655+
"files_changed": len(changes.get("changes", [])),
656+
},
657+
"files": [],
658+
"diff_format": diff_format,
659+
"truncated": False,
660+
}
661+
662+
# Process file changes
663+
for change in changes.get("changes", []):
664+
file_info = {
665+
"path": change.get("new_path") or change.get("old_path", ""),
666+
"status": self._get_file_status(change),
667+
"additions": 0, # GitLab doesn't provide line counts in changes
668+
"deletions": 0,
669+
"binary": change.get("diff", "").startswith("Binary files"),
670+
}
671+
672+
# Include diff content if requested
673+
if include_diff and not file_info["binary"]:
674+
file_info["diff"] = change.get("diff", "")
675+
676+
response["files"].append(file_info)
677+
678+
# Try to get overall stats from MR
679+
if hasattr(mr, "changes_count"):
680+
response["total_changes"]["files_changed"] = mr.changes_count
681+
682+
return response
683+
684+
except GitlabError as e:
685+
if e.response_code == 404:
686+
raise ResourceNotFoundError("merge_request", mr_id, self.platform_name)
687+
raise PlatformError(
688+
f"Failed to get merge request diff {mr_id}: {e}", self.platform_name
689+
)
690+
691+
async def get_merge_request_commits(
692+
self, project_id: str, mr_id: str, **filters
693+
) -> Dict[str, Any]:
694+
"""Get commits for a GitLab merge request."""
695+
if not self.client:
696+
await self.authenticate()
697+
698+
try:
699+
project = self.client.projects.get(project_id)
700+
mr = project.mergerequests.get(mr_id)
701+
702+
# Get commits from the merge request
703+
commits = mr.commits()
704+
705+
response: Dict[str, Any] = {
706+
"mr_id": str(mr_id),
707+
"total_commits": len(commits),
708+
"commits": [],
709+
}
710+
711+
# Process each commit
712+
for commit in commits:
713+
commit_info = {
714+
"sha": commit.id,
715+
"message": commit.message,
716+
"author": commit.author_name,
717+
"authored_date": commit.authored_date,
718+
"committer": commit.committer_name,
719+
"committed_date": commit.committed_date,
720+
"url": commit.web_url if hasattr(commit, "web_url") else "",
721+
}
722+
723+
# Add stats if available
724+
if hasattr(commit, "stats"):
725+
commit_info["additions"] = getattr(commit.stats, "additions", 0)
726+
commit_info["deletions"] = getattr(commit.stats, "deletions", 0)
727+
728+
response["commits"].append(commit_info)
729+
730+
return response
731+
732+
except GitlabError as e:
733+
if e.response_code == 404:
734+
raise ResourceNotFoundError("merge_request", mr_id, self.platform_name)
735+
raise PlatformError(
736+
f"Failed to get merge request commits {mr_id}: {e}", self.platform_name
737+
)
738+
739+
def _get_file_status(self, change: Dict) -> str:
740+
"""Determine file status from GitLab change object."""
741+
new_file = change.get("new_file", False)
742+
deleted_file = change.get("deleted_file", False)
743+
renamed_file = change.get("renamed_file", False)
744+
745+
if new_file:
746+
return "added"
747+
elif deleted_file:
748+
return "deleted"
749+
elif renamed_file:
750+
return "renamed"
751+
else:
752+
return "modified"
753+
631754
def parse_branch_reference(self, branch_ref: str) -> Dict[str, Any]:
632755
"""Parse GitLab branch reference into components.
633756

git_mcp/services/platform_service.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,3 +737,33 @@ async def list_forks(
737737
"note": "Use GitHub/GitLab web interface to view forks for now",
738738
}
739739
]
740+
741+
@staticmethod
742+
async def get_merge_request_diff(
743+
platform_name: str, project_id: str, mr_id: str, **options
744+
) -> Dict[str, Any]:
745+
"""Get diff/changes for a merge request."""
746+
adapter = PlatformService.get_adapter(platform_name)
747+
diff_data = await adapter.get_merge_request_diff(project_id, mr_id, **options)
748+
749+
diff_data.update(
750+
{"platform": platform_name, "project_id": project_id, "mr_id": mr_id}
751+
)
752+
753+
return diff_data
754+
755+
@staticmethod
756+
async def get_merge_request_commits(
757+
platform_name: str, project_id: str, mr_id: str, **filters
758+
) -> Dict[str, Any]:
759+
"""Get commits for a merge request."""
760+
adapter = PlatformService.get_adapter(platform_name)
761+
commits_data = await adapter.get_merge_request_commits(
762+
project_id, mr_id, **filters
763+
)
764+
765+
commits_data.update(
766+
{"platform": platform_name, "project_id": project_id, "mr_id": mr_id}
767+
)
768+
769+
return commits_data

0 commit comments

Comments
 (0)