Skip to content

Commit eb2e587

Browse files
yumeminamiclaude
andcommitted
Implement GitLab fork MR support with target_project_id parameter
- Add cross-project merge request support in GitLab adapter - Implement parse_branch_reference method for GitLab platform - Update MCP server interface with target_project_id parameter - Add comprehensive documentation for GitLab fork MR usage - Update slash commands with GitLab fork examples - Enhance CLAUDE.md with GitLab vs GitHub fork workflow comparison This enables complete fork-to-upstream workflow for GitLab projects, complementing the existing GitHub fork PR support. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 4166430 commit eb2e587

File tree

5 files changed

+220
-38
lines changed

5 files changed

+220
-38
lines changed

CLAUDE.md

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ uv run git-mcp-server --help
3535

3636
# Test MCP server directly
3737
echo '{"jsonrpc": "2.0", "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "test", "version": "1.0.0"}}, "id": 1}' | git-mcp-server
38+
39+
# Build distribution packages
40+
uv build
41+
42+
# Run development version in interactive MCP mode
43+
uv run mcp dev git_mcp/mcp_server.py
3844
```
3945

4046
### Installation & Setup
@@ -141,7 +147,9 @@ When working on this codebase:
141147

142148
## Dependencies
143149

144-
**Python Version**: Requires Python >=3.13
150+
**Python Version**: Requires Python >=3.13 (specified in pyproject.toml)
151+
152+
**Build System**: Uses hatchling as the build backend
145153

146154
Core dependencies (from pyproject.toml):
147155
- `python-gitlab>=4.4.0` - GitLab API client
@@ -164,6 +172,8 @@ Development tools:
164172
- `pytest-asyncio>=0.23.0` - Async testing support
165173
- `pip-audit>=2.0.0` - Security vulnerability scanning
166174

175+
**Pre-commit Configuration**: Includes ruff (linting/formatting), bandit (security), mypy (type checking), and standard file checks. Run with `uv run pre-commit run --all-files`.
176+
167177
## API Documentation
168178

169179
**Platform API References:**
@@ -202,3 +212,64 @@ Development tools:
202212
- Bandit security scanning is configured to skip safe subprocess usage (`B404`, `B603`, `B607`)
203213
- Platform connections are tested before storing configuration
204214
- Username auto-detection reduces manual configuration and potential errors
215+
216+
## MCP Tools Reference
217+
218+
The MCP server exposes approximately 25 tools organized into categories:
219+
220+
**Platform Management**: `list_platforms`, `test_platform_connection`, `refresh_platform_username`, `get_platform_config`, `get_current_user_info`
221+
222+
**Project Operations**: `list_projects`, `get_project_details`, `create_project`, `delete_project`
223+
224+
**Issue Management**: `list_issues`, `list_all_issues`, `list_my_issues`, `get_issue_details`, `get_issue_by_url`, `create_issue`, `update_issue`, `close_issue`
225+
226+
**Merge Requests**: `list_merge_requests`, `get_merge_request_details`, `create_merge_request` (支持 GitLab 跨项目 MR), `list_my_merge_requests`
227+
228+
**Repository Operations**: `create_fork`, `get_fork_info`, `list_forks`
229+
230+
All tools support async operations and return structured data for integration with AI assistants.
231+
232+
## GitLab Fork MR 支持
233+
234+
**GitLab 跨项目 Merge Request** 现已完全支持,使用 `target_project_id` 参数:
235+
236+
### GitLab Fork MR 用法
237+
238+
```python
239+
# GitLab 跨项目 MR(从 fork 到上游项目)
240+
create_merge_request(
241+
platform="gitlab",
242+
project_id="456", # fork 项目 ID(源项目)
243+
source_branch="feature-branch",
244+
target_branch="main",
245+
title="Fix issue #123",
246+
target_project_id="123", # 上游项目 ID(目标项目)
247+
description="详细描述..."
248+
)
249+
250+
# GitLab 同项目 MR(现有功能保持不变)
251+
create_merge_request(
252+
platform="gitlab",
253+
project_id="123",
254+
source_branch="feature-branch",
255+
target_branch="main",
256+
title="Fix issue #123"
257+
)
258+
```
259+
260+
### GitHub Fork PR 对比
261+
262+
```python
263+
# GitHub 跨仓库 PR(使用分支引用格式)
264+
create_merge_request(
265+
platform="github",
266+
project_id="upstream-owner/upstream-repo", # 目标上游仓库
267+
source_branch="fork-owner:feature-branch", # fork 分支引用
268+
target_branch="main",
269+
title="Fix issue #123"
270+
)
271+
```
272+
273+
**关键区别**
274+
- **GitLab**: 使用 `target_project_id` 参数指定目标项目
275+
- **GitHub**: 使用 `owner:branch` 格式在 `source_branch` 中指定跨仓库引用

git_mcp/claude_commands/pr.md

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Analyze the changes made thoroughly and consider multiple ways to present them c
6262

6363
5. **Fork Workflow Examples**
6464

65-
**Example 1: Fork-to-Upstream PR**
65+
**Example 1: GitHub Fork-to-Upstream PR**
6666
```
6767
# Check if current repo is a fork
6868
get_fork_info("github", "myuser/upstream-project")
@@ -78,11 +78,28 @@ Analyze the changes made thoroughly and consider multiple ways to present them c
7878
)
7979
```
8080

81-
**Example 2: Same-Repository PR (existing behavior)**
81+
**Example 2: GitLab Fork-to-Upstream MR**
8282
```
83+
# Check if current repo is a fork
84+
get_fork_info("gitlab", "456") # Fork project ID
85+
86+
# If it's a fork, create MR to upstream
8387
create_merge_request(
84-
platform="github",
85-
project_id="myuser/my-project",
88+
platform="gitlab",
89+
project_id="456", # Fork project ID (source)
90+
source_branch="feature-branch", # Simple branch name
91+
target_branch="main", # Upstream main branch
92+
target_project_id="123", # Upstream project ID (target)
93+
title="Fix issue #123: Add new feature",
94+
description="Implements feature as requested in issue..."
95+
)
96+
```
97+
98+
**Example 3: Same-Repository PR/MR (existing behavior)**
99+
```
100+
create_merge_request(
101+
platform="github", # or "gitlab"
102+
project_id="myuser/my-project", # or GitLab project ID
86103
source_branch="feature-branch", # Simple branch name
87104
target_branch="main",
88105
title="Fix issue #123: Add new feature",

git_mcp/gemini_commands/pr.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,17 @@ Analyze the changes made thoroughly and consider multiple ways to present them c
2222
4. **Create Pull/Merge Request with Fork Support**
2323
Use `create_merge_request()` with enhanced fork support:
2424
25-
**For Fork-to-Upstream PRs:**
25+
**For GitHub Fork-to-Upstream PRs:**
2626
- Source branch: `username:branch-name` format (automatically detected)
2727
- Target repository: Upstream repository (parent)
2828
- Target branch: Usually `main` or `master`
2929
30-
**For Same-Repository PRs:**
30+
**For GitLab Fork-to-Upstream MRs:**
31+
- Source branch: `branch-name` format (simple branch name)
32+
- Use `target_project_id` parameter for upstream project
33+
- Target branch: Usually `main` or `master`
34+
35+
**For Same-Repository PRs/MRs:**
3136
- Source branch: `branch-name` format (current behavior)
3237
- Target repository: Current repository
3338
- Target branch: As specified or default

git_mcp/mcp_server.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,9 +223,36 @@ async def create_merge_request(
223223
target_branch: str = "main",
224224
description: Optional[str] = None,
225225
assignee: Optional[str] = None,
226+
target_project_id: Optional[str] = None,
226227
**kwargs,
227228
) -> Dict[str, Any]:
228-
"""Create a new merge request"""
229+
"""Create a new merge request with cross-project support
230+
231+
Args:
232+
platform: Git platform name (github, gitlab, etc.)
233+
project_id: Source project ID (for cross-project MRs) or project ID (for same-project MRs)
234+
title: Merge request title
235+
source_branch: Source branch name or 'owner:branch' format for cross-repo
236+
target_branch: Target branch name (default: 'main')
237+
description: Optional merge request description
238+
assignee: Optional assignee username
239+
target_project_id: Optional target project ID for cross-project merge requests
240+
**kwargs: Additional platform-specific parameters
241+
242+
Returns:
243+
Dict containing merge request details
244+
245+
Examples:
246+
# Same-project MR
247+
create_merge_request("gitlab", "123", "Fix bug", "feature-branch", "main")
248+
249+
# Cross-project MR (fork to upstream)
250+
create_merge_request("gitlab", "456", "Fix bug", "feature-branch", "main",
251+
target_project_id="123")
252+
253+
# GitHub cross-repo PR (using branch format)
254+
create_merge_request("github", "upstream/repo", "Fix bug", "fork-owner:feature-branch", "main")
255+
"""
229256
create_kwargs = kwargs.copy()
230257

231258
# Some MCP clients pass a single 'kwargs' argument as a JSON string.
@@ -266,6 +293,9 @@ async def create_merge_request(
266293
if assignee:
267294
create_kwargs["assignee_username"] = assignee
268295

296+
if target_project_id:
297+
create_kwargs["target_project_id"] = target_project_id
298+
269299
print(f"Debug: MCP Server - kwargs being passed: {list(create_kwargs.keys())}")
270300
return await PlatformService.create_merge_request(
271301
platform, project_id, title, source_branch, target_branch, **create_kwargs

git_mcp/platforms/gitlab.py

Lines changed: 89 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -342,41 +342,100 @@ async def create_merge_request(
342342
title: str,
343343
**kwargs,
344344
) -> MergeRequestResource:
345-
"""Create a new GitLab merge request."""
345+
"""Create a new GitLab merge request with cross-project support."""
346346
if not self.client:
347347
await self.authenticate()
348348

349349
try:
350-
project = self.client.projects.get(project_id)
350+
# Parse branch references to handle cross-project scenarios
351+
source_ref = self.parse_branch_reference(source_branch)
352+
target_ref = self.parse_branch_reference(target_branch)
351353

352-
# Verify branches exist before creating merge request
353-
try:
354-
# Check if source branch exists
355-
source_branches = project.branches.list(search=source_branch)
356-
if not any(b.name == source_branch for b in source_branches):
357-
raise PlatformError(
358-
f"Source branch '{source_branch}' not found in project {project_id}. "
359-
f"Make sure the branch is pushed to the remote repository.",
360-
self.platform_name,
354+
# Determine source and target projects
355+
source_project_id = project_id
356+
target_project_id = kwargs.get("target_project_id")
357+
358+
# Handle cross-project merge request setup
359+
if target_project_id:
360+
# Cross-project MR: source project creates MR to target project
361+
print(
362+
f"Debug: GitLab - Creating cross-project MR from project {source_project_id} to {target_project_id}"
363+
)
364+
source_project = self.client.projects.get(source_project_id)
365+
target_project = self.client.projects.get(target_project_id)
366+
367+
# Verify source branch exists in source project
368+
try:
369+
source_branches = source_project.branches.list(
370+
search=source_ref["branch"]
371+
)
372+
if not any(b.name == source_ref["branch"] for b in source_branches):
373+
raise PlatformError(
374+
f"Source branch '{source_ref['branch']}' not found in project {source_project_id}. "
375+
f"Make sure the branch is pushed to the remote repository.",
376+
self.platform_name,
377+
)
378+
except GitlabError as branch_error:
379+
print(
380+
f"Warning: Could not verify source branch existence: {branch_error}"
361381
)
362382

363-
# Check if target branch exists
364-
target_branches = project.branches.list(search=target_branch)
365-
if not any(b.name == target_branch for b in target_branches):
366-
raise PlatformError(
367-
f"Target branch '{target_branch}' not found in project {project_id}",
368-
self.platform_name,
383+
# Verify target branch exists in target project
384+
try:
385+
target_branches = target_project.branches.list(
386+
search=target_ref["branch"]
369387
)
370-
except GitlabError as branch_error:
371-
print(f"Warning: Could not verify branch existence: {branch_error}")
372-
# Continue with merge request creation even if branch check fails
388+
if not any(b.name == target_ref["branch"] for b in target_branches):
389+
raise PlatformError(
390+
f"Target branch '{target_ref['branch']}' not found in project {target_project_id}",
391+
self.platform_name,
392+
)
393+
except GitlabError as branch_error:
394+
print(
395+
f"Warning: Could not verify target branch existence: {branch_error}"
396+
)
397+
398+
# Use source project to create the MR
399+
project = source_project
400+
else:
401+
# Same-project MR (existing behavior)
402+
print(
403+
f"Debug: GitLab - Creating same-project MR in project {source_project_id}"
404+
)
405+
project = self.client.projects.get(source_project_id)
406+
407+
# Verify branches exist in same project
408+
try:
409+
# Check if source branch exists
410+
source_branches = project.branches.list(search=source_ref["branch"])
411+
if not any(b.name == source_ref["branch"] for b in source_branches):
412+
raise PlatformError(
413+
f"Source branch '{source_ref['branch']}' not found in project {source_project_id}. "
414+
f"Make sure the branch is pushed to the remote repository.",
415+
self.platform_name,
416+
)
417+
418+
# Check if target branch exists
419+
target_branches = project.branches.list(search=target_ref["branch"])
420+
if not any(b.name == target_ref["branch"] for b in target_branches):
421+
raise PlatformError(
422+
f"Target branch '{target_ref['branch']}' not found in project {source_project_id}",
423+
self.platform_name,
424+
)
425+
except GitlabError as branch_error:
426+
print(f"Warning: Could not verify branch existence: {branch_error}")
427+
# Continue with merge request creation even if branch check fails
373428

374429
mr_data = {
375-
"source_branch": source_branch,
376-
"target_branch": target_branch,
430+
"source_branch": source_ref["branch"],
431+
"target_branch": target_ref["branch"],
377432
"title": title,
378433
}
379434

435+
# Add target_project_id for cross-project MR
436+
if target_project_id:
437+
mr_data["target_project_id"] = int(target_project_id)
438+
380439
# Handle assignee parameter conversion
381440
if "assignee_username" in kwargs:
382441
assignee_username = kwargs.pop("assignee_username")
@@ -573,28 +632,28 @@ def parse_branch_reference(self, branch_ref: str) -> Dict[str, Any]:
573632
"""Parse GitLab branch reference into components.
574633
575634
Args:
576-
branch_ref: Branch reference in format 'branch' or 'owner:branch'
635+
branch_ref: Branch reference in format 'branch' or 'project:branch'
577636
578637
Returns:
579-
Dict with keys: 'owner' (optional), 'branch', 'is_cross_repo'
638+
Dict with keys: 'project' (optional), 'branch', 'is_cross_project'
580639
"""
581640
if ":" in branch_ref:
582-
# Cross-repository reference: 'owner:branch'
641+
# Cross-project reference: 'project:branch'
583642
parts = branch_ref.split(":", 1)
584643
if len(parts) != 2:
585644
raise ValueError(f"Invalid branch reference format: {branch_ref}")
586645

587-
owner, branch = parts
588-
if not owner or not branch:
646+
project, branch = parts
647+
if not project or not branch:
589648
raise ValueError(f"Invalid branch reference format: {branch_ref}")
590649

591-
return {"owner": owner, "branch": branch, "is_cross_repo": True} # type: ignore[dict-item]
650+
return {"project": project, "branch": branch, "is_cross_project": True} # type: ignore[dict-item]
592651
else:
593-
# Same-repository reference: 'branch'
652+
# Same-project reference: 'branch'
594653
if not branch_ref:
595654
raise ValueError("Branch reference cannot be empty")
596655

597-
return {"branch": branch_ref, "is_cross_repo": False} # type: ignore[dict-item]
656+
return {"branch": branch_ref, "is_cross_project": False} # type: ignore[dict-item]
598657

599658
# Helper methods
600659
def _convert_to_project_resource(self, project) -> ProjectResource:

0 commit comments

Comments
 (0)