gitbolt 0.0.0.dev17__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 (38) hide show
  1. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/PKG-INFO +33 -1
  2. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/README.md +32 -0
  3. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/pyproject.toml +1 -1
  4. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/__init__.py +1 -0
  5. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/base.py +99 -29
  6. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/impl/simple.py +7 -2
  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.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt.egg-info/PKG-INFO +33 -1
  10. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt.egg-info/SOURCES.txt +4 -3
  11. gitbolt-0.0.0.dev17/src/gitbolt/subprocess/session.py +0 -33
  12. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/LICENSE +0 -0
  13. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/setup.cfg +0 -0
  14. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/_internal_init.py +0 -0
  15. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/add.py +0 -0
  16. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/base.py +0 -0
  17. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/constants.py +0 -0
  18. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/exceptions.py +0 -0
  19. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/ls_tree.py +0 -0
  20. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/models.py +0 -0
  21. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/py.typed +0 -0
  22. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/pytest_plugin.py +0 -0
  23. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/__init__.py +0 -0
  24. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/_internal_init.py +0 -0
  25. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/add.py +0 -0
  26. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/constants.py +0 -0
  27. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/exceptions.py +0 -0
  28. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/impl/__init__.py +0 -0
  29. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/ls_tree.py +0 -0
  30. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/runner/__init__.py +0 -0
  31. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/runner/base.py +0 -0
  32. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/runner/simple.py +0 -0
  33. /gitbolt-0.0.0.dev17/src/gitbolt/subprocess/utils.py → /gitbolt-0.0.0.dev18/src/gitbolt/subprocess/utils/cmd.py +0 -0
  34. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/utils.py +0 -0
  35. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt.egg-info/dependency_links.txt +0 -0
  36. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt.egg-info/entry_points.txt +0 -0
  37. {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt.egg-info/requires.txt +0 -0
  38. {gitbolt-0.0.0.dev17 → 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.dev17
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>
@@ -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.dev17"
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"
@@ -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,6 +7,7 @@ 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
12
13
  from contextlib import AbstractContextManager
@@ -326,65 +327,136 @@ class GitCommand(Git, ABC):
326
327
  >>> with _git.session(cat_file=["cat-file", "--batch"]) as ses: # obtain, start and ctx manage the session.
327
328
  ... pass # any communication can be done by Popen semantics.
328
329
 
330
+
331
+ Note: All commands/processes are assumed:
332
+
333
+ - in bytes mode.
334
+ - pipe their stdin and stdout.
335
+
329
336
  :param commands: list of string git commands suppliable to ``subprocess.Popen`` or ``subprocess.Popen`` lambdas.
330
337
  :returns: A (not yet started) long-running ``GitSession`` context manager.
331
338
  """
332
339
  ...
333
340
 
334
341
 
342
+ # TODO: extract a base session class from this
335
343
  class GitSession(HasGitUnderneath[GitCommand], AbstractContextManager):
336
-
337
344
  def __init__(self, git: GitCommand, **commands: Callable[[], Popen[bytes]]):
338
345
  """
339
- Context Manager to start a git long-running session. Useful when a command is to be held in open state and be
340
- communicated with its stdin and stdout. This is way faster that spawning multiple processes each time.
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.
341
349
 
342
350
  Examples:
343
351
 
344
352
  Obtain a session:
345
353
 
346
354
  >>> import gitbolt
355
+ >>> from gitbolt.subprocess.base import GitSession
347
356
  >>> _git = gitbolt.get_git_command()
348
- >>> ses = _git.session(ls_tree=["ls-tree", "HEAD"], cat_file=["cat-file", "--batch"])
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))
349
359
  >>> with ses: # start the session by ctx mgr
350
360
  ... pass # any communication can be done by Popen semantics.
351
361
 
352
362
  Start the session in one go:
353
363
 
354
- >>> with _git.session(cat_file=["cat-file", "--batch"]) as ses: # obtain, start and ctx manage the session.
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.
355
365
  ... pass # any communication can be done by Popen semantics.
356
366
 
367
+
368
+ Note: All processes are assumed:
369
+
370
+ - in bytes mode.
371
+ - pipe their stdin and stdout.
372
+
357
373
  :param git: ``gitbolt.subprocess.GitCommand`` instance.
358
374
  :param commands: commands in kwargs fashion.
359
375
  """
360
376
  self._git = git
361
377
  self.unstarted_commands: dict[str, Callable[[], Popen[bytes]]] = commands
362
378
  self.started_commands: dict[str, Popen[bytes]] = dict()
363
- self.commands: SimpleNamespace | None = None
379
+ self._commands: SimpleNamespace | None = None
380
+ self.__started = False
381
+ self.__done = False
382
+ self.__depth = 0
364
383
 
384
+ @override
365
385
  def __enter__(self) -> Self:
366
- processes_started_keys: list[str] = []
367
- for unstarted_command_key, unstarted_command in self.unstarted_commands.items():
368
- self.started_commands[unstarted_command_key] = unstarted_command().__enter__()
369
- processes_started_keys.append(unstarted_command_key)
370
- self.commands = SimpleNamespace(**self.started_commands)
371
- for pk in processes_started_keys:
372
- del self.unstarted_commands[pk]
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
373
402
 
403
+ @override
374
404
  @property
375
405
  def git(self) -> GitCommand:
376
406
  return self._git
377
407
 
378
- def __exit__(self, exc_type, exc_value, traceback, /):
379
- process_done_keys: list[str] = []
380
- for started_popen_key, started_popen in self.started_commands.items():
381
- started_popen.__exit__(exc_type, exc_value, traceback)
382
- process_done_keys.append(started_popen_key)
383
- self.commands = None
384
- for pk in process_done_keys:
385
- del self.started_commands[pk]
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
386
420
  return False
387
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
+
388
460
 
389
461
  class GitSubcmdCommand(GitSubCommand, HasGitUnderneath["GitCommand"], Protocol):
390
462
  """
