GitPython 3.1.44__tar.gz → 3.1.45__tar.gz

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.
Files changed (70) hide show
  1. {gitpython-3.1.44 → gitpython-3.1.45}/AUTHORS +1 -0
  2. {gitpython-3.1.44 → gitpython-3.1.45/GitPython.egg-info}/PKG-INFO +15 -4
  3. {gitpython-3.1.44 → gitpython-3.1.45}/GitPython.egg-info/requires.txt +2 -2
  4. {gitpython-3.1.44/GitPython.egg-info → gitpython-3.1.45}/PKG-INFO +15 -4
  5. gitpython-3.1.45/VERSION +1 -0
  6. {gitpython-3.1.44 → gitpython-3.1.45}/doc/Makefile +1 -1
  7. {gitpython-3.1.44 → gitpython-3.1.45}/doc/source/changes.rst +6 -0
  8. {gitpython-3.1.44 → gitpython-3.1.45}/git/__init__.py +1 -1
  9. {gitpython-3.1.44 → gitpython-3.1.45}/git/cmd.py +239 -222
  10. {gitpython-3.1.44 → gitpython-3.1.45}/git/config.py +17 -10
  11. {gitpython-3.1.44 → gitpython-3.1.45}/git/index/base.py +24 -10
  12. {gitpython-3.1.44 → gitpython-3.1.45}/git/index/fun.py +1 -1
  13. {gitpython-3.1.44 → gitpython-3.1.45}/git/objects/base.py +2 -2
  14. {gitpython-3.1.44 → gitpython-3.1.45}/git/objects/commit.py +2 -2
  15. {gitpython-3.1.44 → gitpython-3.1.45}/git/objects/submodule/base.py +11 -1
  16. {gitpython-3.1.44 → gitpython-3.1.45}/git/objects/tree.py +3 -1
  17. {gitpython-3.1.44 → gitpython-3.1.45}/git/refs/log.py +1 -1
  18. {gitpython-3.1.44 → gitpython-3.1.45}/git/refs/symbolic.py +24 -17
  19. {gitpython-3.1.44 → gitpython-3.1.45}/git/repo/base.py +22 -20
  20. {gitpython-3.1.44 → gitpython-3.1.45}/git/repo/fun.py +8 -2
  21. {gitpython-3.1.44 → gitpython-3.1.45}/git/util.py +9 -5
  22. {gitpython-3.1.44 → gitpython-3.1.45}/pyproject.toml +5 -7
  23. gitpython-3.1.45/requirements.txt +2 -0
  24. {gitpython-3.1.44 → gitpython-3.1.45}/setup.py +0 -1
  25. gitpython-3.1.44/VERSION +0 -1
  26. gitpython-3.1.44/requirements.txt +0 -2
  27. {gitpython-3.1.44 → gitpython-3.1.45}/CHANGES +0 -0
  28. {gitpython-3.1.44 → gitpython-3.1.45}/CONTRIBUTING.md +0 -0
  29. {gitpython-3.1.44 → gitpython-3.1.45}/GitPython.egg-info/SOURCES.txt +0 -0
  30. {gitpython-3.1.44 → gitpython-3.1.45}/GitPython.egg-info/dependency_links.txt +0 -0
  31. {gitpython-3.1.44 → gitpython-3.1.45}/GitPython.egg-info/not-zip-safe +0 -0
  32. {gitpython-3.1.44 → gitpython-3.1.45}/GitPython.egg-info/top_level.txt +0 -0
  33. {gitpython-3.1.44 → gitpython-3.1.45}/LICENSE +0 -0
  34. {gitpython-3.1.44 → gitpython-3.1.45}/MANIFEST.in +0 -0
  35. {gitpython-3.1.44 → gitpython-3.1.45}/README.md +0 -0
  36. {gitpython-3.1.44 → gitpython-3.1.45}/doc/.gitignore +0 -0
  37. {gitpython-3.1.44 → gitpython-3.1.45}/doc/requirements.txt +0 -0
  38. {gitpython-3.1.44 → gitpython-3.1.45}/doc/source/conf.py +0 -0
  39. {gitpython-3.1.44 → gitpython-3.1.45}/doc/source/index.rst +0 -0
  40. {gitpython-3.1.44 → gitpython-3.1.45}/doc/source/intro.rst +0 -0
  41. {gitpython-3.1.44 → gitpython-3.1.45}/doc/source/quickstart.rst +0 -0
  42. {gitpython-3.1.44 → gitpython-3.1.45}/doc/source/reference.rst +0 -0
  43. {gitpython-3.1.44 → gitpython-3.1.45}/doc/source/roadmap.rst +0 -0
  44. {gitpython-3.1.44 → gitpython-3.1.45}/doc/source/tutorial.rst +0 -0
  45. {gitpython-3.1.44 → gitpython-3.1.45}/git/compat.py +0 -0
  46. {gitpython-3.1.44 → gitpython-3.1.45}/git/db.py +0 -0
  47. {gitpython-3.1.44 → gitpython-3.1.45}/git/diff.py +0 -0
  48. {gitpython-3.1.44 → gitpython-3.1.45}/git/exc.py +0 -0
  49. {gitpython-3.1.44 → gitpython-3.1.45}/git/index/__init__.py +0 -0
  50. {gitpython-3.1.44 → gitpython-3.1.45}/git/index/typ.py +0 -0
  51. {gitpython-3.1.44 → gitpython-3.1.45}/git/index/util.py +0 -0
  52. {gitpython-3.1.44 → gitpython-3.1.45}/git/objects/__init__.py +0 -0
  53. {gitpython-3.1.44 → gitpython-3.1.45}/git/objects/blob.py +0 -0
  54. {gitpython-3.1.44 → gitpython-3.1.45}/git/objects/fun.py +0 -0
  55. {gitpython-3.1.44 → gitpython-3.1.45}/git/objects/submodule/__init__.py +0 -0
  56. {gitpython-3.1.44 → gitpython-3.1.45}/git/objects/submodule/root.py +0 -0
  57. {gitpython-3.1.44 → gitpython-3.1.45}/git/objects/submodule/util.py +0 -0
  58. {gitpython-3.1.44 → gitpython-3.1.45}/git/objects/tag.py +0 -0
  59. {gitpython-3.1.44 → gitpython-3.1.45}/git/objects/util.py +0 -0
  60. {gitpython-3.1.44 → gitpython-3.1.45}/git/py.typed +0 -0
  61. {gitpython-3.1.44 → gitpython-3.1.45}/git/refs/__init__.py +0 -0
  62. {gitpython-3.1.44 → gitpython-3.1.45}/git/refs/head.py +0 -0
  63. {gitpython-3.1.44 → gitpython-3.1.45}/git/refs/reference.py +0 -0
  64. {gitpython-3.1.44 → gitpython-3.1.45}/git/refs/remote.py +0 -0
  65. {gitpython-3.1.44 → gitpython-3.1.45}/git/refs/tag.py +0 -0
  66. {gitpython-3.1.44 → gitpython-3.1.45}/git/remote.py +0 -0
  67. {gitpython-3.1.44 → gitpython-3.1.45}/git/repo/__init__.py +0 -0
  68. {gitpython-3.1.44 → gitpython-3.1.45}/git/types.py +0 -0
  69. {gitpython-3.1.44 → gitpython-3.1.45}/setup.cfg +0 -0
  70. {gitpython-3.1.44 → gitpython-3.1.45}/test-requirements.txt +0 -0
