Skip to content

Commit f3dec2b

Browse files
committed
feat: Add cherry-pick tools
Changes: - Add cherry_pick_change tool for cherry-picking a single change to a destination branch - Add cherry_pick_chain tool for cherry-picking an entire relation chain preserving dependency order - Update docs with new cherry-pick tools and use cases Change-Id: I66fe8602b24fd12f05d2138d5fcf634646c785d3
1 parent 5666642 commit f3dec2b

5 files changed

Lines changed: 736 additions & 1 deletion

File tree

docs/available_tools.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,6 @@ This document lists the tools available in the Gerrit MCP Server, extracted from
3737
- **get_bugs_from_cl**: Extracts bug IDs from the commit message of a CL.
3838
- **post_review_comment**: Posts a review comment on a specific line of a file
3939
in a CL.
40+
- **cherry_pick_change**: Cherry-picks a single change to a destination branch.
41+
- **cherry_pick_chain**: Cherry-picks an entire relation chain (series of
42+
dependent changes) to a destination branch, maintaining dependency order.

docs/use_cases.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ Here are a few examples of how you can use the Gerrit MCP Server with a language
2323
| **Advanced** | "Revert CL 12345 with the message 'Broke the build'." | `revert_change` |
2424
| | "What other changes would be submitted with CL 67890?" | `changes_submitted_together` |
2525
| | "Create a new change in project 'test-project', branch 'dev', with subject 'Test new feature'." | `create_change` |
26+
| **Cherry-pick** | "Cherry-pick CL 12345 to the 'release-1.0' branch." | `cherry_pick_change` |
27+
| | "Cherry-pick the entire chain of CL 67890 to the 'stable' branch." | `cherry_pick_chain` |
28+
| | "Where has CL 12345 been cherry-picked to?" | `get_change_details` + `query_changes` |
2629

2730
## Data Analysis Use Cases
2831

gerrit_mcp_server/main.py

