|
11 | 11 | importlogging
|
12 | 12 | importos
|
13 | 13 | importsignal
|
14 |
| -fromsubprocessimportPopen,PIPE,DEVNULL |
| 14 | +fromsubprocessimportPopen,PIPE,DEVNULL,run,CalledProcessError |
15 | 15 | importsubprocess
|
16 | 16 | importthreading
|
17 | 17 | fromtextwrapimportdedent
|
| 18 | +frompathlibimportPath |
18 | 19 |
|
19 |
| -fromgit.compatimportdefenc,force_bytes,safe_decode |
| 20 | +fromgit.compatimportdefenc,force_bytes,safe_decode,is_win |
20 | 21 | fromgit.excimport (
|
21 | 22 | CommandError,
|
22 | 23 | GitCommandError,
|
@@ -305,6 +306,175 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
|
305 | 306 | the top level ``__init__``.
|
306 | 307 | """
|
307 | 308 |
|
| 309 | +_bash_exec_env_var="GIT_PYTHON_BASH_EXECUTABLE" |
| 310 | + |
| 311 | +bash_exec_name="bash" |
| 312 | +"""Default bash command that should work on Linux, Windows, and other systems.""" |
| 313 | + |
| 314 | +GIT_PYTHON_BASH_EXECUTABLE=None |
| 315 | +"""Provide the full path to the bash executable. Otherwise it assumes bash is in the path. |
| 316 | +
|
| 317 | + Note that the bash executable is actually found during the refresh step in |
| 318 | + the top level ``__init__``. |
| 319 | + """ |
| 320 | + |
| 321 | +@classmethod |
| 322 | +def_get_default_bash_path(cls): |
| 323 | +# Assumes that, if user is running in Windows, they probably are using |
| 324 | +# Git for Windows, which includes Git BASH and should be associated |
| 325 | +# with the configured Git command set in `refresh()`. Regardless of |
| 326 | +# if the Git command assumes it is installed in (root)/cmd/git.exe or |
| 327 | +# (root)/bin/git.exe, the root is always up two levels from the git |
| 328 | +# command. Try going up to levels from the currently configured |
| 329 | +# git command, then navigate to (root)/bin/bash.exe. If this exists, |
| 330 | +# prefer it over the WSL version in System32, direct access to which |
| 331 | +# is reportedly deprecated. Fail back to default "bash.exe" if |
| 332 | +# the Git for Windows lookup doesn't work. |
| 333 | +# |
| 334 | +# This addresses issues where git hooks are intended to run assuming |
| 335 | +# the "native" Windows environment as seen by git.exe rather than |
| 336 | +# inside the git sandbox of WSL, which is likely configured |
| 337 | +# independetly of the Windows Git. A noteworthy example are repos with |
| 338 | +# Git LFS, where Git LFS may be installed in Windows but not in WSL. |
| 339 | +ifnotis_win: |
| 340 | +return'bash' |
| 341 | +try: |
| 342 | +wheregit=run(['where',Git.GIT_PYTHON_GIT_EXECUTABLE], |
| 343 | +check=True,stdout=PIPE).stdout |
| 344 | +exceptCalledProcessError: |
| 345 | +return'bash.exe' |
| 346 | +gitpath=Path(wheregit.decode(defenc).splitlines()[0]) |
| 347 | +gitroot=gitpath.parent.parent |
| 348 | +gitbash=gitroot/'bin'/'bash.exe' |
| 349 | +returnstr(gitbash)ifgitbash.existselse'bash.exe' |
| 350 | + |
| 351 | +@classmethod |
| 352 | +defrefresh_bash(cls,path:Union[None,PathLike]=None)->bool: |
| 353 | +"""This gets called by the refresh function (see the top level __init__).""" |
| 354 | +# Discern which path to refresh with. |
| 355 | +ifpathisnotNone: |
| 356 | +new_bash=os.path.expanduser(path) |
| 357 | +new_bash=os.path.abspath(new_bash) |
| 358 | +else: |
| 359 | +new_bash=os.environ.get(cls._bash_exec_env_var) |
| 360 | +ifnew_bashisNone: |
| 361 | +new_bash=cls._get_default_bash_path() |
| 362 | + |
| 363 | +# Keep track of the old and new bash executable path. |
| 364 | +old_bash=cls.GIT_PYTHON_BASH_EXECUTABLE |
| 365 | +cls.GIT_PYTHON_BASH_EXECUTABLE=new_bash |
| 366 | + |
| 367 | +# Test if the new git executable path is valid. A GitCommandNotFound error is |
| 368 | +# spawned by us. A PermissionError is spawned if the git executable cannot be |
| 369 | +# executed for whatever reason. |
| 370 | +has_bash=False |
| 371 | +try: |
| 372 | +run([cls.GIT_PYTHON_BASH_EXECUTABLE,'--version']) |
| 373 | +has_bash=True |
| 374 | +exceptCalledProcessError: |
| 375 | +pass |
| 376 | + |
| 377 | +# Warn or raise exception if test failed. |
| 378 | +ifnothas_bash: |
| 379 | +err= ( |
| 380 | +dedent( |
| 381 | +f"""\ |
| 382 | + Bad bash executable. |
| 383 | + The bash executable must be specified in one of the following ways: |
| 384 | + - be included in your $PATH |
| 385 | + - be set via ${cls._bash_exec_env_var} |
| 386 | + - explicitly set via git.refresh_bash() |
| 387 | + """ |
| 388 | + ) |
| 389 | + ) |
| 390 | + |
| 391 | +# Revert to whatever the old_bash was. |
| 392 | +cls.GIT_PYTHON_BASH_EXECUTABLE=old_bash |
| 393 | + |
| 394 | +ifold_bashisNone: |
| 395 | +# On the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is None) we only |
| 396 | +# are quiet, warn, or error depending on the GIT_PYTHON_REFRESH value. |
| 397 | + |
| 398 | +# Determine what the user wants to happen during the initial refresh we |
| 399 | +# expect GIT_PYTHON_REFRESH to either be unset or be one of the |
| 400 | +# following values: |
| 401 | +# |
| 402 | +# 0|q|quiet|s|silence|n|none |
| 403 | +# 1|w|warn|warning |
| 404 | +# 2|r|raise|e|error |
| 405 | + |
| 406 | +mode=os.environ.get(cls._refresh_env_var,"raise").lower() |
| 407 | + |
| 408 | +quiet= ["quiet","q","silence","s","none","n","0"] |
| 409 | +warn= ["warn","w","warning","1"] |
| 410 | +error= ["error","e","raise","r","2"] |
| 411 | + |
| 412 | +ifmodeinquiet: |
| 413 | +pass |
| 414 | +elifmodeinwarnormodeinerror: |
| 415 | +err= ( |
| 416 | +dedent( |
| 417 | +"""\ |
| 418 | + %s |
| 419 | + All commit hook commands will error until this is rectified. |
| 420 | +
|
| 421 | + This initial warning can be silenced or aggravated in the future by setting the |
| 422 | + $%s environment variable. Use one of the following values: |
| 423 | + - %s: for no warning or exception |
| 424 | + - %s: for a printed warning |
| 425 | + - %s: for a raised exception |
| 426 | +
|
| 427 | + Example: |
| 428 | + export %s=%s |
| 429 | + """ |
| 430 | + ) |
| 431 | +% ( |
| 432 | +err, |
| 433 | +cls._refresh_env_var, |
| 434 | +"|".join(quiet), |
| 435 | +"|".join(warn), |
| 436 | +"|".join(error), |
| 437 | +cls._refresh_env_var, |
| 438 | +quiet[0], |
| 439 | + ) |
| 440 | + ) |
| 441 | + |
| 442 | +ifmodeinwarn: |
| 443 | +print("WARNING: %s"%err) |
| 444 | +else: |
| 445 | +raiseImportError(err) |
| 446 | +else: |
| 447 | +err= ( |
| 448 | +dedent( |
| 449 | +"""\ |
| 450 | + %s environment variable has been set but it has been set with an invalid value. |
| 451 | +
|
| 452 | + Use only the following values: |
| 453 | + - %s: for no warning or exception |
| 454 | + - %s: for a printed warning |
| 455 | + - %s: for a raised exception |
| 456 | + """ |
| 457 | + ) |
| 458 | +% ( |
| 459 | +cls._refresh_env_var, |
| 460 | +"|".join(quiet), |
| 461 | +"|".join(warn), |
| 462 | +"|".join(error), |
| 463 | + ) |
| 464 | + ) |
| 465 | +raiseImportError(err) |
| 466 | + |
| 467 | +# We get here if this was the init refresh and the refresh mode was not |
| 468 | +# error. Go ahead and set the GIT_PYTHON_BASH_EXECUTABLE such that we |
| 469 | +# discern the difference between a first import and a second import. |
| 470 | +cls.GIT_PYTHON_BASH_EXECUTABLE=cls.bash_exec_name |
| 471 | +else: |
| 472 | +# After the first refresh (when GIT_PYTHON_BASH_EXECUTABLE is no longer |
| 473 | +# None) we raise an exception. |
| 474 | +raiseGitCommandNotFound("bash",err) |
| 475 | + |
| 476 | +returnhas_bash |
| 477 | + |
308 | 478 | @classmethod
|
309 | 479 | defrefresh(cls,path:Union[None,PathLike]=None)->bool:
|
310 | 480 | """This gets called by the refresh function (see the top level __init__)."""
|
|