@@ -55,5 +55,6 @@ Contributors are:
55
55
  -Eliah Kagan <eliah.kagan _at_ gmail.com>
56
56
  -Ethan Lin <et.repositories _at_ gmail.com>
57
57
  -Jonas Scharpf <jonas.scharpf _at_ checkmk.com>
58
+ -Gordon Marx
58
59
 
59
60
  Portions derived from other open source works and are clearly marked.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: GitPython
3
- Version: 3.1.44
3
+ Version: 3.1.45
4
4
  Summary: GitPython is a Python library used to interact with Git repositories
5
5
  Home-page: https://github.com/gitpython-developers/GitPython
6
6
  Author: Sebastian Thiel, Michael Trier
@@ -9,7 +9,6 @@ License: BSD-3-Clause
9
9
  Classifier: Development Status :: 5 - Production/Stable
10
10
  Classifier: Environment :: Console
11
11
  Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: BSD License
13
12
  Classifier: Operating System :: OS Independent
14
13
  Classifier: Operating System :: POSIX
15
14
  Classifier: Operating System :: Microsoft :: Windows
@@ -28,7 +27,7 @@ Description-Content-Type: text/markdown
28
27
  License-File: LICENSE
29
28
  License-File: AUTHORS
30
29
  Requires-Dist: gitdb<5,>=4.0.1
31
- Requires-Dist: typing-extensions>=3.7.4.3; python_version < "3.8"
30
+ Requires-Dist: typing-extensions>=3.10.0.2; python_version < "3.10"
32
31
  Provides-Extra: test
33
32
  Requires-Dist: coverage[toml]; extra == "test"
34
33
  Requires-Dist: ddt!=1.4.3,>=1.1.1; extra == "test"
@@ -45,6 +44,18 @@ Provides-Extra: doc
45
44
  Requires-Dist: sphinx<7.2,>=7.1.2; extra == "doc"
46
45
  Requires-Dist: sphinx_rtd_theme; extra == "doc"
47
46
  Requires-Dist: sphinx-autodoc-typehints; extra == "doc"