@@ -696,30 +768,30 @@ class UncheckedSubcmd(GitSubcmdCommand, RootDirOp, Protocol):
696
768
  self,
697
769
  subcommand_args: list[str],
698
770
  *popen_args: Any,
699
- text: Literal[True] = True,
771
+ text: Literal[False] = False,
700
772
  **popen_kwargs: Any,
701
- ) -> Popen[str]: ...
773
+ ) -> Popen[bytes]: ...
702
774
 
703
775
  @overload
704
776
  def popen(
705
777
  self,
706
778
  subcommand_args: list[str],
707
779
  *popen_args: Any,
708
- text: Literal[False] = False,
780
+ text: Literal[True] = True,
709
781
  **popen_kwargs: Any,
710
- ) -> Popen[bytes]: ...
782
+ ) -> Popen[str]: ...
711
783
 
712
784
  def popen(
713
785
  self,
714
786
  subcommand_args: list[str],
715
787
  *popen_args: Any,
716
- text: Literal[True, False] = False,
788
+ text: Literal[False, True] = False,
717
789
  **popen_kwargs: Any,
718
790
  ) -> Popen[str] | Popen[bytes]:
719
791
  """
720
792
  Open unchecked git subcommand communicable process, using ``subprocess.Popen``.
721
793
 
722
- 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.
723
795
 
724
796
  :param subcommand_args: the full subcommand argument list.
725
797
  :param popen_args: additional subprocess positionals.
@@ -734,7 +806,6 @@ class UncheckedSubcmd(GitSubcmdCommand, RootDirOp, Protocol):
734
806
  stdin = popen_kwargs.pop("stdin", PIPE)
735
807
  stdout = popen_kwargs.pop("stdout", PIPE)
736
808
  stderr = popen_kwargs.pop("stderr", PIPE)
737
- bufsize = popen_kwargs.pop("bufsize", 0)
738
809
  # Popen the git command
739
810
  result = self.git.runner.popen_git_command(
740
811
  main_cmd_args,
@@ -746,7 +817,6 @@ class UncheckedSubcmd(GitSubcmdCommand, RootDirOp, Protocol):
746
817
  stdin=stdin,
747
818
  stdout=stdout,
748
819
  stderr=stderr,
749
- bufsize=bufsize,
750
820
  **popen_kwargs,
751
821
  )
752
822
  return result
@@ -170,7 +170,6 @@ class UncheckedSubcmdImpl(UncheckedSubcmd, GitSubcmdCommandImpl):
170
170
 
171
171
 
172
172
  class SimpleGitCommand(GitCommand, RootDirOp):
173
-
174
173
  def __init__(
175
174
  self,
176
175
  git_root_dir: Path = Path.cwd(),
@@ -250,11 +249,17 @@ class SimpleGitCommand(GitCommand, RootDirOp):
250
249
 
251
250
  def session(self, **commands: list[str] | Callable[[], Popen[bytes]]) -> GitSession:
252
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
+
253
258
  for cmd_name, runnable_cmd in commands.items():
254
259
  if callable(runnable_cmd):
255
260
  cmds[cmd_name] = runnable_cmd
256
261
  else:
257
- cmds[cmd_name] = lambda : self.subcmd_unchecked.popen(runnable_cmd, text=False)
262
+ cmds[cmd_name] = lambda_for_subcmd_popen(runnable_cmd)
258
263
  return GitSession(self, **cmds)
259
264
 
260
265
 
@@ -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.dev17
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>
@@ -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,10 +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/session.py
29
- src/gitbolt/subprocess/utils.py
30
28
  src/gitbolt/subprocess/impl/__init__.py
31
29
  src/gitbolt/subprocess/impl/simple.py
32
30
  src/gitbolt/subprocess/runner/__init__.py
33
31
  src/gitbolt/subprocess/runner/base.py
34
- 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,33 +0,0 @@
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
- import subprocess
12
-
13
- from vt.utils.commons.commons.core_py import read_exact
14
-
15
-
16
- def cat_file_blob_content(cat_file_popen: subprocess.Popen[bytes], blob_hash: bytes) -> bytes:
17
- """
18
- Read git blob contents using a long-running batched cat-file process.
19
-
20
- Spawning new ``git show`` processes can be slower and resource consuming.
21
-
22
- :param cat_file_popen: long-running ``git cat-file --batch`` process in bytes mode.
23
- :param blob_hash: hash to be read from cat-file.
24
- :return: contents of blob hash in bytes.
25
- """
26
- write_obj = blob_hash + b"\n"
27
- cat_file_popen.stdin.write(write_obj)
28
- cat_file_popen.stdin.flush()
29
- header = cat_file_popen.stdout.readline()
30
- obj, typ, size = header.split()
31
- blob_content: bytes = read_exact(cat_file_popen.stdout, int(size))
32
- cat_file_popen.stdout.readline()
33
- return blob_content
File without changes
File without changes