2323import os
2424import re
2525import subprocess
26+ import weakref
2627from collections import defaultdict
2728from contextlib import contextmanager
2829from enum import Enum
@@ -506,8 +507,39 @@ def decode_c_quoted_str(text: str) -> str:
506507 return text
507508
508509
510+ def maybe_close_subprocess (process : Optional [subprocess .Popen ]) -> None :
511+ """Closes a subprocess safely to avoid resource warnings and resource starvation
512+
513+ This function ensures the safe termination of a subprocess by properly closing its
514+ standard input and output streams, waiting for it to exit, and forcefully
515+ killing it if necessary. It is designed to handle `git cat-file --batch-command`
516+ and similar persistent subprocesses.
517+
518+ It is designed to be used as a ` weakref.finalize ` callback.
519+
520+ Parameters
521+ ----------
522+ process
523+ The subprocess instance to close. If None, the function does nothing.
524+ """
525+ if process is None :
526+ return
527+
528+ # closing stdin and stdout should end the persistent `git cat-file` process
529+ process .stdout .close () # to avoid ResourceWarning: unclosed file <_io.BufferedReader name=3>
530+ process .stdin .close () # just in case, see above
531+ process .wait () # to avoid ResourceWarning: subprocess NNN is still running
532+ process .kill () # just in case
533+
534+
509535class GitRepo :
510- """Class representing Git repository, for performing operations on"""
536+ """Class representing Git repository, for performing operations on
537+
538+ Attributes
539+ ----------
540+ repo : Path
541+ stores Path to the Git repository
542+ """
511543 path_encoding = 'utf8'
512544 default_file_encoding = 'utf8'
513545 log_encoding = 'utf8'
@@ -529,6 +561,8 @@ def __init__(self, repo_dir: PathLike):
529561 # TODO: check that `git_directory` is a path to git repository
530562 # TODO: remember absolute path (it is safer)
531563 self .repo = Path (repo_dir )
564+ self ._cat_file : Optional [subprocess .Popen ] = None
565+ self ._finalizer = weakref .finalize (self , maybe_close_subprocess , self ._cat_file )
532566
533567 def __repr__ (self ):
534568 class_name = type (self ).__name__
@@ -673,7 +707,7 @@ def batch_command(self) -> subprocess.Popen:
673707 in the `--batch-command` mode, buffered (because of `--buffer`),
674708 see https://git-scm.com/docs/git-cat-file
675709 """
676- return subprocess .Popen (
710+ self . _cat_file = subprocess .Popen (
677711 [
678712 'git' , '-C' , str (self .repo ),
679713 'cat-file' , '--batch-command' , '--buffer' ,
@@ -684,6 +718,18 @@ def batch_command(self) -> subprocess.Popen:
684718 text = True ,
685719 bufsize = 1 , # line buffered
686720 )
721+ return self ._cat_file
722+
723+ def close_batch_command (self ) -> None :
724+ """Close persistent connection to `git cat-file --batch-command --buffer`
725+
726+ This should be done only when you are finished with using it, because
727+ at least with the current implementation calling this method would make
728+ `.are_valid_objects()` and `.filter_valid_commits()` methods fail;
729+ they rely on persistence of the cached `.batch_command` property.
730+ """
731+ self ._finalizer ()
732+ self ._cat_file = None
687733
688734 def format_patch (self ,
689735 output_dir : Optional [PathLike ] = None ,
@@ -1476,6 +1522,9 @@ def filter_valid_commits(self, commits: Iterable[str], to_oid: bool = False) ->
14761522 ----------
14771523 commits
14781524 A list of commit identifiers to check
1525+ to_oid
1526+ Whether to convert elements in `commits` to SHA-1 object identifiers,
1527+ for example, "HEAD" to "3a27ee24b37a3e9572a0acc0aaecd22cc9c10bc7"
14791528
14801529 Yields
14811530 ------
0 commit comments