47
+ Dynamic: author
48
+ Dynamic: author-email
49
+ Dynamic: classifier
50
+ Dynamic: description
51
+ Dynamic: description-content-type
52
+ Dynamic: home-page
53
+ Dynamic: license
54
+ Dynamic: license-file
55
+ Dynamic: provides-extra
56
+ Dynamic: requires-dist
57
+ Dynamic: requires-python
58
+ Dynamic: summary
48
59
 
49
60
  ![Python package](https://github.com/gitpython-developers/GitPython/workflows/Python%20package/badge.svg)
50
61
  [![Documentation Status](https://readthedocs.org/projects/gitpython/badge/?version=stable)](https://readthedocs.org/projects/gitpython/?badge=stable)
@@ -1,7 +1,7 @@
1
1
  gitdb<5,>=4.0.1
2
2
 
3
- [:python_version < "3.8"]
4
- typing-extensions>=3.7.4.3
3
+ [:python_version < "3.10"]
4
+ typing-extensions>=3.10.0.2
5
5
 
6
6
  [doc]
7
7
  sphinx<7.2,>=7.1.2
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: GitPython
3
- Version: 3.1.44
3
+ Version: 3.1.45
4
4
  Summary: GitPython is a Python library used to interact with Git repositories
5
5
  Home-page: https://github.com/gitpython-developers/GitPython
6
6
  Author: Sebastian Thiel, Michael Trier
@@ -9,7 +9,6 @@ License: BSD-3-Clause
9
9
  Classifier: Development Status :: 5 - Production/Stable
10
10
  Classifier: Environment :: Console
11
11
  Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: BSD License
13
12
  Classifier: Operating System :: OS Independent
14
13
  Classifier: Operating System :: POSIX
15
14
  Classifier: Operating System :: Microsoft :: Windows
@@ -28,7 +27,7 @@ Description-Content-Type: text/markdown
28
27
  License-File: LICENSE
29
28
  License-File: AUTHORS
30
29
  Requires-Dist: gitdb<5,>=4.0.1
31
- Requires-Dist: typing-extensions>=3.7.4.3; python_version < "3.8"
30
+ Requires-Dist: typing-extensions>=3.10.0.2; python_version < "3.10"
32
31
  Provides-Extra: test
33
32
  Requires-Dist: coverage[toml]; extra == "test"
34
33
  Requires-Dist: ddt!=1.4.3,>=1.1.1; extra == "test"
@@ -45,6 +44,18 @@ Provides-Extra: doc
45
44
  Requires-Dist: sphinx<7.2,>=7.1.2; extra == "doc"
46
45
  Requires-Dist: sphinx_rtd_theme; extra == "doc"
47
46
  Requires-Dist: sphinx-autodoc-typehints; extra == "doc"
47
+ Dynamic: author
48
+ Dynamic: author-email
49
+ Dynamic: classifier
50
+ Dynamic: description
51
+ Dynamic: description-content-type
52
+ Dynamic: home-page
53
+ Dynamic: license
54
+ Dynamic: license-file
55
+ Dynamic: provides-extra
56
+ Dynamic: requires-dist
57
+ Dynamic: requires-python
58
+ Dynamic: summary
48
59
 
49
60
  ![Python package](https://github.com/gitpython-developers/GitPython/workflows/Python%20package/badge.svg)
50
61
  [![Documentation Status](https://readthedocs.org/projects/gitpython/badge/?version=stable)](https://readthedocs.org/projects/gitpython/?badge=stable)
@@ -0,0 +1 @@
1
+ 3.1.45
@@ -3,7 +3,7 @@
3
3
 
4
4
  # You can set these variables from the command line.
5
5
  BUILDDIR = build
6
- SPHINXOPTS = -W
6
+ SPHINXOPTS =
7
7
  SPHINXBUILD = sphinx-build
8
8
  PAPER =
9
9
 
@@ -2,6 +2,12 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ 3.1.45
6
+ ======
7
+
8
+ See the following for all changes.
9
+ https://github.com/gitpython-developers/GitPython/releases/tag/3.1.45
10
+
5
11
  3.1.44
6
12
  ======
7
13
 
@@ -86,7 +86,7 @@ __all__ = [
86
86
  "to_hex_sha",
87
87
  ]
88
88
 
89
- __version__ = '3.1.44'
89
+ __version__ = '3.1.45'
90
90
 
91
91
  from typing import Any, List, Optional, Sequence, TYPE_CHECKING, Tuple, Union
92
92
 
@@ -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." f" kill_after_timeout={kill_after_timeout} seconds"
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 an value.
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, and in the rare case the
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
- class AutoInterrupt:
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
- def __del__(self) -> None:
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 ' "secs." % (
1339
+ err = 'Timeout: the command "%s" did not complete in %d secs.' % (
1323
1340
  " ".join(redacted_command),
1324
1341
  timeout,
1325
1342
  )