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.
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/PKG-INFO +33 -1
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/README.md +32 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/pyproject.toml +1 -1
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/__init__.py +1 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/base.py +99 -29
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/impl/simple.py +7 -2
- gitbolt-0.0.0.dev18/src/gitbolt/subprocess/utils/__init__.py +6 -0
- gitbolt-0.0.0.dev18/src/gitbolt/subprocess/utils/session.py +134 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt.egg-info/PKG-INFO +33 -1
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt.egg-info/SOURCES.txt +4 -3
- gitbolt-0.0.0.dev17/src/gitbolt/subprocess/session.py +0 -33
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/LICENSE +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/setup.cfg +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/_internal_init.py +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/add.py +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/base.py +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/constants.py +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/exceptions.py +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/ls_tree.py +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/models.py +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/py.typed +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/pytest_plugin.py +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/__init__.py +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/_internal_init.py +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/add.py +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/constants.py +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/exceptions.py +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/impl/__init__.py +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/ls_tree.py +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/runner/__init__.py +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/runner/base.py +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/runner/simple.py +0 -0
- /gitbolt-0.0.0.dev17/src/gitbolt/subprocess/utils.py → /gitbolt-0.0.0.dev18/src/gitbolt/subprocess/utils/cmd.py +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt/utils.py +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt.egg-info/dependency_links.txt +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt.egg-info/entry_points.txt +0 -0
- {gitbolt-0.0.0.dev17 → gitbolt-0.0.0.dev18}/src/gitbolt.egg-info/requires.txt +0 -0
- {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.
|
|
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`.
|
|
@@ -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
|
-
|
|
340
|
-
communicated with its stdin and stdout. This is way faster that spawning multiple
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
process_done_keys
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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[
|
|
771
|
+
text: Literal[False] = False,
|
|
700
772
|
**popen_kwargs: Any,
|
|
701
|
-
) -> Popen[
|
|
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[
|
|
780
|
+
text: Literal[True] = True,
|
|
709
781
|
**popen_kwargs: Any,
|
|
710
|
-
) -> Popen[
|
|
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[
|
|
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
|
|
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] =
|
|
262
|
+
cmds[cmd_name] = lambda_for_subcmd_popen(runnable_cmd)
|
|
258
263
|
return GitSession(self, **cmds)
|
|
259
264
|
|
|
260
265
|
|
|
@@ -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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|