Lines changed: 206 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,211 @@ async def revert_submission(
782782
raise e
783783

784784

785+
@mcp.tool()
786+
async def cherry_pick_change(
787+
change_id: str,
788+
destination: str,
789+
revision_id: str = "current",
790+
message: Optional[str] = None,
791+
keep_reviewers: bool = False,
792+
allow_conflicts: bool = True,
793+
allow_empty: bool = False,
794+
gerrit_base_url: Optional[str] = None,
795+
):
796+
"""
797+
Cherry-picks a single change to a destination branch.
798+
"""
799+
config = load_gerrit_config()
800+
gerrit_hosts = config.get("gerrit_hosts", [])
801+
base_url = _normalize_gerrit_url(
802+
_get_gerrit_base_url(gerrit_base_url), gerrit_hosts
803+
)
804+
url = f"{base_url}/changes/{change_id}/revisions/{revision_id}/cherrypick"
805+
payload = {"destination": destination}
806+
if message:
807+
payload["message"] = message
808+
if keep_reviewers:
809+
payload["keep_reviewers"] = True
810+
if allow_conflicts:
811+
payload["allow_conflicts"] = True
812+
if allow_empty:
813+
payload["allow_empty"] = True
814+
args = _create_post_args(url, payload)
815+
816+
try:
817+
result_str = await run_curl(args, base_url)
818+
cherry_info = json.loads(result_str)
819+
if "id" in cherry_info and "_number" in cherry_info:
820+
output = (
821+
f"Successfully cherry-picked CL {change_id} to branch {destination}.\n"
822+
f"New CL created: {cherry_info['_number']}\n"
823+
f"Subject: {cherry_info['subject']}"
824+
)
825+
return [{"type": "text", "text": output}]
826+
else:
827+
return [
828+
{
829+
"type": "text",
830+
"text": f"Failed to cherry-pick CL {change_id}. Response: {result_str}",
831+
}
832+
]
833+
except json.JSONDecodeError:
834+
return [
835+
{
836+
"type": "text",
837+
"text": f"Failed to cherry-pick CL {change_id}. Response: {result_str}",
838+
}
839+
]
840+
except Exception as e:
841+
with open(LOG_FILE_PATH, "a") as log_file:
842+
log_file.write(
843+
f"[gerrit-mcp-server] Error cherry-picking CL {change_id}: {e}\n"
844+
)
845+
raise e
846+
847+
848+
@mcp.tool()
849+
async def cherry_pick_chain(
850+
change_id: str,
851+
destination: str,
852+
revision_id: str = "current",
853+
keep_reviewers: bool = False,
854+
allow_conflicts: bool = True,
855+
allow_empty: bool = False,
856+
gerrit_base_url: Optional[str] = None,
857+
):
858+
"""
859+
Cherry-picks an entire relation chain (series of dependent changes) to a
860+
destination branch, maintaining dependency order. Fetches the related changes
861+
for the given change, then cherry-picks each one sequentially from parent to
862+
child so the chain structure is preserved on the destination branch.
863+
"""
864+
config = load_gerrit_config()
865+
gerrit_hosts = config.get("gerrit_hosts", [])
866+
base_url = _normalize_gerrit_url(
867+
_get_gerrit_base_url(gerrit_base_url), gerrit_hosts
868+
)
869+
870+
# Step 1: Fetch the relation chain
871+
related_url = (
872+
f"{base_url}/changes/{change_id}/revisions/{revision_id}/related"
873+
)
874+
try:
875+
result_str = await run_curl([related_url], base_url)
876+
related_info = json.loads(result_str)
877+
except (json.JSONDecodeError, Exception) as e:
878+
return [
879+
{
880+
"type": "text",
881+
"text": f"Failed to fetch related changes for CL {change_id}: {e}",
882+
}
883+
]
884+
885+
changes = related_info.get("changes", [])
886+
if not changes:
887+
return [
888+
{
889+
"type": "text",
890+
"text": (
891+
f"No related changes found for CL {change_id}. "
892+
"Use cherry_pick_change for a single change."
893+
),
894+
}
895+
]
896+
897+
# Step 2: Reverse so we cherry-pick parent-to-child
898+
# (the /related API returns child-first, ancestors last)
899+
changes.reverse()
900+
901+
results = []
902+
parent_commit = None
903+
904+
for i, related_change in enumerate(changes):
905+
cid = str(related_change["_change_number"])
906+
rid = str(related_change.get("_revision_number", "current"))
907+
908+
payload = {"destination": destination}
909+
if keep_reviewers:
910+
payload["keep_reviewers"] = True
911+
if allow_conflicts:
912+
payload["allow_conflicts"] = True
913+
if allow_empty:
914+
payload["allow_empty"] = True
915+
if parent_commit:
916+
payload["base"] = parent_commit
917+
918+
cherry_url = (
919+
f"{base_url}/changes/{cid}/revisions/{rid}/cherrypick"
920+
)
921+
args = _create_post_args(cherry_url, payload)
922+
923+
try:
924+
result_str = await run_curl(args, base_url)
925+
cherry_info = json.loads(result_str)
926+
927+
if "id" not in cherry_info or "_number" not in cherry_info:
928+
error_output = (
929+
f"Cherry-pick chain failed at CL {cid} "
930+
f"({i + 1}/{len(changes)}).\n"
931+
f"Response: {result_str}\n"
932+
)
933+
if results:
934+
error_output += "Successfully cherry-picked before failure:\n"
935+
for r in results:
936+
error_output += (
937+
f"- CL {r['original']} -> new CL {r['new_number']}: "
938+
f"{r['subject']}\n"
939+
)
940+
return [{"type": "text", "text": error_output}]
941+
942+
# The cherry-pick response doesn't include current_revision
943+
# by default. Fetch the new change with CURRENT_REVISION to
944+
# get the commit SHA needed as 'base' for the next cherry-pick.
945+
new_cl = cherry_info["_number"]
946+
detail_url = (
947+
f"{base_url}/changes/{new_cl}?o=CURRENT_REVISION"
948+
)
949+
detail_str = await run_curl([detail_url], base_url)
950+
detail_info = json.loads(detail_str)
951+
parent_commit = detail_info.get("current_revision")
952+
953+
results.append(
954+
{
955+
"original": cid,
956+
"new_number": new_cl,
957+
"subject": cherry_info.get("subject", ""),
958+
}
959+
)
960+
except Exception as e:
961+
error_output = (
962+
f"Cherry-pick chain failed at CL {cid} "
963+
f"({i + 1}/{len(changes)}): {e}\n"
964+
)
965+
if results:
966+
error_output += "Successfully cherry-picked before failure:\n"
967+
for r in results:
968+
error_output += (
969+
f"- CL {r['original']} -> new CL {r['new_number']}: "
970+
f"{r['subject']}\n"
971+
)
972+
with open(LOG_FILE_PATH, "a") as log_file:
973+
log_file.write(
974+
f"[gerrit-mcp-server] Error cherry-picking chain at CL {cid}: {e}\n"
975+
)
976+
return [{"type": "text", "text": error_output}]
977+
978+
# Step 3: Report success
979+
output = (
980+
f"Successfully cherry-picked chain of {len(results)} changes "
981+
f"to branch {destination}:\n"
982+
)
983+
for r in results:
984+
output += (
985+
f"- CL {r['original']} -> new CL {r['new_number']}: {r['subject']}\n"
986+
)
987+
return [{"type": "text", "text": output}]
988+
989+
785990
@mcp.tool()
786991
async def create_change(
787992
project: str,
@@ -1257,4 +1462,4 @@ def cli_main(argv: List[str]):
12571462
if __name__ == "__main__":
12581463
cli_main(sys.argv)
12591464

1260-
app = mcp.streamable_http_app()
1465+
app = mcp.streamable_http_app()

0 commit comments

Comments
 (0)