1010from typing import Any
1111from typing import Callable
1212from typing import Generator
13+ from typing import Literal
14+ from typing import Optional
15+ from typing import overload
1316
1417import cruft
1518import typer
1619from cookiecutter .utils import work_in
17- from pygments .lexers import q
1820from typer .models import OptionInfo
1921
2022
@@ -35,12 +37,24 @@ def remove_readonly(func: Callable[[str], Any], path: str, _: Any) -> None:
3537 func (path )
3638
3739
38- def run_command (command : str , * args : str ) -> subprocess .CompletedProcess :
40+ @overload
41+ def run_command (command : str , * args : str , ignore_error : Literal [True ]) -> Optional [subprocess .CompletedProcess ]:
42+ ...
43+
44+
45+ @overload
46+ def run_command (command : str , * args : str , ignore_error : Literal [False ] = ...) -> subprocess .CompletedProcess :
47+ ...
48+
49+
50+ def run_command (command : str , * args : str , ignore_error : bool = False ) -> Optional [subprocess .CompletedProcess ]:
3951 """Runs the provided command in a subprocess."""
4052 try :
4153 process = subprocess .run ([command , * args ], check = True , capture_output = True , text = True )
4254 return process
4355 except subprocess .CalledProcessError as error :
56+ if ignore_error :
57+ return None
4458 print (error .stdout , end = "" )
4559 print (error .stderr , end = "" , file = sys .stderr )
4660 raise
@@ -50,6 +64,28 @@ def run_command(command: str, *args: str) -> subprocess.CompletedProcess:
5064uv : partial [subprocess .CompletedProcess ] = partial (run_command , "uv" )
5165
5266
67+ def require_clean_and_up_to_date_repo () -> None :
68+ """Checks if the repo is clean and up to date with any important branches."""
69+ git ("fetch" )
70+ git ("status" , "--porcelain" )
71+ if not is_branch_synced_with_remote ("develop" ):
72+ raise ValueError ("develop is not synced with origin/develop" )
73+ if not is_branch_synced_with_remote ("main" ):
74+ raise ValueError ("main is not synced with origin/main" )
75+ if not is_ancestor ("main" , "develop" ):
76+ raise ValueError ("main is not an ancestor of develop" )
77+
78+
79+ def is_branch_synced_with_remote (branch : str ) -> bool :
80+ """Checks if the branch is synced with its remote."""
81+ return is_ancestor (branch , f"origin/{ branch } " ) and is_ancestor (f"origin/{ branch } " , branch )
82+
83+
84+ def is_ancestor (ancestor : str , descendent : str ) -> bool :
85+ """Checks if the branch is synced with its remote."""
86+ return git ("merge-base" , "--is-ancestor" , ancestor , descendent ).returncode == 0
87+
88+
5389@contextmanager
5490def in_new_demo (
5591 demos_cache_folder : Path ,
0 commit comments