gitbolt 0.0.0.dev16__tar.gz → 0.0.0.dev18__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 (37) hide show
  1. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/PKG-INFO +34 -2
  2. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/README.md +32 -0
  3. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/pyproject.toml +2 -2
  4. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/__init__.py +1 -0
  5. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/base.py +165 -8
  6. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/impl/simple.py +18 -1
  7. gitbolt-0.0.0.dev18/src/gitbolt/subprocess/utils/__init__.py +6 -0
  8. gitbolt-0.0.0.dev18/src/gitbolt/subprocess/utils/session.py +134 -0
  9. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt.egg-info/PKG-INFO +34 -2
  10. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt.egg-info/SOURCES.txt +4 -2
  11. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt.egg-info/requires.txt +1 -1
  12. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/LICENSE +0 -0
  13. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/setup.cfg +0 -0
  14. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/_internal_init.py +0 -0
  15. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/add.py +0 -0
  16. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/base.py +0 -0
  17. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/constants.py +0 -0
  18. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/exceptions.py +0 -0
  19. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/ls_tree.py +0 -0
  20. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/models.py +0 -0
  21. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/py.typed +0 -0
  22. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/pytest_plugin.py +0 -0
  23. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/__init__.py +0 -0
  24. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/_internal_init.py +0 -0
  25. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/add.py +0 -0
  26. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/constants.py +0 -0
  27. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/exceptions.py +0 -0
  28. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/impl/__init__.py +0 -0
  29. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/ls_tree.py +0 -0
  30. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/runner/__init__.py +0 -0
  31. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/runner/base.py +0 -0
  32. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/runner/simple.py +0 -0
  33. /gitbolt-0.0.0.dev16/src/gitbolt/subprocess/utils.py → /gitbolt-0.0.0.dev18/src/gitbolt/subprocess/utils/cmd.py +0 -0
  34. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/utils.py +0 -0
  35. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt.egg-info/dependency_links.txt +0 -0
  36. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt.egg-info/entry_points.txt +0 -0
  37. {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitbolt
3
- Version: 0.0.0.dev16
3
+ Version: 0.0.0.dev18
4
4
  Summary: Fast, flexible and type-safe Git commands in Python.
5
5
  Author-email: Suhas Krishna Srivastava <suhas.srivastava@vaastav.tech>
6
6
  Maintainer-email: Suhas Krishna Srivastava <suhas.srivastava@vaastav.tech>
@@ -27,7 +27,7 @@ Requires-Python: >=3.12
27
27
  Description-Content-Type: text/markdown
28
28
  License-File: LICENSE
29
29
  Requires-Dist: vt-err-hndlr>=0.0.0dev6
30
- Requires-Dist: vt-commons>=0.0.1.dev9
30
+ Requires-Dist: vt-commons>=0.0.1.dev11
31
31
  Provides-Extra: pygit2
32
32
  Requires-Dist: pygit2; extra == "pygit2"
33
33
  Provides-Extra: test
@@ -366,6 +366,38 @@ with git.subcmd_unchecked.popen(["cat-file", "--batch-command"]) as cf:
366
366
 
367
367
  Error handling and I/O management is left to the client/caller.
368
368
 
369
+ #### 🏃 Communicate with long-running git sessions
370
+
371
+ Introduced in `0.0.0.dev17` to:
372
+ - Make long-running git command sessions.
373
+ - Have reentrant sessions.
374
+ - Communicate with long-running git command session using their `stdin` and `stdout`.
375
+
376
+ Get long-running process sessions and communicate with them for batching and faster operations.
377
+
378
+ ```python
379
+ import gitbolt
380
+ from gitbolt.subprocess.utils.session import cat_file_blob_content, cat_file_tree_content
381
+
382
+ git = gitbolt.get_git_command()
383
+ with git.session(
384
+ cat_file1=["cat-file", "--batch"],
385
+ cat_file2=["cat-file", "--batch"],
386
+ cat_file3=lambda: git.subcmd_unchecked.popen(["cat-file", "--batch"]), # pass your own git subcmd Popen(s)
387
+ mktree=["mktree"]
388
+ ) as ses:
389
+ for mode_, sha_, name_ in cat_file_tree_content(ses.commands.cat_file2, b"HEAD^{tree}"):
390
+ print(mode_, sha_, name_)
391
+ print("+" * 40)
392
+ print(cat_file_blob_content(ses.commands.cat_file3, b"9854cb4d432a881f59d38582791cf2636e7819d9"))
393
+ with ses: # is reentrant, if you want
394
+ ses.commands.mktree.stdin.write(b"100644 blob <some-blob-hash> <some-filename>\n")
395
+ ses.commands.mktree.stdin.flush()
396
+ tree_hash = ses.commands.mktree.stdout.read()
397
+
398
+ assert not ses.active # session no long active
399
+ ```
400
+
369
401
  #### 💻 Run commands received from CLI
370
402
 
371
403
  Introduced in `0.0.0.dev11` is the ability to take commands from CLI and run it inside `gitbolt`.
@@ -330,6 +330,38 @@ with git.subcmd_unchecked.popen(["cat-file", "--batch-command"]) as cf:
330
330
 
331
331
  Error handling and I/O management is left to the client/caller.
332
332
 
333
+ #### 🏃 Communicate with long-running git sessions
334
+
335
+ Introduced in `0.0.0.dev17` to:
336
+ - Make long-running git command sessions.
337
+ - Have reentrant sessions.
338
+ - Communicate with long-running git command session using their `stdin` and `stdout`.
339
+
340
+ Get long-running process sessions and communicate with them for batching and faster operations.
341
+
342
+ ```python
343
+ import gitbolt
344
+ from gitbolt.subprocess.utils.session import cat_file_blob_content, cat_file_tree_content
345
+
346
+ git = gitbolt.get_git_command()
347
+ with git.session(
348
+ cat_file1=["cat-file", "--batch"],
349
+ cat_file2=["cat-file", "--batch"],
350
+ cat_file3=lambda: git.subcmd_unchecked.popen(["cat-file", "--batch"]), # pass your own git subcmd Popen(s)
351
+ mktree=["mktree"]
352
+ ) as ses:
353
+ for mode_, sha_, name_ in cat_file_tree_content(ses.commands.cat_file2, b"HEAD^{tree}"):
354
+ print(mode_, sha_, name_)
355
+ print("+" * 40)
356
+ print(cat_file_blob_content(ses.commands.cat_file3, b"9854cb4d432a881f59d38582791cf2636e7819d9"))
357
+ with ses: # is reentrant, if you want
358
+ ses.commands.mktree.stdin.write(b"100644 blob <some-blob-hash> <some-filename>\n")
359
+ ses.commands.mktree.stdin.flush()
360
+ tree_hash = ses.commands.mktree.stdout.read()
361
+
362
+ assert not ses.active # session no long active
363
+ ```
364
+
333
365
  #### 💻 Run commands received from CLI
334
366
 
335
367
  Introduced in `0.0.0.dev11` is the ability to take commands from CLI and run it inside `gitbolt`.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gitbolt"
3
- version = "0.0.0.dev16"
3
+ version = "0.0.0.dev18"
4
4
  description = "Fast, flexible and type-safe Git commands in Python."
5
5
  requires-python = ">=3.12"
6
6
  readme = "README.md"
@@ -37,7 +37,7 @@ classifiers = [
37
37
  ]
38
38
  dependencies = [
39
39
  "vt-err-hndlr >= 0.0.0dev6",
40
- "vt-commons >= 0.0.1.dev9",
40
+ "vt-commons >= 0.0.1.dev11",
41
41
  ]
42
42
 
43
43
  [project.optional-dependencies]
@@ -17,6 +17,7 @@ from gitbolt.base import LsTree as LsTree
17
17
  from gitbolt.base import Version as Version
18
18
  from gitbolt.base import Add as Add
19
19
  from gitbolt.subprocess.base import GitCommand
20
+ from gitbolt.subprocess.base import GitSession as GitSession
20
21
  # endregion
21
22
 
22
23
 
@@ -7,10 +7,13 @@ Git command interfaces with default implementation using subprocess calls.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import sys
10
11
  from abc import abstractmethod, ABC
11
12
  from collections.abc import Callable
13
+ from contextlib import AbstractContextManager
12
14
  from pathlib import Path
13
15
  from subprocess import CompletedProcess, Popen, PIPE
16
+ from types import SimpleNamespace
14
17
  from typing import override, Protocol, Unpack, Self, overload, Literal, Any
15
18
 
16
19
  from vt.utils.commons.commons.core_py import is_unset, not_none_not_unset
@@ -295,9 +298,165 @@ class GitCommand(Git, ABC):
295
298
  def subcmd_unchecked(self) -> UncheckedSubcmd:
296
299
  """
297
300
  Run an unchecked git subcommand using subprocess.
301
+
302
+ :returns: An unchecked subcommand instance that simply runs the asked command in a ``subprocess.run()`` with
303
+ optional ``subprocess.Popen()``.
298
304
  """
299
305
  ...
300
306
 
307
+ @abstractmethod
308
+ def session(self, **commands: list[str] | Callable[[], Popen[bytes]]) -> GitSession:
309
+ """
310
+ Run long-running git session with multiple unchecked subcommands using ``subprocess.Popen`` and
311
+ communicate with them.
312
+
313
+ Client/Caller can communicate with ``GitSession``'s processes with their stdin and stdout.
314
+
315
+ Examples:
316
+
317
+ Obtain a session:
318
+
319
+ >>> import gitbolt
320
+ >>> _git = gitbolt.get_git_command()
321
+ >>> ses = _git.session(ls_tree=["ls-tree", "HEAD"], cat_file=["cat-file", "--batch"])
322
+ >>> with ses: # start the session by ctx mgr
323
+ ... pass # any communication can be done by Popen semantics.
324
+
325
+ Start the session in one go:
326
+
327
+ >>> with _git.session(cat_file=["cat-file", "--batch"]) as ses: # obtain, start and ctx manage the session.
328
+ ... pass # any communication can be done by Popen semantics.
329
+
330
+
331
+ Note: All commands/processes are assumed:
332
+
333
+ - in bytes mode.
334
+ - pipe their stdin and stdout.
335
+
336
+ :param commands: list of string git commands suppliable to ``subprocess.Popen`` or ``subprocess.Popen`` lambdas.
337
+ :returns: A (not yet started) long-running ``GitSession`` context manager.
338
+ """
339
+ ...
340
+
341
+
342
+ # TODO: extract a base session class from this
343
+ class GitSession(HasGitUnderneath[GitCommand], AbstractContextManager):
344
+ def __init__(self, git: GitCommand, **commands: Callable[[], Popen[bytes]]):
345
+ """
346
+ Reusable and reentrant context Manager to start a git long-running session. Useful when a command is to be
347
+ held in open state and be communicated with its stdin and stdout. This is way faster that spawning multiple
348
+ processes each time.
349
+
350
+ Examples:
351
+
352
+ Obtain a session:
353
+
354
+ >>> import gitbolt
355
+ >>> from gitbolt.subprocess.base import GitSession
356
+ >>> _git = gitbolt.get_git_command()
357
+ >>> ses = GitSession(_git, ls_tree= lambda : _git.subcmd_unchecked.popen(["ls-tree", "HEAD"], text=False),
358
+ ... cat_file= lambda : _git.subcmd_unchecked.popen(["cat-file", "--batch"], text=False))
359
+ >>> with ses: # start the session by ctx mgr
360
+ ... pass # any communication can be done by Popen semantics.
361
+
362
+ Start the session in one go:
363
+
364
+ >>> with GitSession(_git, cat_file= lambda : _git.subcmd_unchecked.popen(["cat-file", "--batch"], text=False)) as ses: # obtain, start and ctx manage the session.
365
+ ... pass # any communication can be done by Popen semantics.
366
+
367
+
368
+ Note: All processes are assumed:
369
+
370
+ - in bytes mode.
371
+ - pipe their stdin and stdout.
372
+
373
+ :param git: ``gitbolt.subprocess.GitCommand`` instance.
374
+ :param commands: commands in kwargs fashion.
375
+ """
376
+ self._git = git
377
+ self.unstarted_commands: dict[str, Callable[[], Popen[bytes]]] = commands
378
+ self.started_commands: dict[str, Popen[bytes]] = dict()
379
+ self._commands: SimpleNamespace | None = None
380
+ self.__started = False
381
+ self.__done = False
382
+ self.__depth = 0
383
+
384
+ @override
385
+ def __enter__(self) -> Self:
386
+ if self.depth == 0:
387
+ processes_started_keys: list[str] = []
388
+ for unstarted_command_key, unstarted_command in self.unstarted_commands.items():
389
+ try:
390
+ self.started_commands[unstarted_command_key] = unstarted_command().__enter__()
391
+ except Exception:
392
+ for k in reversed(processes_started_keys):
393
+ self.started_commands[k].__exit__(*sys.exc_info())
394
+ raise
395
+ processes_started_keys.append(unstarted_command_key)
396
+ self._commands = SimpleNamespace(**self.started_commands)
397
+ for pk in processes_started_keys:
398
+ del self.unstarted_commands[pk]
399
+ self.__started = True
400
+ self.__depth += 1
401
+ return self
402
+
403
+ @override
404
+ @property
405
+ def git(self) -> GitCommand:
406
+ return self._git
407
+
408
+ @override
409
+ def __exit__(self, exc_type, exc_value, traceback, /) -> Literal[False]:
410
+ self.__depth -= 1
411
+ if self.__depth == 0:
412
+ process_done_keys: list[str] = []
413
+ for started_popen_key, started_popen in self.started_commands.items():
414
+ started_popen.__exit__(exc_type, exc_value, traceback)
415
+ process_done_keys.append(started_popen_key)
416
+ self._commands = None
417
+ for pk in process_done_keys:
418
+ del self.started_commands[pk]
419
+ self.__done = True
420
+ return False
421
+
422
+ @property
423
+ def started(self) -> bool:
424
+ """
425
+ :returns: Whether the session has started.
426
+ """
427
+ return self.__started
428
+
429
+ @property
430
+ def done(self) -> bool:
431
+ """
432
+ :returns: Whether the session has completed and thus exited/closed.
433
+ """
434
+ return self.__done
435
+
436
+ @property
437
+ def active(self) -> bool:
438
+ """
439
+ :returns: Whether the session is currently actively running.
440
+ """
441
+ return self.started and not self.done
442
+
443
+ @property
444
+ def commands(self) -> SimpleNamespace:
445
+ """
446
+ :returns: All the registered commands and these can be accessed by member notations.
447
+ :raises RuntimeError: if commands are queried when the session is inactive.
448
+ """
449
+ if self._commands is None or not self.active:
450
+ raise RuntimeError("GitSession not active")
451
+ return self._commands
452
+
453
+ @property
454
+ def depth(self) -> int:
455
+ """
456
+ :returns: The current session depth. Multilevel context managers increment the depth each y one.
457
+ """
458
+ return self.__depth
459
+
301
460
 
302
461
  class GitSubcmdCommand(GitSubCommand, HasGitUnderneath["GitCommand"], Protocol):
303
462
  """
@@ -609,30 +768,30 @@ class UncheckedSubcmd(GitSubcmdCommand, RootDirOp, Protocol):
609
768
  self,
610
769
  subcommand_args: list[str],
611
770
  *popen_args: Any,
612
- text: Literal[True] = True,
771
+ text: Literal[False] = False,
613
772
  **popen_kwargs: Any,
614
- ) -> Popen[str]: ...
773
+ ) -> Popen[bytes]: ...
615
774
 
616
775
  @overload
617
776
  def popen(
618
777
  self,
619
778
  subcommand_args: list[str],
620
779
  *popen_args: Any,
621
- text: Literal[False] = False,
780
+ text: Literal[True] = True,
622
781
  **popen_kwargs: Any,
623
- ) -> Popen[bytes]: ...
782
+ ) -> Popen[str]: ...
624
783
 
625
784
  def popen(
626
785
  self,
627
786
  subcommand_args: list[str],
628
787
  *popen_args: Any,
629
- text: Literal[True, False] = False,
788
+ text: Literal[False, True] = False,
630
789
  **popen_kwargs: Any,
631
790
  ) -> Popen[str] | Popen[bytes]:
632
791
  """
633
792
  Open unchecked git subcommand communicable process, using ``subprocess.Popen``.
634
793
 
635
- All the arguments are congruent to ``subprocess.Popen`` and mostly passes as-is.
794
+ All the arguments are congruent to ``subprocess.Popen`` and mostly pass as-is.
636
795
 
637
796
  :param subcommand_args: the full subcommand argument list.
638
797
  :param popen_args: additional subprocess positionals.
@@ -647,7 +806,6 @@ class UncheckedSubcmd(GitSubcmdCommand, RootDirOp, Protocol):
647
806
  stdin = popen_kwargs.pop("stdin", PIPE)
648
807
  stdout = popen_kwargs.pop("stdout", PIPE)
649
808
  stderr = popen_kwargs.pop("stderr", PIPE)
650
- bufsize = popen_kwargs.pop("bufsize", 0)
651
809
  # Popen the git command
652
810
  result = self.git.runner.popen_git_command(
653
811
  main_cmd_args,
@@ -659,7 +817,6 @@ class UncheckedSubcmd(GitSubcmdCommand, RootDirOp, Protocol):
659
817
  stdin=stdin,
660
818
  stdout=stdout,
661
819
  stderr=stderr,
662
- bufsize=bufsize,
663
820
  **popen_kwargs,
664
821
  )
665
822
  return result
@@ -9,7 +9,8 @@ from __future__ import annotations
9
9
 
10
10
  from abc import ABC
11
11
  from pathlib import Path
12
- from typing import override, Literal, overload
12
+ from subprocess import Popen
13
+ from typing import override, Literal, overload, Callable
13
14
 
14
15
  from vt.utils.commons.commons.op import RootDirOp
15
16
 
@@ -24,6 +25,7 @@ from gitbolt.subprocess import (
24
25
  UncheckedSubcmd,
25
26
  )
26
27
  from gitbolt.subprocess.add import AddCLIArgsBuilder
28
+ from gitbolt.subprocess.base import GitSession
27
29
  from gitbolt.subprocess.constants import VERSION_CMD
28
30
  from gitbolt.subprocess.ls_tree import LsTreeCLIArgsBuilder
29
31
  from gitbolt.subprocess.runner import GitCommandRunner
@@ -245,6 +247,21 @@ class SimpleGitCommand(GitCommand, RootDirOp):
245
247
  subcmd_unchecked._set_underlying_git(self)
246
248
  return subcmd_unchecked
247
249
 
250
+ def session(self, **commands: list[str] | Callable[[], Popen[bytes]]) -> GitSession:
251
+ cmds: dict[str, Callable[[], Popen[bytes]]] = {}
252
+
253
+ # curb the lambda-late-binding-trap
254
+ # resource: https://medium.com/skiller-whale/late-binding-variables-its-a-trap-c17af980164f
255
+ def lambda_for_subcmd_popen(_runnable_cmd: list[str]):
256
+ return lambda: self.subcmd_unchecked.popen(_runnable_cmd)
257
+
258
+ for cmd_name, runnable_cmd in commands.items():
259
+ if callable(runnable_cmd):
260
+ cmds[cmd_name] = runnable_cmd
261
+ else:
262
+ cmds[cmd_name] = lambda_for_subcmd_popen(runnable_cmd)
263
+ return GitSession(self, **cmds)
264
+
248
265
 
249
266
  class CLISimpleGitCommand(SimpleGitCommand):
250
267
  """
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env python3
2
+ # coding=utf-8
3
+
4
+ """
5
+ Utilities related to processors specific to git commands using subprocess.
6
+ """
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env python3
2
+ # coding=utf-8
3
+
4
+ """
5
+ Git command sessions utilities for long-running commands.
6
+
7
+ Create sessions for long-running commands and communicate with them using their stdin/stdout.
8
+
9
+ Much faster that subprocess creation for each input/output pair.
10
+ """
11
+
12
+ import subprocess
13
+ from typing import Iterable, IO, cast
14
+
15
+
16
+ def cat_file_blob_content(
17
+ cat_file_popen: subprocess.Popen[bytes], blob_hash: bytes
18
+ ) -> bytes:
19
+ """
20
+ Read git blob contents using a long-running batched cat-file process.
21
+
22
+ Spawning new ``git show`` processes can be slower and resource consuming.
23
+
24
+ Note: ``cat_file_popen`` must pipe its ``stdin`` and ``stdout`` and run in ``bytes`` mode.
25
+
26
+ :param cat_file_popen: long-running ``git cat-file --batch`` process in bytes mode and pipes its stdin and stdout.
27
+ :param blob_hash: hash to be read from cat-file.
28
+ :return: contents of blob hash in bytes.
29
+ """
30
+ write_obj = blob_hash + b"\n"
31
+ cat_file_popen_stdin: IO[bytes] = cast(IO[bytes], cat_file_popen.stdin) # stdin is assumed to be piped
32
+ cat_file_popen_stdout: IO[bytes] = cast(IO[bytes], cat_file_popen.stdout) # stdout is assumed to be piped
33
+ cat_file_popen_stdin.write(write_obj)
34
+ cat_file_popen_stdin.flush()
35
+ header = cat_file_popen_stdout.readline()
36
+ obj, typ, size = header.split()
37
+ blob_content: bytes = cat_file_popen_stdout.read(int(size))
38
+ cat_file_popen_stdout.read(1)
39
+ return blob_content
40
+
41
+
42
+ def parse_tree(data: bytes) -> Iterable[tuple[bytes, bytes, bytes]]:
43
+ """
44
+ Parse bytes tree data.
45
+
46
+ Typically best for parsing tree data from the stdout of a long-running cat-file query for tree.
47
+
48
+ :param data: tree data in bytes format.
49
+ :returns: iterable of (mode, sha, filename/filepath).
50
+ """
51
+ i = 0
52
+ n = len(data)
53
+ while i < n:
54
+ # mode
55
+ j = data.find(b" ", i)
56
+ mode = data[i:j]
57
+ # filename
58
+ k = data.find(b"\x00", j)
59
+ name = data[j + 1 : k]
60
+ # sha (20 bytes binary)
61
+ sha = data[k + 1 : k + 21]
62
+
63
+ yield mode, sha.hex().encode(), name
64
+ i = k + 21
65
+
66
+
67
+ def cat_file_tree_content(
68
+ cat_file_popen: subprocess.Popen[bytes], tree_hash: bytes, recursive: bool = False,
69
+ prefix: bytes = b"",
70
+ ) -> list[tuple[bytes, bytes, bytes]]:
71
+ """
72
+ Get tree data from the stdout of a long-running cat-file query for tree hash.
73
+
74
+ Note: ``cat_file_popen`` must pipe its ``stdin`` and ``stdout`` and run in ``bytes`` mode.
75
+
76
+ :param cat_file_popen: long-running ``git cat-file --batch`` process in bytes mode and pipes its stdin and stdout.
77
+ :param tree_hash: tree hash to be read from cat-file.
78
+ :param recursive: recursively query the full tree.
79
+ :param prefix: prefix, useful when doing recursive tree query.
80
+ :return: list of (mode, sha, filename).
81
+ """
82
+ tree_content = cat_file_blob_content(cat_file_popen, tree_hash)
83
+ output: list[tuple[bytes, bytes, bytes]] = []
84
+ if not recursive:
85
+ for mode, sha, name in parse_tree(tree_content):
86
+ # leaf node (blob, symlink, submodule)
87
+ output.append((mode, sha, name,))
88
+ else:
89
+ for mode, sha, name in parse_tree(tree_content):
90
+ full_name = prefix + name
91
+
92
+ if mode == b"40000": # directory (tree)
93
+ # recurse into subtree
94
+ sub_tree = cat_file_tree_content(
95
+ cat_file_popen,
96
+ sha,
97
+ recursive=True,
98
+ prefix=full_name + b"/",
99
+ )
100
+ output.extend(sub_tree)
101
+ else:
102
+ # leaf node (blob, symlink, submodule)
103
+ output.append((mode, sha, full_name,))
104
+
105
+ return output
106
+
107
+
108
+ if __name__ == "__main__":
109
+ import gitbolt
110
+
111
+ git = gitbolt.get_git_command()
112
+ # with git.session(cat_file=["cat-file", "--batch"]) as ses1, git.session(cat_file=["catfile", "--batch"]) as ses2:
113
+ # for mode_, sha_, name_ in cat_file_tree_content(ses1.commands.cat_file, b"HEAD^{tree}"):
114
+ # print(mode_, sha_, name_)
115
+ with git.session(
116
+ cat_file1=["catfile", "--batch"],
117
+ cat_file2=["cat-file", "--batch"],
118
+ cat_file3=["cat-file", "--batch"],
119
+ mktree=lambda: git.subcmd_unchecked.popen(["mktree"]),
120
+ ) as ses:
121
+ for mode_, sha_, name_ in cat_file_tree_content(ses.commands.cat_file2, b"HEAD^{tree}"):
122
+ print(mode_, sha_, name_)
123
+ print("+" * 40)
124
+ print(cat_file_blob_content(ses.commands.cat_file3, b"9854cb4d432a881f59d38582791cf2636e7819d9"))
125
+ # cf_p_2 = git.subcmd_unchecked.popen(["catfile", "--batch"])
126
+ # cf_p_1 = git.subcmd_unchecked.popen(["cat-file", "--batch"])
127
+ # for mode_, sha_, name_ in cat_file_tree_content(cf_p_1, b"HEAD^{tree}"):
128
+ # print(mode_, sha_, name_)
129
+ # sess = gitbolt.GitSession(git, cf_p_2=lambda: git.subcmd_unchecked.popen(["catfile", "--batch"]),
130
+ # cf_p_1=lambda: git.subcmd_unchecked.popen(["cat-file", "--batch"]),
131
+ # mktree=lambda: git.subcmd_unchecked.popen(["mktree", "--batch"]))
132
+ # with sess:
133
+ # for mode_, sha_, name_ in cat_file_tree_content(sess.commands.cf_p_1, b"HEAD^{tree}"):
134
+ # print(mode_, sha_, name_)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitbolt
3
- Version: 0.0.0.dev16
3
+ Version: 0.0.0.dev18
4
4
  Summary: Fast, flexible and type-safe Git commands in Python.
5
5
  Author-email: Suhas Krishna Srivastava <suhas.srivastava@vaastav.tech>
6
6
  Maintainer-email: Suhas Krishna Srivastava <suhas.srivastava@vaastav.tech>
@@ -27,7 +27,7 @@ Requires-Python: >=3.12
27
27
  Description-Content-Type: text/markdown
28
28
  License-File: LICENSE
29
29
  Requires-Dist: vt-err-hndlr>=0.0.0dev6
30
- Requires-Dist: vt-commons>=0.0.1.dev9
30
+ Requires-Dist: vt-commons>=0.0.1.dev11
31
31
  Provides-Extra: pygit2
32
32
  Requires-Dist: pygit2; extra == "pygit2"
33
33
  Provides-Extra: test
@@ -366,6 +366,38 @@ with git.subcmd_unchecked.popen(["cat-file", "--batch-command"]) as cf:
366
366
 
367
367
  Error handling and I/O management is left to the client/caller.
368
368
 
369
+ #### 🏃 Communicate with long-running git sessions
370
+
371
+ Introduced in `0.0.0.dev17` to:
372
+ - Make long-running git command sessions.
373
+ - Have reentrant sessions.
374
+ - Communicate with long-running git command session using their `stdin` and `stdout`.
375
+
376
+ Get long-running process sessions and communicate with them for batching and faster operations.
377
+
378
+ ```python
379
+ import gitbolt
380
+ from gitbolt.subprocess.utils.session import cat_file_blob_content, cat_file_tree_content
381
+
382
+ git = gitbolt.get_git_command()
383
+ with git.session(
384
+ cat_file1=["cat-file", "--batch"],
385
+ cat_file2=["cat-file", "--batch"],
386
+ cat_file3=lambda: git.subcmd_unchecked.popen(["cat-file", "--batch"]), # pass your own git subcmd Popen(s)
387
+ mktree=["mktree"]
388
+ ) as ses:
389
+ for mode_, sha_, name_ in cat_file_tree_content(ses.commands.cat_file2, b"HEAD^{tree}"):
390
+ print(mode_, sha_, name_)
391
+ print("+" * 40)
392
+ print(cat_file_blob_content(ses.commands.cat_file3, b"9854cb4d432a881f59d38582791cf2636e7819d9"))
393
+ with ses: # is reentrant, if you want
394
+ ses.commands.mktree.stdin.write(b"100644 blob <some-blob-hash> <some-filename>\n")
395
+ ses.commands.mktree.stdin.flush()
396
+ tree_hash = ses.commands.mktree.stdout.read()
397
+
398
+ assert not ses.active # session no long active
399
+ ```
400
+
369
401
  #### 💻 Run commands received from CLI
370
402
 
371
403
  Introduced in `0.0.0.dev11` is the ability to take commands from CLI and run it inside `gitbolt`.
@@ -25,9 +25,11 @@ src/gitbolt/subprocess/base.py
25
25
  src/gitbolt/subprocess/constants.py
26
26
  src/gitbolt/subprocess/exceptions.py
27
27
  src/gitbolt/subprocess/ls_tree.py
28
- src/gitbolt/subprocess/utils.py
29
28
  src/gitbolt/subprocess/impl/__init__.py
30
29
  src/gitbolt/subprocess/impl/simple.py
31
30
  src/gitbolt/subprocess/runner/__init__.py
32
31
  src/gitbolt/subprocess/runner/base.py
33
- src/gitbolt/subprocess/runner/simple.py
32
+ src/gitbolt/subprocess/runner/simple.py
33
+ src/gitbolt/subprocess/utils/__init__.py
34
+ src/gitbolt/subprocess/utils/cmd.py
35
+ src/gitbolt/subprocess/utils/session.py
@@ -1,5 +1,5 @@
1
1
  vt-err-hndlr>=0.0.0dev6
2
- vt-commons>=0.0.1.dev9
2
+ vt-commons>=0.0.1.dev11
3
3
 
4
4
  [pygit2]
5
5
  pygit2
File without changes
File without changes