3636from packaging .specifiers import Specifier
3737from termcolor import colored
3838
39- from ts_utils .metadata import ObsoleteMetadata , StubMetadata , read_metadata , update_metadata
39+ from ts_utils .metadata import (
40+ ObsoleteMetadata ,
41+ StubMetadata ,
42+ StubtestSettings ,
43+ read_metadata ,
44+ read_stubtest_settings ,
45+ update_metadata ,
46+ )
4047from ts_utils .paths import PYRIGHT_CONFIG , STUBS_PATH , distribution_path
4148from ts_utils .stubs import third_party_stubs
4249
@@ -772,36 +779,41 @@ async def update_pull_request_label(*, pr_number: int, session: aiohttp.ClientSe
772779 response .raise_for_status ()
773780
774781
782+ def remote_branch_exists (branch : str ) -> bool :
783+ return (
784+ subprocess .run (["git" , "show-ref" , "--verify" , "--quiet" , f"refs/remotes/origin/{ branch } " ], check = False ).returncode == 0
785+ )
786+
787+
775788def has_non_stubsabot_commits (branch : str ) -> bool :
776789 assert not branch .startswith ("origin/" )
777- try :
778- # commits on origin/branch that are not on branch or are
779- # patch equivalent to a commit on branch
780- output = subprocess .check_output (
781- ["git" , "log" , "--right-only" , "--pretty=%an" , "--cherry-pick" , f"{ branch } ...origin/{ branch } " ],
782- stderr = subprocess .DEVNULL ,
783- )
784- return bool (set (output .splitlines ()) - {b"stubsabot" })
785- except subprocess .CalledProcessError :
786- # origin/branch does not exist
790+
791+ if not remote_branch_exists (branch ):
787792 return False
788793
794+ # commits on origin/branch that are not on branch or are
795+ # patch equivalent to a commit on branch
796+ output = subprocess .check_output (
797+ ["git" , "log" , "--right-only" , "--pretty=%an" , "--cherry-pick" , f"{ branch } ...origin/{ branch } " ], stderr = subprocess .DEVNULL
798+ )
799+ return bool (set (output .splitlines ()) - {b"stubsabot" })
800+
789801
790802def latest_commit_is_different_to_last_commit_on_origin (branch : str ) -> bool :
791803 assert not branch .startswith ("origin/" )
792- try :
793- # https://www.git-scm.com/docs/git-range-diff
794- # If the number of lines is >1,
795- # it indicates that something about our commit is different to the last commit
796- # (Could be the commit "content", or the commit message).
797- commit_comparison = subprocess .run (
798- ["git" , "range-diff" , f"origin/{ branch } ~1..origin/{ branch } " , "HEAD~1..HEAD" ], check = True , capture_output = True
799- )
800- return len (commit_comparison .stdout .splitlines ()) > 1
801- except subprocess .CalledProcessError :
802- # origin/branch does not exist
804+
805+ if not remote_branch_exists (branch ):
803806 return True
804807
808+ # https://www.git-scm.com/docs/git-range-diff
809+ # If the number of lines is >1,
810+ # it indicates that something about our commit is different to the last commit
811+ # (Could be the commit "content", or the commit message).
812+ commit_comparison = subprocess .run (
813+ ["git" , "range-diff" , f"origin/{ branch } ~1..origin/{ branch } " , "HEAD~1..HEAD" ], check = True , capture_output = True
814+ )
815+ return len (commit_comparison .stdout .splitlines ()) > 1
816+
805817
806818class RemoteConflictError (Exception ):
807819 pass
@@ -810,7 +822,12 @@ class RemoteConflictError(Exception):
810822def somewhat_safe_force_push (branch : str ) -> None :
811823 if has_non_stubsabot_commits (branch ):
812824 raise RemoteConflictError (f"origin/{ branch } has non-stubsabot changes that are not on { branch } !" )
813- subprocess .check_call (["git" , "push" , "origin" , branch , "--force" ])
825+ result = subprocess .run (["git" , "push" , "--quiet" , "origin" , branch , "--force" ], text = True , capture_output = True , check = False )
826+ if result .returncode :
827+ # Capture both streams to suppress stderr on success without losing it on failure
828+ sys .stdout .write (result .stdout )
829+ sys .stderr .write (result .stderr )
830+ result .check_returncode ()
814831
815832
816833def normalize (name : str ) -> str :
@@ -824,15 +841,13 @@ def normalize(name: str) -> str:
824841BRANCH_PREFIX = "stubsabot"
825842
826843
827- def get_update_pr_body (update : Update , metadata : Mapping [ str , Any ] ) -> str :
844+ def get_update_pr_body (update : Update , settings : StubtestSettings ) -> str :
828845 body = "\n " .join (f"{ k } : { v } " for k , v in update .links .items ())
829846
830847 if update .diff_analysis is not None :
831848 body += f"\n \n { update .diff_analysis } "
832849
833- stubtest_settings : dict [str , Any ] = metadata .get ("tool" , {}).get ("stubtest" , {})
834- stubtest_will_run = not stubtest_settings .get ("skip" , False )
835- if stubtest_will_run :
850+ if not settings .skip :
836851 body += textwrap .dedent ("""
837852
838853 If stubtest fails for this PR:
@@ -866,66 +881,55 @@ def remove_stubs(distribution: str) -> None:
866881 f .writelines (lines )
867882
868883
869- async def suggest_typeshed_update (update : Update , session : aiohttp .ClientSession , action_level : ActionLevel ) -> None :
884+ def get_commit_message (action : Update | Obsolete | Remove ) -> tuple [str , str ]:
885+ """Return git commit message title and body."""
886+ match action :
887+ case Update ():
888+ stubtest_settings = read_stubtest_settings (action .distribution )
889+ return (
890+ f"[stubsabot] Bump { action .distribution } to { action .new_version } " ,
891+ get_update_pr_body (action , stubtest_settings ),
892+ )
893+ case Obsolete ():
894+ return (
895+ f"[stubsabot] Mark { action .distribution } as obsolete since { action .obsolete_since_version } " ,
896+ "\n " .join (f"{ k } : { v } " for k , v in action .links .items ()),
897+ )
898+ case Remove ():
899+ return (
900+ f"[stubsabot] Remove { action .distribution } as { action .reason } " ,
901+ "\n " .join (f"{ k } : { v } " for k , v in action .links .items ()),
902+ )
903+
904+
905+ def run_action (action : Update | Obsolete | Remove ) -> None :
906+ match action :
907+ case Update ():
908+ update_metadata (action .distribution , version = action .new_version )
909+ case Obsolete ():
910+ obsolete_t = cast (dict [str , object ], tomlkit .inline_table ())
911+ obsolete_t .update ({"version" : action .obsolete_since_version , "date" : action .obsolete_since_date .date ().isoformat ()})
912+ update_metadata (action .distribution , obsolete_since = obsolete_t )
913+ case Remove ():
914+ remove_stubs (action .distribution )
915+
916+
917+ async def process_typeshed_change (
918+ action : Update | Obsolete | Remove , session : aiohttp .ClientSession , action_level : ActionLevel
919+ ) -> None :
870920 if action_level <= ActionLevel .nothing :
871921 return
872- title = f"[stubsabot] Bump { update .distribution } to { update .new_version } "
873- async with _repo_lock :
874- branch_name = f"{ BRANCH_PREFIX } /{ normalize (update .distribution )} "
875- subprocess .check_call (["git" , "checkout" , "-B" , branch_name , "origin/main" ])
876- meta = update_metadata (update .distribution , version = update .new_version )
877- body = get_update_pr_body (update , meta )
878- subprocess .check_call (["git" , "commit" , "--all" , "-m" , f"{ title } \n \n { body } " ])
879- if action_level <= ActionLevel .local :
880- return
881- if not latest_commit_is_different_to_last_commit_on_origin (branch_name ):
882- print (f"No pushing to origin required: origin/{ branch_name } exists and requires no changes!" )
883- return
884- somewhat_safe_force_push (branch_name )
885- if action_level <= ActionLevel .fork :
886- return
887922
888- await create_or_update_pull_request (title = title , body = body , branch_name = branch_name , session = session )
889-
890-
891- async def suggest_typeshed_obsolete (obsolete : Obsolete , session : aiohttp .ClientSession , action_level : ActionLevel ) -> None :
892- if action_level <= ActionLevel .nothing :
893- return
894- title = f"[stubsabot] Mark { obsolete .distribution } as obsolete since { obsolete .obsolete_since_version } "
895- async with _repo_lock :
896- branch_name = f"{ BRANCH_PREFIX } /{ normalize (obsolete .distribution )} "
897- subprocess .check_call (["git" , "checkout" , "-B" , branch_name , "origin/main" ])
898- obsolete_t = cast (dict [str , object ], tomlkit .inline_table ())
899- obsolete_t .update ({"version" : obsolete .obsolete_since_version , "date" : obsolete .obsolete_since_date .date ().isoformat ()})
900- update_metadata (obsolete .distribution , obsolete_since = obsolete_t )
901- body = "\n " .join (f"{ k } : { v } " for k , v in obsolete .links .items ())
902- subprocess .check_call (["git" , "commit" , "--all" , "-m" , f"{ title } \n \n { body } " ])
903- if action_level <= ActionLevel .local :
904- return
905- if not latest_commit_is_different_to_last_commit_on_origin (branch_name ):
906- print (f"No PR required: origin/{ branch_name } exists and requires no changes!" )
907- return
908- somewhat_safe_force_push (branch_name )
909- if action_level <= ActionLevel .fork :
910- return
911-
912- await create_or_update_pull_request (title = title , body = body , branch_name = branch_name , session = session )
913-
914-
915- async def suggest_typeshed_remove (remove : Remove , session : aiohttp .ClientSession , action_level : ActionLevel ) -> None :
916- if action_level <= ActionLevel .nothing :
917- return
918- title = f"[stubsabot] Remove { remove .distribution } as { remove .reason } "
919923 async with _repo_lock :
920- branch_name = f"{ BRANCH_PREFIX } /{ normalize (remove .distribution )} "
921- subprocess .check_call (["git" , "checkout" , "-B" , branch_name , "origin/main" ])
922- remove_stubs ( remove . distribution )
923- body = " \n " . join ( f" { k } : { v } " for k , v in remove . links . items () )
924- subprocess .check_call (["git" , "commit" , "--all" , "-m" , f"{ title } \n \n { body } " ])
924+ branch_name = f"{ BRANCH_PREFIX } /{ normalize (action .distribution )} "
925+ subprocess .check_call (["git" , "checkout" , "--quiet" , "- B" , branch_name , "origin/main" ])
926+ run_action ( action )
927+ title , body = get_commit_message ( action )
928+ subprocess .check_call (["git" , "commit" , "--quiet" , "-- all" , "-m" , f"{ title } \n \n { body } " ])
925929 if action_level <= ActionLevel .local :
926930 return
927931 if not latest_commit_is_different_to_last_commit_on_origin (branch_name ):
928- print (f"No pushing to origin required: origin/{ branch_name } exists and requires no changes!" )
932+ print (f"No pushing to origin required: ' origin/{ branch_name } ' exists and requires no changes!" )
929933 return
930934 somewhat_safe_force_push (branch_name )
931935 if action_level <= ActionLevel .fork :
@@ -995,13 +999,13 @@ async def main() -> int:
995999
9961000 action_count = 0
9971001 for task in asyncio .as_completed (tasks ):
998- update = await task
999- print (f"{ update .distribution } ... " , end = "" )
1000- print (update )
1002+ action = await task
1003+ print (f"{ action .distribution } ... " , end = "" )
1004+ print (action )
10011005
1002- if isinstance (update , NoUpdate ):
1006+ if isinstance (action , NoUpdate ):
10031007 continue
1004- if isinstance (update , Error ):
1008+ if isinstance (action , Error ):
10051009 error = True
10061010 continue
10071011
@@ -1011,16 +1015,8 @@ async def main() -> int:
10111015 action_count += 1
10121016
10131017 try :
1014- if isinstance (update , Update ):
1015- await suggest_typeshed_update (update , session , action_level = args .action_level )
1016- continue
1017- if isinstance (update , Obsolete ):
1018- await suggest_typeshed_obsolete (update , session , action_level = args .action_level )
1019- continue
1020- # Redundant, but keeping for extra runtime validation
1021- if isinstance (update , Remove ): # pyright: ignore[reportUnnecessaryIsInstance]
1022- await suggest_typeshed_remove (update , session , action_level = args .action_level )
1023- continue
1018+ await process_typeshed_change (action , session , action_level = args .action_level )
1019+ continue
10241020 except RemoteConflictError as e :
10251021 print (colored (f"... but ran into { type (e ).__qualname__ } : { e } " , "red" ))
10261022 continue
@@ -1029,7 +1025,7 @@ async def main() -> int:
10291025 # if you need to cleanup, try:
10301026 # git branch -D $(git branch --list 'stubsabot/*')
10311027 if args .action_level >= ActionLevel .local and original_branch :
1032- subprocess .check_call (["git" , "checkout" , original_branch ])
1028+ subprocess .check_call (["git" , "checkout" , "--quiet" , original_branch ])
10331029
10341030 return 1 if error else 0
10351031
0 commit comments