@@ -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 ()
786991async def create_change (
787992 project : str ,
@@ -1257,4 +1462,4 @@ def cli_main(argv: List[str]):
12571462if __name__ == "__main__" :
12581463 cli_main (sys .argv )
12591464
1260- app = mcp .streamable_http_app ()
1465+ app = mcp .streamable_http_app ()
0 commit comments