GitPython 3.1.44__py3-none-any.whl → 3.1.46__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- git/__init__.py +1 -1
- git/cmd.py +239 -222
- git/config.py +26 -17
- git/diff.py +3 -2
- git/index/base.py +29 -15
- git/index/fun.py +12 -6
- git/index/typ.py +16 -3
- git/index/util.py +2 -2
- git/objects/base.py +2 -2
- git/objects/blob.py +2 -1
- git/objects/commit.py +3 -3
- git/objects/submodule/base.py +14 -4
- git/objects/tree.py +7 -3
- git/refs/head.py +1 -5
- git/refs/log.py +2 -2
- git/refs/reference.py +2 -1
- git/refs/symbolic.py +39 -31
- git/refs/tag.py +4 -4
- git/repo/base.py +35 -38
- git/repo/fun.py +9 -3
- git/types.py +2 -2
- git/util.py +24 -20
- {GitPython-3.1.44.dist-info → gitpython-3.1.46.dist-info}/METADATA +16 -5
- gitpython-3.1.46.dist-info/RECORD +44 -0
- {GitPython-3.1.44.dist-info → gitpython-3.1.46.dist-info}/WHEEL +1 -1
- {GitPython-3.1.44.dist-info → gitpython-3.1.46.dist-info/licenses}/AUTHORS +1 -0
- GitPython-3.1.44.dist-info/RECORD +0 -44
- {GitPython-3.1.44.dist-info → gitpython-3.1.46.dist-info/licenses}/LICENSE +0 -0
- {GitPython-3.1.44.dist-info → gitpython-3.1.46.dist-info}/top_level.txt +0 -0
git/__init__.py
CHANGED
git/cmd.py
CHANGED
|
@@ -60,6 +60,11 @@ from typing import (
|
|
|
60
60
|
overload,
|
|
61
61
|
)
|
|
62
62
|
|
|
63
|
+
if sys.version_info >= (3, 10):
|
|
64
|
+
from typing import TypeAlias
|
|
65
|
+
else:
|
|
66
|
+
from typing_extensions import TypeAlias
|
|
67
|
+
|
|
63
68
|
from git.types import Literal, PathLike, TBD
|
|
64
69
|
|
|
65
70
|
if TYPE_CHECKING:
|
|
@@ -207,7 +212,7 @@ def handle_process_output(
|
|
|
207
212
|
)
|
|
208
213
|
if stderr_handler:
|
|
209
214
|
error_str: Union[str, bytes] = (
|
|
210
|
-
"error: process killed because it timed out.
|
|
215
|
+
f"error: process killed because it timed out. kill_after_timeout={kill_after_timeout} seconds"
|
|
211
216
|
)
|
|
212
217
|
if not decode_streams and isinstance(p_stderr, BinaryIO):
|
|
213
218
|
# Assume stderr_handler needs binary input.
|
|
@@ -268,12 +273,12 @@ if sys.platform == "win32":
|
|
|
268
273
|
if shell:
|
|
269
274
|
# The original may be immutable, or the caller may reuse it. Mutate a copy.
|
|
270
275
|
env = {} if env is None else dict(env)
|
|
271
|
-
env["NoDefaultCurrentDirectoryInExePath"] = "1" # The "1" can be
|
|
276
|
+
env["NoDefaultCurrentDirectoryInExePath"] = "1" # The "1" can be any value.
|
|
272
277
|
|
|
273
278
|
# When not using a shell, the current process does the search in a
|
|
274
279
|
# CreateProcessW API call, so the variable must be set in our environment. With
|
|
275
280
|
# a shell, that's unnecessary if https://github.com/python/cpython/issues/101283
|
|
276
|
-
# is patched. In Python versions where it is unpatched,
|
|
281
|
+
# is patched. In Python versions where it is unpatched, in the rare case the
|
|
277
282
|
# ComSpec environment variable is unset, the search for the shell itself is
|
|
278
283
|
# unsafe. Setting NoDefaultCurrentDirectoryInExePath in all cases, as done here,
|
|
279
284
|
# is simpler and protects against that. (As above, the "1" can be any value.)
|
|
@@ -308,6 +313,230 @@ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], exc
|
|
|
308
313
|
|
|
309
314
|
## -- End Utilities -- @}
|
|
310
315
|
|
|
316
|
+
|
|
317
|
+
class _AutoInterrupt:
|
|
318
|
+
"""Process wrapper that terminates the wrapped process on finalization.
|
|
319
|
+
|
|
320
|
+
This kills/interrupts the stored process instance once this instance goes out of
|
|
321
|
+
scope. It is used to prevent processes piling up in case iterators stop reading.
|
|
322
|
+
|
|
323
|
+
All attributes are wired through to the contained process object.
|
|
324
|
+
|
|
325
|
+
The wait method is overridden to perform automatic status code checking and possibly
|
|
326
|
+
raise.
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
__slots__ = ("proc", "args", "status")
|
|
330
|
+
|
|
331
|
+
# If this is non-zero it will override any status code during _terminate, used
|
|
332
|
+
# to prevent race conditions in testing.
|
|
333
|
+
_status_code_if_terminate: int = 0
|
|
334
|
+
|
|
335
|
+
def __init__(self, proc: Union[None, subprocess.Popen], args: Any) -> None:
|
|
336
|
+
self.proc = proc
|
|
337
|
+
self.args = args
|
|
338
|
+
self.status: Union[int, None] = None
|
|
339
|
+
|
|
340
|
+
def _terminate(self) -> None:
|
|
341
|
+
"""Terminate the underlying process."""
|
|
342
|
+
if self.proc is None:
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
proc = self.proc
|
|
346
|
+
self.proc = None
|
|
347
|
+
if proc.stdin:
|
|
348
|
+
proc.stdin.close()
|
|
349
|
+
if proc.stdout:
|
|
350
|
+
proc.stdout.close()
|
|
351
|
+
if proc.stderr:
|
|
352
|
+
proc.stderr.close()
|
|
353
|
+
# Did the process finish already so we have a return code?
|
|
354
|
+
try:
|
|
355
|
+
if proc.poll() is not None:
|
|
356
|
+
self.status = self._status_code_if_terminate or proc.poll()
|
|
357
|
+
return
|
|
358
|
+
except OSError as ex:
|
|
359
|
+
_logger.info("Ignored error after process had died: %r", ex)
|
|
360
|
+
|
|
361
|
+
# It can be that nothing really exists anymore...
|
|
362
|
+
if os is None or getattr(os, "kill", None) is None:
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
# Try to kill it.
|
|
366
|
+
try:
|
|
367
|
+
proc.terminate()
|
|
368
|
+
status = proc.wait() # Ensure the process goes away.
|
|
369
|
+
|
|
370
|
+
self.status = self._status_code_if_terminate or status
|
|
371
|
+
except OSError as ex:
|
|
372
|
+
_logger.info("Ignored error after process had died: %r", ex)
|
|
373
|
+
# END exception handling
|
|
374
|
+
|
|
375
|
+
def __del__(self) -> None:
|
|
376
|
+
self._terminate()
|
|
377
|
+
|
|
378
|
+
def __getattr__(self, attr: str) -> Any:
|
|
379
|
+
return getattr(self.proc, attr)
|
|
380
|
+
|
|
381
|
+
# TODO: Bad choice to mimic `proc.wait()` but with different args.
|
|
382
|
+
def wait(self, stderr: Union[None, str, bytes] = b"") -> int:
|
|
383
|
+
"""Wait for the process and return its status code.
|
|
384
|
+
|
|
385
|
+
:param stderr:
|
|
386
|
+
Previously read value of stderr, in case stderr is already closed.
|
|
387
|
+
|
|
388
|
+
:warn:
|
|
389
|
+
May deadlock if output or error pipes are used and not handled separately.
|
|
390
|
+
|
|
391
|
+
:raise git.exc.GitCommandError:
|
|
392
|
+
If the return status is not 0.
|
|
393
|
+
"""
|
|
394
|
+
if stderr is None:
|
|
395
|
+
stderr_b = b""
|
|
396
|
+
stderr_b = force_bytes(data=stderr, encoding="utf-8")
|
|
397
|
+
status: Union[int, None]
|
|
398
|
+
if self.proc is not None:
|
|
399
|
+
status = self.proc.wait()
|
|
400
|
+
p_stderr = self.proc.stderr
|
|
401
|
+
else: # Assume the underlying proc was killed earlier or never existed.
|
|
402
|
+
status = self.status
|
|
403
|
+
p_stderr = None
|
|
404
|
+
|
|
405
|
+
def read_all_from_possibly_closed_stream(stream: Union[IO[bytes], None]) -> bytes:
|
|
406
|
+
if stream:
|
|
407
|
+
try:
|
|
408
|
+
return stderr_b + force_bytes(stream.read())
|
|
409
|
+
except (OSError, ValueError):
|
|
410
|
+
return stderr_b or b""
|
|
411
|
+
else:
|
|
412
|
+
return stderr_b or b""
|
|
413
|
+
|
|
414
|
+
# END status handling
|
|
415
|
+
|
|
416
|
+
if status != 0:
|
|
417
|
+
errstr = read_all_from_possibly_closed_stream(p_stderr)
|
|
418
|
+
_logger.debug("AutoInterrupt wait stderr: %r" % (errstr,))
|
|
419
|
+
raise GitCommandError(remove_password_if_present(self.args), status, errstr)
|
|
420
|
+
return status
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
_AutoInterrupt.__name__ = "AutoInterrupt"
|
|
424
|
+
_AutoInterrupt.__qualname__ = "Git.AutoInterrupt"
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class _CatFileContentStream:
|
|
428
|
+
"""Object representing a sized read-only stream returning the contents of
|
|
429
|
+
an object.
|
|
430
|
+
|
|
431
|
+
This behaves like a stream, but counts the data read and simulates an empty stream
|
|
432
|
+
once our sized content region is empty.
|
|
433
|
+
|
|
434
|
+
If not all data are read to the end of the object's lifetime, we read the rest to
|
|
435
|
+
ensure the underlying stream continues to work.
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
__slots__ = ("_stream", "_nbr", "_size")
|
|
439
|
+
|
|
440
|
+
def __init__(self, size: int, stream: IO[bytes]) -> None:
|
|
441
|
+
self._stream = stream
|
|
442
|
+
self._size = size
|
|
443
|
+
self._nbr = 0 # Number of bytes read.
|
|
444
|
+
|
|
445
|
+
# Special case: If the object is empty, has null bytes, get the final
|
|
446
|
+
# newline right away.
|
|
447
|
+
if size == 0:
|
|
448
|
+
stream.read(1)
|
|
449
|
+
# END handle empty streams
|
|
450
|
+
|
|
451
|
+
def read(self, size: int = -1) -> bytes:
|
|
452
|
+
bytes_left = self._size - self._nbr
|
|
453
|
+
if bytes_left == 0:
|
|
454
|
+
return b""
|
|
455
|
+
if size > -1:
|
|
456
|
+
# Ensure we don't try to read past our limit.
|
|
457
|
+
size = min(bytes_left, size)
|
|
458
|
+
else:
|
|
459
|
+
# They try to read all, make sure it's not more than what remains.
|
|
460
|
+
size = bytes_left
|
|
461
|
+
# END check early depletion
|
|
462
|
+
data = self._stream.read(size)
|
|
463
|
+
self._nbr += len(data)
|
|
464
|
+
|
|
465
|
+
# Check for depletion, read our final byte to make the stream usable by
|
|
466
|
+
# others.
|
|
467
|
+
if self._size - self._nbr == 0:
|
|
468
|
+
self._stream.read(1) # final newline
|
|
469
|
+
# END finish reading
|
|
470
|
+
return data
|
|
471
|
+
|
|
472
|
+
def readline(self, size: int = -1) -> bytes:
|
|
473
|
+
if self._nbr == self._size:
|
|
474
|
+
return b""
|
|
475
|
+
|
|
476
|
+
# Clamp size to lowest allowed value.
|
|
477
|
+
bytes_left = self._size - self._nbr
|
|
478
|
+
if size > -1:
|
|
479
|
+
size = min(bytes_left, size)
|
|
480
|
+
else:
|
|
481
|
+
size = bytes_left
|
|
482
|
+
# END handle size
|
|
483
|
+
|
|
484
|
+
data = self._stream.readline(size)
|
|
485
|
+
self._nbr += len(data)
|
|
486
|
+
|
|
487
|
+
# Handle final byte.
|
|
488
|
+
if self._size - self._nbr == 0:
|
|
489
|
+
self._stream.read(1)
|
|
490
|
+
# END finish reading
|
|
491
|
+
|
|
492
|
+
return data
|
|
493
|
+
|
|
494
|
+
def readlines(self, size: int = -1) -> List[bytes]:
|
|
495
|
+
if self._nbr == self._size:
|
|
496
|
+
return []
|
|
497
|
+
|
|
498
|
+
# Leave all additional logic to our readline method, we just check the size.
|
|
499
|
+
out = []
|
|
500
|
+
nbr = 0
|
|
501
|
+
while True:
|
|
502
|
+
line = self.readline()
|
|
503
|
+
if not line:
|
|
504
|
+
break
|
|
505
|
+
out.append(line)
|
|
506
|
+
if size > -1:
|
|
507
|
+
nbr += len(line)
|
|
508
|
+
if nbr > size:
|
|
509
|
+
break
|
|
510
|
+
# END handle size constraint
|
|
511
|
+
# END readline loop
|
|
512
|
+
return out
|
|
513
|
+
|
|
514
|
+
# skipcq: PYL-E0301
|
|
515
|
+
def __iter__(self) -> "Git.CatFileContentStream":
|
|
516
|
+
return self
|
|
517
|
+
|
|
518
|
+
def __next__(self) -> bytes:
|
|
519
|
+
line = self.readline()
|
|
520
|
+
if not line:
|
|
521
|
+
raise StopIteration
|
|
522
|
+
|
|
523
|
+
return line
|
|
524
|
+
|
|
525
|
+
next = __next__
|
|
526
|
+
|
|
527
|
+
def __del__(self) -> None:
|
|
528
|
+
bytes_left = self._size - self._nbr
|
|
529
|
+
if bytes_left:
|
|
530
|
+
# Read and discard - seeking is impossible within a stream.
|
|
531
|
+
# This includes any terminating newline.
|
|
532
|
+
self._stream.read(bytes_left + 1)
|
|
533
|
+
# END handle incomplete read
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
_CatFileContentStream.__name__ = "CatFileContentStream"
|
|
537
|
+
_CatFileContentStream.__qualname__ = "Git.CatFileContentStream"
|
|
538
|
+
|
|
539
|
+
|
|
311
540
|
_USE_SHELL_DEFAULT_MESSAGE = (
|
|
312
541
|
"Git.USE_SHELL is deprecated, because only its default value of False is safe. "
|
|
313
542
|
"It will be removed in a future release."
|
|
@@ -321,7 +550,7 @@ _USE_SHELL_DANGER_MESSAGE = (
|
|
|
321
550
|
)
|
|
322
551
|
|
|
323
552
|
|
|
324
|
-
def _warn_use_shell(extra_danger: bool) -> None:
|
|
553
|
+
def _warn_use_shell(*, extra_danger: bool) -> None:
|
|
325
554
|
warnings.warn(
|
|
326
555
|
_USE_SHELL_DANGER_MESSAGE if extra_danger else _USE_SHELL_DEFAULT_MESSAGE,
|
|
327
556
|
DeprecationWarning,
|
|
@@ -337,12 +566,12 @@ class _GitMeta(type):
|
|
|
337
566
|
|
|
338
567
|
def __getattribute(cls, name: str) -> Any:
|
|
339
568
|
if name == "USE_SHELL":
|
|
340
|
-
_warn_use_shell(False)
|
|
569
|
+
_warn_use_shell(extra_danger=False)
|
|
341
570
|
return super().__getattribute__(name)
|
|
342
571
|
|
|
343
572
|
def __setattr(cls, name: str, value: Any) -> Any:
|
|
344
573
|
if name == "USE_SHELL":
|
|
345
|
-
_warn_use_shell(value)
|
|
574
|
+
_warn_use_shell(extra_danger=value)
|
|
346
575
|
super().__setattr__(name, value)
|
|
347
576
|
|
|
348
577
|
if not TYPE_CHECKING:
|
|
@@ -728,221 +957,9 @@ class Git(metaclass=_GitMeta):
|
|
|
728
957
|
f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it."
|
|
729
958
|
)
|
|
730
959
|
|
|
731
|
-
|
|
732
|
-
"""Process wrapper that terminates the wrapped process on finalization.
|
|
733
|
-
|
|
734
|
-
This kills/interrupts the stored process instance once this instance goes out of
|
|
735
|
-
scope. It is used to prevent processes piling up in case iterators stop reading.
|
|
736
|
-
|
|
737
|
-
All attributes are wired through to the contained process object.
|
|
738
|
-
|
|
739
|
-
The wait method is overridden to perform automatic status code checking and
|
|
740
|
-
possibly raise.
|
|
741
|
-
"""
|
|
742
|
-
|
|
743
|
-
__slots__ = ("proc", "args", "status")
|
|
744
|
-
|
|
745
|
-
# If this is non-zero it will override any status code during _terminate, used
|
|
746
|
-
# to prevent race conditions in testing.
|
|
747
|
-
_status_code_if_terminate: int = 0
|
|
748
|
-
|
|
749
|
-
def __init__(self, proc: Union[None, subprocess.Popen], args: Any) -> None:
|
|
750
|
-
self.proc = proc
|
|
751
|
-
self.args = args
|
|
752
|
-
self.status: Union[int, None] = None
|
|
753
|
-
|
|
754
|
-
def _terminate(self) -> None:
|
|
755
|
-
"""Terminate the underlying process."""
|
|
756
|
-
if self.proc is None:
|
|
757
|
-
return
|
|
758
|
-
|
|
759
|
-
proc = self.proc
|
|
760
|
-
self.proc = None
|
|
761
|
-
if proc.stdin:
|
|
762
|
-
proc.stdin.close()
|
|
763
|
-
if proc.stdout:
|
|
764
|
-
proc.stdout.close()
|
|
765
|
-
if proc.stderr:
|
|
766
|
-
proc.stderr.close()
|
|
767
|
-
# Did the process finish already so we have a return code?
|
|
768
|
-
try:
|
|
769
|
-
if proc.poll() is not None:
|
|
770
|
-
self.status = self._status_code_if_terminate or proc.poll()
|
|
771
|
-
return
|
|
772
|
-
except OSError as ex:
|
|
773
|
-
_logger.info("Ignored error after process had died: %r", ex)
|
|
774
|
-
|
|
775
|
-
# It can be that nothing really exists anymore...
|
|
776
|
-
if os is None or getattr(os, "kill", None) is None:
|
|
777
|
-
return
|
|
778
|
-
|
|
779
|
-
# Try to kill it.
|
|
780
|
-
try:
|
|
781
|
-
proc.terminate()
|
|
782
|
-
status = proc.wait() # Ensure the process goes away.
|
|
783
|
-
|
|
784
|
-
self.status = self._status_code_if_terminate or status
|
|
785
|
-
except OSError as ex:
|
|
786
|
-
_logger.info("Ignored error after process had died: %r", ex)
|
|
787
|
-
# END exception handling
|
|
788
|
-
|
|
789
|
-
def __del__(self) -> None:
|
|
790
|
-
self._terminate()
|
|
791
|
-
|
|
792
|
-
def __getattr__(self, attr: str) -> Any:
|
|
793
|
-
return getattr(self.proc, attr)
|
|
794
|
-
|
|
795
|
-
# TODO: Bad choice to mimic `proc.wait()` but with different args.
|
|
796
|
-
def wait(self, stderr: Union[None, str, bytes] = b"") -> int:
|
|
797
|
-
"""Wait for the process and return its status code.
|
|
798
|
-
|
|
799
|
-
:param stderr:
|
|
800
|
-
Previously read value of stderr, in case stderr is already closed.
|
|
801
|
-
|
|
802
|
-
:warn:
|
|
803
|
-
May deadlock if output or error pipes are used and not handled
|
|
804
|
-
separately.
|
|
805
|
-
|
|
806
|
-
:raise git.exc.GitCommandError:
|
|
807
|
-
If the return status is not 0.
|
|
808
|
-
"""
|
|
809
|
-
if stderr is None:
|
|
810
|
-
stderr_b = b""
|
|
811
|
-
stderr_b = force_bytes(data=stderr, encoding="utf-8")
|
|
812
|
-
status: Union[int, None]
|
|
813
|
-
if self.proc is not None:
|
|
814
|
-
status = self.proc.wait()
|
|
815
|
-
p_stderr = self.proc.stderr
|
|
816
|
-
else: # Assume the underlying proc was killed earlier or never existed.
|
|
817
|
-
status = self.status
|
|
818
|
-
p_stderr = None
|
|
819
|
-
|
|
820
|
-
def read_all_from_possibly_closed_stream(stream: Union[IO[bytes], None]) -> bytes:
|
|
821
|
-
if stream:
|
|
822
|
-
try:
|
|
823
|
-
return stderr_b + force_bytes(stream.read())
|
|
824
|
-
except (OSError, ValueError):
|
|
825
|
-
return stderr_b or b""
|
|
826
|
-
else:
|
|
827
|
-
return stderr_b or b""
|
|
828
|
-
|
|
829
|
-
# END status handling
|
|
830
|
-
|
|
831
|
-
if status != 0:
|
|
832
|
-
errstr = read_all_from_possibly_closed_stream(p_stderr)
|
|
833
|
-
_logger.debug("AutoInterrupt wait stderr: %r" % (errstr,))
|
|
834
|
-
raise GitCommandError(remove_password_if_present(self.args), status, errstr)
|
|
835
|
-
return status
|
|
836
|
-
|
|
837
|
-
# END auto interrupt
|
|
838
|
-
|
|
839
|
-
class CatFileContentStream:
|
|
840
|
-
"""Object representing a sized read-only stream returning the contents of
|
|
841
|
-
an object.
|
|
842
|
-
|
|
843
|
-
This behaves like a stream, but counts the data read and simulates an empty
|
|
844
|
-
stream once our sized content region is empty.
|
|
845
|
-
|
|
846
|
-
If not all data are read to the end of the object's lifetime, we read the
|
|
847
|
-
rest to ensure the underlying stream continues to work.
|
|
848
|
-
"""
|
|
849
|
-
|
|
850
|
-
__slots__ = ("_stream", "_nbr", "_size")
|
|
851
|
-
|
|
852
|
-
def __init__(self, size: int, stream: IO[bytes]) -> None:
|
|
853
|
-
self._stream = stream
|
|
854
|
-
self._size = size
|
|
855
|
-
self._nbr = 0 # Number of bytes read.
|
|
856
|
-
|
|
857
|
-
# Special case: If the object is empty, has null bytes, get the final
|
|
858
|
-
# newline right away.
|
|
859
|
-
if size == 0:
|
|
860
|
-
stream.read(1)
|
|
861
|
-
# END handle empty streams
|
|
862
|
-
|
|
863
|
-
def read(self, size: int = -1) -> bytes:
|
|
864
|
-
bytes_left = self._size - self._nbr
|
|
865
|
-
if bytes_left == 0:
|
|
866
|
-
return b""
|
|
867
|
-
if size > -1:
|
|
868
|
-
# Ensure we don't try to read past our limit.
|
|
869
|
-
size = min(bytes_left, size)
|
|
870
|
-
else:
|
|
871
|
-
# They try to read all, make sure it's not more than what remains.
|
|
872
|
-
size = bytes_left
|
|
873
|
-
# END check early depletion
|
|
874
|
-
data = self._stream.read(size)
|
|
875
|
-
self._nbr += len(data)
|
|
876
|
-
|
|
877
|
-
# Check for depletion, read our final byte to make the stream usable by
|
|
878
|
-
# others.
|
|
879
|
-
if self._size - self._nbr == 0:
|
|
880
|
-
self._stream.read(1) # final newline
|
|
881
|
-
# END finish reading
|
|
882
|
-
return data
|
|
883
|
-
|
|
884
|
-
def readline(self, size: int = -1) -> bytes:
|
|
885
|
-
if self._nbr == self._size:
|
|
886
|
-
return b""
|
|
887
|
-
|
|
888
|
-
# Clamp size to lowest allowed value.
|
|
889
|
-
bytes_left = self._size - self._nbr
|
|
890
|
-
if size > -1:
|
|
891
|
-
size = min(bytes_left, size)
|
|
892
|
-
else:
|
|
893
|
-
size = bytes_left
|
|
894
|
-
# END handle size
|
|
895
|
-
|
|
896
|
-
data = self._stream.readline(size)
|
|
897
|
-
self._nbr += len(data)
|
|
898
|
-
|
|
899
|
-
# Handle final byte.
|
|
900
|
-
if self._size - self._nbr == 0:
|
|
901
|
-
self._stream.read(1)
|
|
902
|
-
# END finish reading
|
|
903
|
-
|
|
904
|
-
return data
|
|
905
|
-
|
|
906
|
-
def readlines(self, size: int = -1) -> List[bytes]:
|
|
907
|
-
if self._nbr == self._size:
|
|
908
|
-
return []
|
|
909
|
-
|
|
910
|
-
# Leave all additional logic to our readline method, we just check the size.
|
|
911
|
-
out = []
|
|
912
|
-
nbr = 0
|
|
913
|
-
while True:
|
|
914
|
-
line = self.readline()
|
|
915
|
-
if not line:
|
|
916
|
-
break
|
|
917
|
-
out.append(line)
|
|
918
|
-
if size > -1:
|
|
919
|
-
nbr += len(line)
|
|
920
|
-
if nbr > size:
|
|
921
|
-
break
|
|
922
|
-
# END handle size constraint
|
|
923
|
-
# END readline loop
|
|
924
|
-
return out
|
|
925
|
-
|
|
926
|
-
# skipcq: PYL-E0301
|
|
927
|
-
def __iter__(self) -> "Git.CatFileContentStream":
|
|
928
|
-
return self
|
|
929
|
-
|
|
930
|
-
def __next__(self) -> bytes:
|
|
931
|
-
line = self.readline()
|
|
932
|
-
if not line:
|
|
933
|
-
raise StopIteration
|
|
934
|
-
|
|
935
|
-
return line
|
|
936
|
-
|
|
937
|
-
next = __next__
|
|
960
|
+
AutoInterrupt: TypeAlias = _AutoInterrupt
|
|
938
961
|
|
|
939
|
-
|
|
940
|
-
bytes_left = self._size - self._nbr
|
|
941
|
-
if bytes_left:
|
|
942
|
-
# Read and discard - seeking is impossible within a stream.
|
|
943
|
-
# This includes any terminating newline.
|
|
944
|
-
self._stream.read(bytes_left + 1)
|
|
945
|
-
# END handle incomplete read
|
|
962
|
+
CatFileContentStream: TypeAlias = _CatFileContentStream
|
|
946
963
|
|
|
947
964
|
def __init__(self, working_dir: Union[None, PathLike] = None) -> None:
|
|
948
965
|
"""Initialize this instance with:
|
|
@@ -971,7 +988,7 @@ class Git(metaclass=_GitMeta):
|
|
|
971
988
|
|
|
972
989
|
def __getattribute__(self, name: str) -> Any:
|
|
973
990
|
if name == "USE_SHELL":
|
|
974
|
-
_warn_use_shell(False)
|
|
991
|
+
_warn_use_shell(extra_danger=False)
|
|
975
992
|
return super().__getattribute__(name)
|
|
976
993
|
|
|
977
994
|
def __getattr__(self, name: str) -> Any:
|
|
@@ -1319,7 +1336,7 @@ class Git(metaclass=_GitMeta):
|
|
|
1319
1336
|
out, err = proc.communicate()
|
|
1320
1337
|
watchdog.cancel()
|
|
1321
1338
|
if kill_check.is_set():
|
|
1322
|
-
err = 'Timeout: the command "%s" did not complete in %d
|
|
1339
|
+
err = 'Timeout: the command "%s" did not complete in %d secs.' % (
|
|
1323
1340
|
" ".join(redacted_command),
|
|
1324
1341
|
timeout,
|
|
1325
1342
|
)
|
git/config.py
CHANGED
|
@@ -66,7 +66,7 @@ _logger = logging.getLogger(__name__)
|
|
|
66
66
|
CONFIG_LEVELS: ConfigLevels_Tup = ("system", "user", "global", "repository")
|
|
67
67
|
"""The configuration level of a configuration file."""
|
|
68
68
|
|
|
69
|
-
CONDITIONAL_INCLUDE_REGEXP = re.compile(r"(?<=includeIf )\"(gitdir|gitdir/i|onbranch):(.+)\"")
|
|
69
|
+
CONDITIONAL_INCLUDE_REGEXP = re.compile(r"(?<=includeIf )\"(gitdir|gitdir/i|onbranch|hasconfig:remote\.\*\.url):(.+)\"")
|
|
70
70
|
"""Section pattern to detect conditional includes.
|
|
71
71
|
|
|
72
72
|
See: https://git-scm.com/docs/git-config#_conditional_includes
|
|
@@ -87,15 +87,15 @@ class MetaParserBuilder(abc.ABCMeta): # noqa: B024
|
|
|
87
87
|
mutating_methods = clsdict[kmm]
|
|
88
88
|
for base in bases:
|
|
89
89
|
methods = (t for t in inspect.getmembers(base, inspect.isroutine) if not t[0].startswith("_"))
|
|
90
|
-
for
|
|
91
|
-
if
|
|
90
|
+
for method_name, method in methods:
|
|
91
|
+
if method_name in clsdict:
|
|
92
92
|
continue
|
|
93
93
|
method_with_values = needs_values(method)
|
|
94
|
-
if
|
|
94
|
+
if method_name in mutating_methods:
|
|
95
95
|
method_with_values = set_dirty_and_flush_changes(method_with_values)
|
|
96
96
|
# END mutating methods handling
|
|
97
97
|
|
|
98
|
-
clsdict[
|
|
98
|
+
clsdict[method_name] = method_with_values
|
|
99
99
|
# END for each name/method pair
|
|
100
100
|
# END for each base
|
|
101
101
|
# END if mutating methods configuration is set
|
|
@@ -496,19 +496,26 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
|
|
|
496
496
|
if mo:
|
|
497
497
|
# We might just have handled the last line, which could contain a quotation we want to remove.
|
|
498
498
|
optname, vi, optval = mo.group("option", "vi", "value")
|
|
499
|
+
optname = self.optionxform(optname.rstrip())
|
|
500
|
+
|
|
499
501
|
if vi in ("=", ":") and ";" in optval and not optval.strip().startswith('"'):
|
|
500
502
|
pos = optval.find(";")
|
|
501
503
|
if pos != -1 and optval[pos - 1].isspace():
|
|
502
504
|
optval = optval[:pos]
|
|
503
505
|
optval = optval.strip()
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
506
|
+
|
|
507
|
+
if len(optval) < 2 or optval[0] != '"':
|
|
508
|
+
# Does not open quoting.
|
|
509
|
+
pass
|
|
510
|
+
elif optval[-1] != '"':
|
|
511
|
+
# Opens quoting and does not close: appears to start multi-line quoting.
|
|
509
512
|
is_multi_line = True
|
|
510
513
|
optval = string_decode(optval[1:])
|
|
511
|
-
|
|
514
|
+
elif optval.find("\\", 1, -1) == -1 and optval.find('"', 1, -1) == -1:
|
|
515
|
+
# Opens and closes quoting. Single line, and all we need is quote removal.
|
|
516
|
+
optval = optval[1:-1]
|
|
517
|
+
# TODO: Handle other quoted content, especially well-formed backslash escapes.
|
|
518
|
+
|
|
512
519
|
# Preserves multiple values for duplicate optnames.
|
|
513
520
|
cursect.add(optname, optval)
|
|
514
521
|
else:
|
|
@@ -567,11 +574,11 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
|
|
|
567
574
|
if keyword.endswith("/i"):
|
|
568
575
|
value = re.sub(
|
|
569
576
|
r"[a-zA-Z]",
|
|
570
|
-
lambda m: "[{
|
|
577
|
+
lambda m: f"[{m.group().lower()!r}{m.group().upper()!r}]",
|
|
571
578
|
value,
|
|
572
579
|
)
|
|
573
580
|
if self._repo.git_dir:
|
|
574
|
-
if fnmatch.fnmatchcase(
|
|
581
|
+
if fnmatch.fnmatchcase(os.fspath(self._repo.git_dir), value):
|
|
575
582
|
paths += self.items(section)
|
|
576
583
|
|
|
577
584
|
elif keyword == "onbranch":
|
|
@@ -583,7 +590,11 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
|
|
|
583
590
|
|
|
584
591
|
if fnmatch.fnmatchcase(branch_name, value):
|
|
585
592
|
paths += self.items(section)
|
|
586
|
-
|
|
593
|
+
elif keyword == "hasconfig:remote.*.url":
|
|
594
|
+
for remote in self._repo.remotes:
|
|
595
|
+
if fnmatch.fnmatchcase(remote.url, value):
|
|
596
|
+
paths += self.items(section)
|
|
597
|
+
break
|
|
587
598
|
return paths
|
|
588
599
|
|
|
589
600
|
def read(self) -> None: # type: ignore[override]
|
|
@@ -622,8 +633,6 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
|
|
|
622
633
|
file_path = cast(IO[bytes], file_path)
|
|
623
634
|
self._read(file_path, file_path.name)
|
|
624
635
|
else:
|
|
625
|
-
# Assume a path if it is not a file-object.
|
|
626
|
-
file_path = cast(PathLike, file_path)
|
|
627
636
|
try:
|
|
628
637
|
with open(file_path, "rb") as fp:
|
|
629
638
|
file_ok = True
|
|
@@ -757,7 +766,7 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
|
|
|
757
766
|
if self.read_only:
|
|
758
767
|
raise IOError("Cannot execute non-constant method %s.%s" % (self, method_name))
|
|
759
768
|
|
|
760
|
-
def add_section(self, section:
|
|
769
|
+
def add_section(self, section: "cp._SectionName") -> None:
|
|
761
770
|
"""Assures added options will stay in order."""
|
|
762
771
|
return super().add_section(section)
|
|
763
772
|
|
git/diff.py
CHANGED
|
@@ -23,13 +23,14 @@ from typing import (
|
|
|
23
23
|
List,
|
|
24
24
|
Match,
|
|
25
25
|
Optional,
|
|
26
|
+
Sequence,
|
|
26
27
|
Tuple,
|
|
27
28
|
TYPE_CHECKING,
|
|
28
29
|
TypeVar,
|
|
29
30
|
Union,
|
|
30
31
|
cast,
|
|
31
32
|
)
|
|
32
|
-
from git.types import
|
|
33
|
+
from git.types import PathLike, Literal
|
|
33
34
|
|
|
34
35
|
if TYPE_CHECKING:
|
|
35
36
|
from subprocess import Popen
|
|
@@ -289,7 +290,7 @@ class DiffIndex(List[T_Diff]):
|
|
|
289
290
|
The class improves the diff handling convenience.
|
|
290
291
|
"""
|
|
291
292
|
|
|
292
|
-
change_type = ("A", "C", "D", "R", "M", "T")
|
|
293
|
+
change_type: Sequence[Literal["A", "C", "D", "R", "M", "T"]] = ("A", "C", "D", "R", "M", "T") # noqa: F821
|
|
293
294
|
"""Change type invariant identifying possible ways a blob can have changed:
|
|
294
295
|
|
|
295
296
|
* ``A`` = Added
|