Skip to content
Open
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
3 changes: 3 additions & 0 deletions docs/available_tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,6 @@ This document lists the tools available in the Gerrit MCP Server, extracted from
- **get_bugs_from_cl**: Extracts bug IDs from the commit message of a CL.
- **post_review_comment**: Posts a review comment on a specific line of a file
in a CL.
- **cherry_pick_change**: Cherry-picks a single change to a destination branch.
- **cherry_pick_chain**: Cherry-picks an entire relation chain (series of
dependent changes) to a destination branch, maintaining dependency order.
3 changes: 3 additions & 0 deletions docs/use_cases.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ Here are a few examples of how you can use the Gerrit MCP Server with a language
| **Advanced** | "Revert CL 12345 with the message 'Broke the build'." | `revert_change` |
| | "What other changes would be submitted with CL 67890?" | `changes_submitted_together` |
| | "Create a new change in project 'test-project', branch 'dev', with subject 'Test new feature'." | `create_change` |
| **Cherry-pick** | "Cherry-pick CL 12345 to the 'release-1.0' branch." | `cherry_pick_change` |
| | "Cherry-pick the entire chain of CL 67890 to the 'stable' branch." | `cherry_pick_chain` |
| | "Where has CL 12345 been cherry-picked to?" | `get_change_details` + `query_changes` |

## Data Analysis Use Cases

Expand Down
207 changes: 206 additions & 1 deletion gerrit_mcp_server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,211 @@ async def revert_submission(
raise e


@mcp.tool()
async def cherry_pick_change(
change_id: str,
destination: str,
revision_id: str = "current",
message: Optional[str] = None,
keep_reviewers: bool = False,
allow_conflicts: bool = True,
allow_empty: bool = False,
gerrit_base_url: Optional[str] = None,
):
"""
Cherry-picks a single change to a destination branch.
"""
config = load_gerrit_config()
gerrit_hosts = config.get("gerrit_hosts", [])
base_url = _normalize_gerrit_url(
_get_gerrit_base_url(gerrit_base_url), gerrit_hosts
)
url = f"{base_url}/changes/{change_id}/revisions/{revision_id}/cherrypick"
payload = {"destination": destination}
if message:
payload["message"] = message
if keep_reviewers:
payload["keep_reviewers"] = True
if allow_conflicts:
payload["allow_conflicts"] = True
if allow_empty:
payload["allow_empty"] = True
args = _create_post_args(url, payload)

try:
result_str = await run_curl(args, base_url)
cherry_info = json.loads(result_str)
if "id" in cherry_info and "_number" in cherry_info:
output = (
f"Successfully cherry-picked CL {change_id} to branch {destination}.\n"
f"New CL created: {cherry_info['_number']}\n"
f"Subject: {cherry_info['subject']}"
)
return [{"type": "text", "text": output}]
else:
return [
{
"type": "text",
"text": f"Failed to cherry-pick CL {change_id}. Response: {result_str}",
}
]
except json.JSONDecodeError:
return [
{
"type": "text",
"text": f"Failed to cherry-pick CL {change_id}. Response: {result_str}",
}
]
except Exception as e:
with open(LOG_FILE_PATH, "a") as log_file:
log_file.write(
f"[gerrit-mcp-server] Error cherry-picking CL {change_id}: {e}\n"
)
raise e


@mcp.tool()
async def cherry_pick_chain(
change_id: str,
destination: str,
revision_id: str = "current",
keep_reviewers: bool = False,
allow_conflicts: bool = True,
allow_empty: bool = False,
gerrit_base_url: Optional[str] = None,
):
"""
Cherry-picks an entire relation chain (series of dependent changes) to a
destination branch, maintaining dependency order. Fetches the related changes
for the given change, then cherry-picks each one sequentially from parent to
child so the chain structure is preserved on the destination branch.
"""
config = load_gerrit_config()
gerrit_hosts = config.get("gerrit_hosts", [])
base_url = _normalize_gerrit_url(
_get_gerrit_base_url(gerrit_base_url), gerrit_hosts
)

# Step 1: Fetch the relation chain
related_url = (
f"{base_url}/changes/{change_id}/revisions/{revision_id}/related"
)
try:
result_str = await run_curl([related_url], base_url)
related_info = json.loads(result_str)
except (json.JSONDecodeError, Exception) as e:
return [
{
"type": "text",
"text": f"Failed to fetch related changes for CL {change_id}: {e}",
}
]

changes = related_info.get("changes", [])
if not changes:
return [
{
"type": "text",
"text": (
f"No related changes found for CL {change_id}. "
"Use cherry_pick_change for a single change."
),
}
]

# Step 2: Reverse so we cherry-pick parent-to-child
# (the /related API returns child-first, ancestors last)
changes.reverse()

results = []
parent_commit = None

for i, related_change in enumerate(changes):
cid = str(related_change["_change_number"])
rid = str(related_change.get("_revision_number", "current"))

payload = {"destination": destination}
if keep_reviewers:
payload["keep_reviewers"] = True
if allow_conflicts:
payload["allow_conflicts"] = True
if allow_empty:
payload["allow_empty"] = True
if parent_commit:
payload["base"] = parent_commit

cherry_url = (
f"{base_url}/changes/{cid}/revisions/{rid}/cherrypick"
)
args = _create_post_args(cherry_url, payload)

try:
result_str = await run_curl(args, base_url)
cherry_info = json.loads(result_str)

if "id" not in cherry_info or "_number" not in cherry_info:
error_output = (
f"Cherry-pick chain failed at CL {cid} "
f"({i + 1}/{len(changes)}).\n"
f"Response: {result_str}\n"
)
if results:
error_output += "Successfully cherry-picked before failure:\n"
for r in results:
error_output += (
f"- CL {r['original']} -> new CL {r['new_number']}: "
f"{r['subject']}\n"
)
return [{"type": "text", "text": error_output}]

# The cherry-pick response doesn't include current_revision
# by default. Fetch the new change with CURRENT_REVISION to
# get the commit SHA needed as 'base' for the next cherry-pick.
new_cl = cherry_info["_number"]
detail_url = (
f"{base_url}/changes/{new_cl}?o=CURRENT_REVISION"
)
detail_str = await run_curl([detail_url], base_url)
detail_info = json.loads(detail_str)
parent_commit = detail_info.get("current_revision")

results.append(
{
"original": cid,
"new_number": new_cl,
"subject": cherry_info.get("subject", ""),
}
)
except Exception as e:
error_output = (
f"Cherry-pick chain failed at CL {cid} "
f"({i + 1}/{len(changes)}): {e}\n"
)
if results:
error_output += "Successfully cherry-picked before failure:\n"
for r in results:
error_output += (
f"- CL {r['original']} -> new CL {r['new_number']}: "
f"{r['subject']}\n"
)
with open(LOG_FILE_PATH, "a") as log_file:
log_file.write(
f"[gerrit-mcp-server] Error cherry-picking chain at CL {cid}: {e}\n"
)
return [{"type": "text", "text": error_output}]

# Step 3: Report success
output = (
f"Successfully cherry-picked chain of {len(results)} changes "
f"to branch {destination}:\n"
)
for r in results:
output += (
f"- CL {r['original']} -> new CL {r['new_number']}: {r['subject']}\n"
)
return [{"type": "text", "text": output}]


@mcp.tool()
async def create_change(
project: str,
Expand Down Expand Up @@ -1257,4 +1462,4 @@ def cli_main(argv: List[str]):
if __name__ == "__main__":
cli_main(sys.argv)

app = mcp.streamable_http_app()
app = mcp.streamable_http_app()
Loading