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.
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/PKG-INFO +34 -2
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/README.md +32 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/pyproject.toml +2 -2
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/__init__.py +1 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/base.py +165 -8
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/impl/simple.py +18 -1
- 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.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt.egg-info/PKG-INFO +34 -2
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt.egg-info/SOURCES.txt +4 -2
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt.egg-info/requires.txt +1 -1
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/LICENSE +0 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/setup.cfg +0 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/_internal_init.py +0 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/add.py +0 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/base.py +0 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/constants.py +0 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/exceptions.py +0 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/ls_tree.py +0 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/models.py +0 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/py.typed +0 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/pytest_plugin.py +0 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/__init__.py +0 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/_internal_init.py +0 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/add.py +0 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/constants.py +0 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/exceptions.py +0 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/impl/__init__.py +0 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/ls_tree.py +0 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/runner/__init__.py +0 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/runner/base.py +0 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/subprocess/runner/simple.py +0 -0
- /gitbolt-0.0.0.dev16/src/gitbolt/subprocess/utils.py → /gitbolt-0.0.0.dev18/src/gitbolt/subprocess/utils/cmd.py +0 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt/utils.py +0 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt.egg-info/dependency_links.txt +0 -0
- {gitbolt-0.0.0.dev16 → gitbolt-0.0.0.dev18}/src/gitbolt.egg-info/entry_points.txt +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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[
|
|
771
|
+
text: Literal[False] = False,
|
|
613
772
|
**popen_kwargs: Any,
|
|
614
|
-
) -> Popen[
|
|
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[
|
|
780
|
+
text: Literal[True] = True,
|
|
622
781
|
**popen_kwargs: Any,
|
|
623
|
-
) -> Popen[
|
|
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[
|
|
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
|
|
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
|
|
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,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>
|
|
@@ -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.
|
|
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
|
|
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
|