gitbolt 0.0.0.dev18__tar.gz → 0.0.0.dev20__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.dev18 → gitbolt-0.0.0.dev20}/PKG-INFO +19 -15
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/README.md +18 -14
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/pyproject.toml +1 -1
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/__init__.py +1 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/base.py +20 -2
- gitbolt-0.0.0.dev20/src/gitbolt/subprocess/utils/models.py +97 -0
- gitbolt-0.0.0.dev20/src/gitbolt/subprocess/utils/session.py +583 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt.egg-info/PKG-INFO +19 -15
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt.egg-info/SOURCES.txt +1 -0
- gitbolt-0.0.0.dev18/src/gitbolt/subprocess/utils/session.py +0 -134
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/LICENSE +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/setup.cfg +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/__init__.py +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/_internal_init.py +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/add.py +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/base.py +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/constants.py +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/exceptions.py +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/ls_tree.py +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/models.py +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/py.typed +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/pytest_plugin.py +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/_internal_init.py +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/add.py +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/constants.py +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/exceptions.py +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/impl/__init__.py +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/impl/simple.py +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/ls_tree.py +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/runner/__init__.py +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/runner/base.py +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/runner/simple.py +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/utils/__init__.py +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/utils/cmd.py +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/utils.py +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt.egg-info/dependency_links.txt +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt.egg-info/entry_points.txt +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt.egg-info/requires.txt +0 -0
- {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/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.dev20
|
|
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>
|
|
@@ -146,7 +146,7 @@ print(version_stdout)
|
|
|
146
146
|
|
|
147
147
|
#### 🧑💻 Modular at the programmatic level
|
|
148
148
|
|
|
149
|
-
Commands are designed to be passed around as objects. This makes them modular and thus users can opt to use only
|
|
149
|
+
Commands are designed to be passed around as objects. This makes them modular and thus users can opt to use only
|
|
150
150
|
particular commands.
|
|
151
151
|
|
|
152
152
|
```python
|
|
@@ -327,7 +327,7 @@ no_advice_reset_git = overridden_git.git_opts_override(no_advice=False)
|
|
|
327
327
|
|
|
328
328
|
At last, run unchecked commands in git.
|
|
329
329
|
|
|
330
|
-
Introduced in `0.0.0.dev4` to
|
|
330
|
+
Introduced in `0.0.0.dev4` to
|
|
331
331
|
- experiment.
|
|
332
332
|
- have consistent interfaced commands run until all subcommands are provided by the library.
|
|
333
333
|
|
|
@@ -377,32 +377,36 @@ Get long-running process sessions and communicate with them for batching and fas
|
|
|
377
377
|
|
|
378
378
|
```python
|
|
379
379
|
import gitbolt
|
|
380
|
-
from gitbolt.subprocess.utils.session import cat_file_blob_content, cat_file_tree_content
|
|
380
|
+
from gitbolt.subprocess.utils.session import cat_file_blob_content, cat_file_tree_data, cat_file_tree_content, \
|
|
381
|
+
mktree_tree_make
|
|
381
382
|
|
|
382
383
|
git = gitbolt.get_git_command()
|
|
383
384
|
with git.session(
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
385
|
+
cat_file1=["cat-file", "--batch"],
|
|
386
|
+
cat_file2=["cat-file", "--batch"],
|
|
387
|
+
cat_file3=lambda: git.subcmd_unchecked.popen(["cat-file", "--batch"]), # pass your own git subcmd Popen(s)
|
|
388
|
+
mktree=["mktree", "--batch", "-z"]
|
|
388
389
|
) as ses:
|
|
389
|
-
|
|
390
|
+
tree_data: list[bytes] = []
|
|
391
|
+
for mode_, sha_, name_ in cat_file_tree_data(ses.commands.cat_file2, b"HEAD^{tree}"):
|
|
390
392
|
print(mode_, sha_, name_)
|
|
393
|
+
for tree_entry in cat_file_tree_content(ses.commands.cat_file2, b"HEAD^{tree}"):
|
|
394
|
+
tree_data.append(tree_entry)
|
|
391
395
|
print("+" * 40)
|
|
392
396
|
print(cat_file_blob_content(ses.commands.cat_file3, b"9854cb4d432a881f59d38582791cf2636e7819d9"))
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
ses.commands.mktree.
|
|
396
|
-
|
|
397
|
+
print("~" * 40)
|
|
398
|
+
with ses: # is reentrant, if you want
|
|
399
|
+
tree_hash = mktree_tree_make(ses.commands.mktree, b"\0".join(tree_data)) # make tree
|
|
400
|
+
print("Obtained tree: ", tree_hash)
|
|
397
401
|
|
|
398
|
-
assert not ses.active
|
|
402
|
+
assert not ses.active # session no long active
|
|
399
403
|
```
|
|
400
404
|
|
|
401
405
|
#### 💻 Run commands received from CLI
|
|
402
406
|
|
|
403
407
|
Introduced in `0.0.0.dev11` is the ability to take commands from CLI and run it inside `gitbolt`.
|
|
404
408
|
|
|
405
|
-
While making a system it may be required to run cli commands as received from cli using gitbolt. An obvious example
|
|
409
|
+
While making a system it may be required to run cli commands as received from cli using gitbolt. An obvious example
|
|
406
410
|
would be to make a system that receives CLI commands and does certain modifications/additions inside `gitbolt` before
|
|
407
411
|
actually running them. An example:
|
|
408
412
|
|
|
@@ -110,7 +110,7 @@ print(version_stdout)
|
|
|
110
110
|
|
|
111
111
|
#### 🧑💻 Modular at the programmatic level
|
|
112
112
|
|
|
113
|
-
Commands are designed to be passed around as objects. This makes them modular and thus users can opt to use only
|
|
113
|
+
Commands are designed to be passed around as objects. This makes them modular and thus users can opt to use only
|
|
114
114
|
particular commands.
|
|
115
115
|
|
|
116
116
|
```python
|
|
@@ -291,7 +291,7 @@ no_advice_reset_git = overridden_git.git_opts_override(no_advice=False)
|
|
|
291
291
|
|
|
292
292
|
At last, run unchecked commands in git.
|
|
293
293
|
|
|
294
|
-
Introduced in `0.0.0.dev4` to
|
|
294
|
+
Introduced in `0.0.0.dev4` to
|
|
295
295
|
- experiment.
|
|
296
296
|
- have consistent interfaced commands run until all subcommands are provided by the library.
|
|
297
297
|
|
|
@@ -341,32 +341,36 @@ Get long-running process sessions and communicate with them for batching and fas
|
|
|
341
341
|
|
|
342
342
|
```python
|
|
343
343
|
import gitbolt
|
|
344
|
-
from gitbolt.subprocess.utils.session import cat_file_blob_content, cat_file_tree_content
|
|
344
|
+
from gitbolt.subprocess.utils.session import cat_file_blob_content, cat_file_tree_data, cat_file_tree_content, \
|
|
345
|
+
mktree_tree_make
|
|
345
346
|
|
|
346
347
|
git = gitbolt.get_git_command()
|
|
347
348
|
with git.session(
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
349
|
+
cat_file1=["cat-file", "--batch"],
|
|
350
|
+
cat_file2=["cat-file", "--batch"],
|
|
351
|
+
cat_file3=lambda: git.subcmd_unchecked.popen(["cat-file", "--batch"]), # pass your own git subcmd Popen(s)
|
|
352
|
+
mktree=["mktree", "--batch", "-z"]
|
|
352
353
|
) as ses:
|
|
353
|
-
|
|
354
|
+
tree_data: list[bytes] = []
|
|
355
|
+
for mode_, sha_, name_ in cat_file_tree_data(ses.commands.cat_file2, b"HEAD^{tree}"):
|
|
354
356
|
print(mode_, sha_, name_)
|
|
357
|
+
for tree_entry in cat_file_tree_content(ses.commands.cat_file2, b"HEAD^{tree}"):
|
|
358
|
+
tree_data.append(tree_entry)
|
|
355
359
|
print("+" * 40)
|
|
356
360
|
print(cat_file_blob_content(ses.commands.cat_file3, b"9854cb4d432a881f59d38582791cf2636e7819d9"))
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
ses.commands.mktree.
|
|
360
|
-
|
|
361
|
+
print("~" * 40)
|
|
362
|
+
with ses: # is reentrant, if you want
|
|
363
|
+
tree_hash = mktree_tree_make(ses.commands.mktree, b"\0".join(tree_data)) # make tree
|
|
364
|
+
print("Obtained tree: ", tree_hash)
|
|
361
365
|
|
|
362
|
-
assert not ses.active
|
|
366
|
+
assert not ses.active # session no long active
|
|
363
367
|
```
|
|
364
368
|
|
|
365
369
|
#### 💻 Run commands received from CLI
|
|
366
370
|
|
|
367
371
|
Introduced in `0.0.0.dev11` is the ability to take commands from CLI and run it inside `gitbolt`.
|
|
368
372
|
|
|
369
|
-
While making a system it may be required to run cli commands as received from cli using gitbolt. An obvious example
|
|
373
|
+
While making a system it may be required to run cli commands as received from cli using gitbolt. An obvious example
|
|
370
374
|
would be to make a system that receives CLI commands and does certain modifications/additions inside `gitbolt` before
|
|
371
375
|
actually running them. An example:
|
|
372
376
|
|
|
@@ -12,4 +12,5 @@ from gitbolt.subprocess.base import AddCommand as AddCommand
|
|
|
12
12
|
from gitbolt.subprocess.base import LsTreeCommand as LsTreeCommand
|
|
13
13
|
from gitbolt.subprocess.base import VersionCommand as VersionCommand
|
|
14
14
|
from gitbolt.subprocess.base import UncheckedSubcmd as UncheckedSubcmd
|
|
15
|
+
from gitbolt.subprocess.base import GitSession as GitSession
|
|
15
16
|
# endregion
|
|
@@ -307,8 +307,7 @@ class GitCommand(Git, ABC):
|
|
|
307
307
|
@abstractmethod
|
|
308
308
|
def session(self, **commands: list[str] | Callable[[], Popen[bytes]]) -> GitSession:
|
|
309
309
|
"""
|
|
310
|
-
Run long-running git session with multiple unchecked subcommands using ``subprocess.Popen`` and
|
|
311
|
-
communicate with them.
|
|
310
|
+
Run long-running git session with multiple unchecked subcommands using ``subprocess.Popen`` and communicate with them.
|
|
312
311
|
|
|
313
312
|
Client/Caller can communicate with ``GitSession``'s processes with their stdin and stdout.
|
|
314
313
|
|
|
@@ -327,6 +326,25 @@ class GitCommand(Git, ABC):
|
|
|
327
326
|
>>> with _git.session(cat_file=["cat-file", "--batch"]) as ses: # obtain, start and ctx manage the session.
|
|
328
327
|
... pass # any communication can be done by Popen semantics.
|
|
329
328
|
|
|
329
|
+
Run special Popen:
|
|
330
|
+
|
|
331
|
+
>>> import gitbolt
|
|
332
|
+
>>> _git = gitbolt.get_git_command()
|
|
333
|
+
>>> ses = _git.session(ls_tree=["ls-tree", "HEAD"],
|
|
334
|
+
... cat_file=lambda : _git.subcmd_unchecked.popen(["cat-file", "--batch"], env=None))
|
|
335
|
+
>>> with ses: # start the session by ctx mgr
|
|
336
|
+
... pass # any communication can be done by Popen semantics.
|
|
337
|
+
|
|
338
|
+
Is reentrant:
|
|
339
|
+
|
|
340
|
+
>>> import gitbolt
|
|
341
|
+
>>> _git = gitbolt.get_git_command()
|
|
342
|
+
>>> ses = _git.session(cat_file=["cat-file", "--batch-check"])
|
|
343
|
+
>>> with ses: # start the session by ctx mgr
|
|
344
|
+
... with ses: # reentrant
|
|
345
|
+
... pass
|
|
346
|
+
... pass # any communication can be done by Popen semantics.
|
|
347
|
+
|
|
330
348
|
|
|
331
349
|
Note: All commands/processes are assumed:
|
|
332
350
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# coding=utf-8
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Models useful for session utilities.
|
|
6
|
+
"""
|
|
7
|
+
import dataclasses
|
|
8
|
+
import datetime
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclasses.dataclass
|
|
12
|
+
class GitRawActor:
|
|
13
|
+
name: bytes
|
|
14
|
+
email: bytes
|
|
15
|
+
time: datetime.datetime
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def from_commit_bytes(cls, commit_bytes: bytes, email_start_pattern: bytes = b" <",
|
|
19
|
+
email_end_pattern: bytes = b"> ") -> "GitRawActor":
|
|
20
|
+
"""
|
|
21
|
+
Obtain ``Actor`` object from the author/committer bytes.
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
|
|
25
|
+
Simple timestamp without timezone:
|
|
26
|
+
|
|
27
|
+
>>> _ss_actor1 = GitRawActor.from_commit_bytes(b"Suhas <sss@ss.ss> 1780211249")
|
|
28
|
+
>>> assert _ss_actor1.name == b"Suhas"
|
|
29
|
+
>>> assert _ss_actor1.email == b"sss@ss.ss"
|
|
30
|
+
>>> assert _ss_actor1.time.timestamp() == 1780211249.0
|
|
31
|
+
|
|
32
|
+
Simple timestamp with timezone:
|
|
33
|
+
|
|
34
|
+
>>> _ss_actor2 = GitRawActor.from_commit_bytes(b"Suhas Srivastava <sss@vaastav.tech> 1780211249 +0530")
|
|
35
|
+
>>> assert _ss_actor2.name == b"Suhas Srivastava"
|
|
36
|
+
>>> assert _ss_actor2.email == b"sss@vaastav.tech"
|
|
37
|
+
|
|
38
|
+
# failing timezone test >>> assert _ss_actor2.time.timestamp() == 1780211249.0
|
|
39
|
+
|
|
40
|
+
:param commit_bytes: author/committer information in bytes form.
|
|
41
|
+
:param email_start_pattern: email spearates name and timestamp. Thus is required as a separator.
|
|
42
|
+
Marks email beginning.
|
|
43
|
+
:param email_end_pattern: email spearates name and timestamp. Thus is required as a separator.
|
|
44
|
+
Marks email ending.
|
|
45
|
+
:returns: ``Actor`` object with all the parsed and set values.
|
|
46
|
+
"""
|
|
47
|
+
email_pattern_start_index = commit_bytes.find(email_start_pattern)
|
|
48
|
+
email_pattern_end_index = commit_bytes.rfind(email_end_pattern)
|
|
49
|
+
email_start_index = email_pattern_start_index+len(email_start_pattern)
|
|
50
|
+
email_end_index = email_pattern_end_index
|
|
51
|
+
name = commit_bytes[:email_pattern_start_index]
|
|
52
|
+
email = commit_bytes[email_start_index:email_end_index]
|
|
53
|
+
# TODO: handle timezone correctly
|
|
54
|
+
time_bytes = commit_bytes[email_end_index+len(email_end_pattern):]
|
|
55
|
+
time_main, time_zone = time_bytes.split() if b" " in time_bytes else (time_bytes, b"")
|
|
56
|
+
date_time_for_iso_str = datetime.datetime.fromtimestamp(int(time_main))
|
|
57
|
+
date_time = datetime.datetime.fromisoformat(f"{date_time_for_iso_str.date()}T{date_time_for_iso_str.time()}{time_zone.decode()}")
|
|
58
|
+
return GitRawActor(name, email, date_time)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclasses.dataclass
|
|
62
|
+
class GitRawSignature:
|
|
63
|
+
signature: bytes
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclasses.dataclass
|
|
67
|
+
class GPGGitRawSignature(GitRawSignature):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclasses.dataclass
|
|
72
|
+
class SSHGitRawSignature(GitRawSignature):
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclasses.dataclass
|
|
77
|
+
class RawCommitBytesObj:
|
|
78
|
+
"""
|
|
79
|
+
Commit info all in bytes as read from ``git cat-file --batch``.
|
|
80
|
+
"""
|
|
81
|
+
commit_hash: bytes
|
|
82
|
+
tree_hash: bytes
|
|
83
|
+
parents: list[bytes]
|
|
84
|
+
author: GitRawActor
|
|
85
|
+
committer: GitRawActor
|
|
86
|
+
signature: GitRawSignature | None
|
|
87
|
+
message: bytes
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclasses.dataclass
|
|
91
|
+
class RawBytesValsOfCommit:
|
|
92
|
+
tree_val: bytes
|
|
93
|
+
commit_parents_vals: list[bytes]
|
|
94
|
+
author_val: bytes
|
|
95
|
+
committer_val: bytes
|
|
96
|
+
signature_val: bytes | None
|
|
97
|
+
commit_message: bytes
|
|
@@ -0,0 +1,583 @@
|
|
|
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
|
+
from typing import Iterable, IO, cast
|
|
13
|
+
|
|
14
|
+
from vt.utils.errors.error_specs import ERR_INVALID_USAGE, ERR_DATA_FORMAT_ERR
|
|
15
|
+
|
|
16
|
+
from gitbolt.exceptions import GitExitingException
|
|
17
|
+
from gitbolt.subprocess.utils.models import GitRawActor, GitRawSignature, GPGGitRawSignature, SSHGitRawSignature, \
|
|
18
|
+
RawCommitBytesObj, RawBytesValsOfCommit
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# region blob
|
|
22
|
+
def cat_file_blob_content(
|
|
23
|
+
cat_file_popen: subprocess.Popen[bytes], blob_hash: bytes, require_type: bytes | None = None
|
|
24
|
+
) -> bytes:
|
|
25
|
+
"""
|
|
26
|
+
Read git blob contents using a long-running batched cat-file process.
|
|
27
|
+
|
|
28
|
+
Spawning new ``git show`` processes can be slower and resource consuming.
|
|
29
|
+
|
|
30
|
+
Note: ``cat_file_popen`` must pipe its ``stdin`` and ``stdout`` and run in ``bytes`` mode.
|
|
31
|
+
|
|
32
|
+
:param cat_file_popen: long-running ``git cat-file --batch`` process in bytes mode and pipes its stdin and stdout.
|
|
33
|
+
:param blob_hash: hash to be read from cat-file.
|
|
34
|
+
:param require_type: Optional git object type for validation.
|
|
35
|
+
:return: contents of blob hash in bytes.
|
|
36
|
+
:raises GitExitingException: when the found object type is not the required type.
|
|
37
|
+
"""
|
|
38
|
+
obj_hash, typ, _, blob_content = cat_file_data(cat_file_popen, blob_hash)
|
|
39
|
+
if require_type and typ != require_type:
|
|
40
|
+
raise GitExitingException(f"Required Git object type: '{require_type.decode()}', found: '{typ.decode()}'",
|
|
41
|
+
exit_code=ERR_INVALID_USAGE) from ValueError(obj_hash)
|
|
42
|
+
return blob_content
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def cat_file_data(cat_file_popen: subprocess.Popen[bytes], blob_hash: bytes) -> tuple[bytes, bytes, bytes, bytes]:
|
|
46
|
+
"""
|
|
47
|
+
Read git contents using a long-running batched cat-file process. Best for programmatic use.
|
|
48
|
+
|
|
49
|
+
Spawning new ``git show`` processes can be slower and resource consuming.
|
|
50
|
+
|
|
51
|
+
Note: ``cat_file_popen`` must pipe its ``stdin`` and ``stdout`` and run in ``bytes`` mode.
|
|
52
|
+
|
|
53
|
+
:param cat_file_popen: long-running ``git cat-file --batch`` process in bytes mode and pipes its stdin and stdout.
|
|
54
|
+
:param blob_hash: hash to be read from cat-file.
|
|
55
|
+
:return: (hash, type, size, content) of the read stream.
|
|
56
|
+
"""
|
|
57
|
+
write_obj = blob_hash + b"\n"
|
|
58
|
+
cat_file_popen_stdin: IO[bytes] = cast(IO[bytes], cat_file_popen.stdin) # stdin is assumed to be piped
|
|
59
|
+
cat_file_popen_stdout: IO[bytes] = cast(IO[bytes], cat_file_popen.stdout) # stdout is assumed to be piped
|
|
60
|
+
cat_file_popen_stdin.write(write_obj)
|
|
61
|
+
cat_file_popen_stdin.flush()
|
|
62
|
+
header = cat_file_popen_stdout.readline()
|
|
63
|
+
obj, typ, size, blob_content = cat_file_read(cat_file_popen_stdout, header)
|
|
64
|
+
cat_file_popen_stdout.read(1)
|
|
65
|
+
return obj, typ, size, blob_content
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def cat_file_read(stream: IO[bytes], header: bytes) -> tuple[bytes, bytes, bytes, bytes]:
|
|
69
|
+
"""
|
|
70
|
+
``git cat-file`` produces contents by first giving a header that contains:
|
|
71
|
+
|
|
72
|
+
- object hash.
|
|
73
|
+
- type (commit, tree, blob, ..., etc.).
|
|
74
|
+
- size - the actual content is next ``size`` bytes long.
|
|
75
|
+
|
|
76
|
+
:param stream: stream to read ``git cat-file --batch`` bytes from.
|
|
77
|
+
:param header: the header line of ``git cat-file --batch``.
|
|
78
|
+
:returns: (hash, type, size, content) of the read stream.
|
|
79
|
+
"""
|
|
80
|
+
obj, typ, size = header.split()
|
|
81
|
+
blob_content: bytes = stream.read(int(size))
|
|
82
|
+
return obj, typ, size, blob_content
|
|
83
|
+
# endregion
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# region tree
|
|
87
|
+
# region query tree
|
|
88
|
+
def parse_cat_file_tree(git_cat_file_data: bytes) -> Iterable[tuple[bytes, bytes, bytes]]:
|
|
89
|
+
"""
|
|
90
|
+
Parse bytes tree data.
|
|
91
|
+
|
|
92
|
+
Typically best for parsing tree data from the stdout of a long-running cat-file query for tree.
|
|
93
|
+
|
|
94
|
+
:param git_cat_file_data: tree data in bytes format as given by ``git cat-file --batch`` for a tree query.
|
|
95
|
+
:returns: iterable of (mode, sha, filename/filepath).
|
|
96
|
+
"""
|
|
97
|
+
i = 0
|
|
98
|
+
n = len(git_cat_file_data)
|
|
99
|
+
while i < n:
|
|
100
|
+
# mode
|
|
101
|
+
j = git_cat_file_data.find(b" ", i)
|
|
102
|
+
mode = git_cat_file_data[i:j]
|
|
103
|
+
# filename
|
|
104
|
+
k = git_cat_file_data.find(b"\x00", j)
|
|
105
|
+
name = git_cat_file_data[j + 1: k]
|
|
106
|
+
# sha (20 bytes binary)
|
|
107
|
+
sha = git_cat_file_data[k + 1: k + 21]
|
|
108
|
+
|
|
109
|
+
yield mode, sha.hex().encode(), name
|
|
110
|
+
i = k + 21
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def cat_file_tree_data(
|
|
114
|
+
cat_file_popen: subprocess.Popen[bytes], tree_hash: bytes, recursive: bool = False,
|
|
115
|
+
_prefix: bytes = b"",
|
|
116
|
+
) -> Iterable[tuple[bytes, bytes, bytes]]:
|
|
117
|
+
"""
|
|
118
|
+
Get tree data from the stdout of a long-running cat-file query for tree hash.
|
|
119
|
+
|
|
120
|
+
Note: ``cat_file_popen`` must pipe its ``stdin`` and ``stdout`` and run in ``bytes`` mode.
|
|
121
|
+
|
|
122
|
+
:param cat_file_popen: long-running ``git cat-file --batch`` process in bytes mode and pipes its stdin and stdout.
|
|
123
|
+
:param tree_hash: tree hash to be read from cat-file.
|
|
124
|
+
:param recursive: recursively query the full tree.
|
|
125
|
+
:param _prefix: prefix, useful when doing recursive tree query (only for internal use).
|
|
126
|
+
:return: iterable of (mode, sha, filename).
|
|
127
|
+
:raises GitExitingException: when ``tree_hash`` is not the hash of a valid git tree.
|
|
128
|
+
"""
|
|
129
|
+
tree_content = cat_file_blob_content(cat_file_popen, tree_hash)
|
|
130
|
+
if not recursive:
|
|
131
|
+
for mode, sha, name in parse_cat_file_tree(tree_content):
|
|
132
|
+
# leaf node (blob, symlink, submodule)
|
|
133
|
+
yield mode, sha, name
|
|
134
|
+
else:
|
|
135
|
+
for mode, sha, name in parse_cat_file_tree(tree_content):
|
|
136
|
+
full_name = _prefix + name
|
|
137
|
+
|
|
138
|
+
if mode == b"40000": # directory (tree)
|
|
139
|
+
# recurse into subtree
|
|
140
|
+
yield from cat_file_tree_data(
|
|
141
|
+
cat_file_popen,
|
|
142
|
+
sha,
|
|
143
|
+
recursive=True,
|
|
144
|
+
_prefix=full_name + b"/",
|
|
145
|
+
)
|
|
146
|
+
else:
|
|
147
|
+
# leaf node (blob, symlink, submodule)
|
|
148
|
+
yield mode, sha, full_name
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def cat_file_tree_content(cat_file_popen: subprocess.Popen[bytes], tree_hash: bytes, recursive: bool = False,
|
|
152
|
+
format_: bytes = b"%(objmode)06o %(objtype)s %(objhash)s\x09%(objpath)s") -> Iterable[bytes]:
|
|
153
|
+
"""
|
|
154
|
+
Get tree data from the stdout of a long-running cat-file query for tree hash in the format specified.
|
|
155
|
+
|
|
156
|
+
Note: ``cat_file_popen`` must pipe its ``stdin`` and ``stdout`` and run in ``bytes`` mode.
|
|
157
|
+
|
|
158
|
+
Query format:
|
|
159
|
+
|
|
160
|
+
- objmode: `100644`, `100755`, `120000`, `040000`, `160000`.
|
|
161
|
+
- objtype: `blob`, `tree`, `commit`.
|
|
162
|
+
- objhash: sha hash of the object.
|
|
163
|
+
- objpath: full path of the object from repo root.
|
|
164
|
+
|
|
165
|
+
Default format usage and explanation:
|
|
166
|
+
|
|
167
|
+
- %(objmode)06o: 06-show six-chars left-padded with 0 (zeros), o - octal number.
|
|
168
|
+
|
|
169
|
+
:param cat_file_popen: long-running ``git cat-file --batch`` process in bytes mode and pipes its stdin and stdout.
|
|
170
|
+
:param tree_hash: tree hash to be read from cat-file.
|
|
171
|
+
:param recursive: recursively query the full tree.
|
|
172
|
+
:param format_: the format in which the queried tree data will be formatted. Follows bytes ``%`` operator
|
|
173
|
+
interpolation. Defaults to the typical ``git ls-tree`` format.
|
|
174
|
+
:return: iterable of tree data in the queried ``format_``.
|
|
175
|
+
:raises GitExitingException: when ``tree_hash`` is not the hash of a valid git tree.
|
|
176
|
+
"""
|
|
177
|
+
for mode, sha, filename in cat_file_tree_data(cat_file_popen, tree_hash, recursive):
|
|
178
|
+
if mode == b"40000":
|
|
179
|
+
# directory
|
|
180
|
+
objtype = b"tree"
|
|
181
|
+
elif mode == b"160000":
|
|
182
|
+
# submodule
|
|
183
|
+
objtype = b"commit"
|
|
184
|
+
elif mode in (b"100644", b"100755", b"120000"):
|
|
185
|
+
# symlnk, executable, regular files
|
|
186
|
+
objtype = b"blob"
|
|
187
|
+
else:
|
|
188
|
+
raise GitExitingException(f"Invalid object mode: {mode.decode()}") from ValueError(tree_hash)
|
|
189
|
+
objmode = int(mode, 8)
|
|
190
|
+
yield format_ % {b"objmode": objmode, b"objtype": objtype, b"objhash": sha, b"objpath": filename}
|
|
191
|
+
# endregion
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# region make tree
|
|
195
|
+
def mktree_tree_make(mktree_popen: subprocess.Popen[bytes], mktree_null_delimited_content: bytes) -> bytes:
|
|
196
|
+
"""
|
|
197
|
+
Make a git tree using ``mktree`` Popen and tree data to get a git tree.
|
|
198
|
+
|
|
199
|
+
Natively supports null-terminated tree entries or ``git mktree --batch -z`` mode.
|
|
200
|
+
|
|
201
|
+
:param mktree_popen: long-running ``mktree --batch`` subprocess.Popen.
|
|
202
|
+
:param mktree_null_delimited_content: the tree data content as taken on stdin on ``mktree --batch -z``. These
|
|
203
|
+
contents are null (``\0``) delimited.
|
|
204
|
+
:returns: the created tree hash.
|
|
205
|
+
"""
|
|
206
|
+
write_obj = mktree_null_delimited_content + b"\0\0"
|
|
207
|
+
tree_hash = single_shot_command(mktree_popen, write_obj, True)
|
|
208
|
+
return tree_hash.strip()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def single_shot_command(long_running_popen: subprocess.Popen[bytes], write_obj: bytes, readline: bool) -> bytes:
|
|
212
|
+
"""
|
|
213
|
+
Make a git tree using ``mktree`` Popen and tree data to get a git tree.
|
|
214
|
+
|
|
215
|
+
Natively supports null-terminated tree entries or ``git mktree --batch -z`` mode.
|
|
216
|
+
|
|
217
|
+
:param long_running_popen: long-running subprocess.Popen.
|
|
218
|
+
:param write_obj: object to write to the stdin of the long-running process.
|
|
219
|
+
:param readline: stop at readline or read the whole stdout.
|
|
220
|
+
:returns: the created tree hash.
|
|
221
|
+
"""
|
|
222
|
+
_popen_stdin: IO[bytes] = cast(IO[bytes], long_running_popen.stdin) # stdin is assumed to be piped
|
|
223
|
+
_popen_stdout: IO[bytes] = cast(IO[bytes], long_running_popen.stdout) # stdout is assumed to be piped
|
|
224
|
+
_popen_stdin.write(write_obj)
|
|
225
|
+
_popen_stdin.flush()
|
|
226
|
+
_popen_out = _popen_stdout.readline() if readline else _popen_stdout.read()
|
|
227
|
+
return _popen_out
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def mktree_tree_data_make(mktree_popen: subprocess.Popen[bytes], mktree_entries: Iterable[bytes]) -> bytes:
|
|
231
|
+
"""
|
|
232
|
+
Make a git tree using ``mktree`` Popen and tree data to get a git tree.
|
|
233
|
+
|
|
234
|
+
Natively supports null-terminated tree entries or ``git mktree --batch -z`` mode.
|
|
235
|
+
|
|
236
|
+
:param mktree_popen: long-running ``mktree --batch`` subprocess.Popen.
|
|
237
|
+
:param mktree_entries: tree entries as required by ``git mktree --batch -z`` or output by ``git ls-tree -z``.
|
|
238
|
+
:returns: the created tree hash.
|
|
239
|
+
"""
|
|
240
|
+
return mktree_tree_make(mktree_popen, b"\0".join(mktree_entries))
|
|
241
|
+
# endregion
|
|
242
|
+
# endregion
|
|
243
|
+
|
|
244
|
+
# region commit
|
|
245
|
+
def cat_file_commit_content(cat_file_popen: subprocess.Popen[bytes], commit_hash: bytes) -> bytes:
|
|
246
|
+
"""
|
|
247
|
+
Get commit data as produced on stdout of a ``git cat-file --batch`` process for the querying of a particular
|
|
248
|
+
commit hash.
|
|
249
|
+
|
|
250
|
+
Spawning new ``git show`` processes can be slower and resource consuming.
|
|
251
|
+
|
|
252
|
+
Note: ``cat_file_popen`` must pipe its ``stdin`` and ``stdout`` and run in ``bytes`` mode.
|
|
253
|
+
|
|
254
|
+
:param cat_file_popen: long-running ``git cat-file --batch`` process in bytes mode and pipes its stdin and stdout.
|
|
255
|
+
:param commit_hash: commit hash to be read from cat-file.
|
|
256
|
+
:return: contents of blob hash in bytes.
|
|
257
|
+
"""
|
|
258
|
+
return cat_file_blob_content(cat_file_popen, commit_hash, b"commit")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def cat_file_commit_data(cat_file_popen: subprocess.Popen[bytes], commit_hash: bytes) -> RawCommitBytesObj:
|
|
262
|
+
"""
|
|
263
|
+
Get commit programmatic data as produced on stdout of a ``git cat-file --batch`` process for the querying
|
|
264
|
+
of a particular commit hash.
|
|
265
|
+
|
|
266
|
+
Spawning new ``git show`` processes can be slower and resource consuming.
|
|
267
|
+
|
|
268
|
+
Note: ``cat_file_popen`` must pipe its ``stdin`` and ``stdout`` and run in ``bytes`` mode.
|
|
269
|
+
|
|
270
|
+
:param cat_file_popen: long-running ``git cat-file --batch`` process in bytes mode and pipes its stdin and stdout.
|
|
271
|
+
:param commit_hash: commit hash to be read from cat-file.
|
|
272
|
+
:return: ``RawCommitBytesObj`` for programmatic use.
|
|
273
|
+
"""
|
|
274
|
+
commit_cat_file_content = cat_file_commit_content(cat_file_popen, commit_hash)
|
|
275
|
+
return parse_cat_file_commit_data(commit_hash, commit_cat_file_content)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def parse_cat_file_commit_data(commit_hash, commit_cat_file_content) -> RawCommitBytesObj:
|
|
279
|
+
"""
|
|
280
|
+
Parser for a commit contents as presented by ``git cat-file --batch`` query for a commit hash.
|
|
281
|
+
|
|
282
|
+
:param commit_hash: hash of the commit to get parsed ``cat-file`` data for.
|
|
283
|
+
:param commit_cat_file_content: the ``cat-file`` commit content.
|
|
284
|
+
:returns: parsed and programmatic ``RawCommitBytesObj``.
|
|
285
|
+
"""
|
|
286
|
+
r = parse_cat_file_commit_content(commit_cat_file_content)
|
|
287
|
+
author = GitRawActor.from_commit_bytes(r.author_val)
|
|
288
|
+
committer = GitRawActor.from_commit_bytes(r.committer_val)
|
|
289
|
+
signature = GitRawSignature(r.signature_val) if r.signature_val else None
|
|
290
|
+
return RawCommitBytesObj(commit_hash, r.tree_val, r.commit_parents_vals, author, committer, signature,
|
|
291
|
+
r.commit_message)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def parse_cat_file_commit_content(commit_cat_file_content) -> RawBytesValsOfCommit:
|
|
295
|
+
"""
|
|
296
|
+
Parser for a commit contents as presented by ``git cat-file --batch`` query for a commit hash.
|
|
297
|
+
|
|
298
|
+
Examples:
|
|
299
|
+
|
|
300
|
+
Simple commit:
|
|
301
|
+
|
|
302
|
+
>>> _simple_commit_content = b\"\"\"tree df1a4e54e5c3db0fb50b83dfab9750250b6aea1b
|
|
303
|
+
... parent b0086231746c37e198f4616f09cf697714f2033e
|
|
304
|
+
... author Suhas Krishna Srivastava <suhasCanDoSo@gmail.com> 1780213058 +0530
|
|
305
|
+
... committer Suhas Krishna Srivastava <suhasCanDoSo@gmail.com> 1780213152 +0530
|
|
306
|
+
...
|
|
307
|
+
... session: add doctests to `Actor.from_commit_bytes()`
|
|
308
|
+
... \"\"\"
|
|
309
|
+
>>> _parsed_simple_commit_content = parse_cat_file_commit_content(_simple_commit_content)
|
|
310
|
+
>>> assert _parsed_simple_commit_content.tree_val == b"df1a4e54e5c3db0fb50b83dfab9750250b6aea1b"
|
|
311
|
+
>>> assert _parsed_simple_commit_content.commit_parents_vals == [b"b0086231746c37e198f4616f09cf697714f2033e"]
|
|
312
|
+
>>> assert _parsed_simple_commit_content.author_val == b"Suhas Krishna Srivastava <suhasCanDoSo@gmail.com> 1780213058 +0530"
|
|
313
|
+
>>> assert _parsed_simple_commit_content.committer_val == b"Suhas Krishna Srivastava <suhasCanDoSo@gmail.com> 1780213152 +0530"
|
|
314
|
+
>>> assert _parsed_simple_commit_content.signature_val is None
|
|
315
|
+
>>> assert _parsed_simple_commit_content.commit_message == b"session: add doctests to `Actor.from_commit_bytes()`\\n"
|
|
316
|
+
|
|
317
|
+
Multiple parents:
|
|
318
|
+
|
|
319
|
+
>>> _multiple_parent_commit = b\"\"\"tree 9b708a0877812922f43db4ddbc87196754937033
|
|
320
|
+
... parent 3622f225ac320c9eece4c5319c89a00661cbcb39
|
|
321
|
+
... parent b6f3199b6866a0522c6fe4eaf07cf4515ac1d8fd
|
|
322
|
+
... parent deadbeefabcdefbeefdead1234566789beefeedd
|
|
323
|
+
... author Suhas Krishna Srivastava <suhasCanDoSo@gmail.com> 1779643218 +0530
|
|
324
|
+
... committer Suhas Krishna Srivastava <suhasCanDoSo@gmail.com> 1779643218 +0530
|
|
325
|
+
...
|
|
326
|
+
... Introduce `Popen`
|
|
327
|
+
...
|
|
328
|
+
... Merge branch 'feat/run-and-popen/main' into develop
|
|
329
|
+
...
|
|
330
|
+
... - Introduced `Popen` interface in runner.
|
|
331
|
+
... - Introduced `Popen` interface in UncheckedSubcmd.
|
|
332
|
+
... \"\"\"
|
|
333
|
+
>>> _parsed_multi_parent_commit = parse_cat_file_commit_content(_multiple_parent_commit)
|
|
334
|
+
>>> assert _parsed_multi_parent_commit.tree_val == b"9b708a0877812922f43db4ddbc87196754937033"
|
|
335
|
+
>>> assert _parsed_multi_parent_commit.commit_parents_vals == [b"3622f225ac320c9eece4c5319c89a00661cbcb39", b"b6f3199b6866a0522c6fe4eaf07cf4515ac1d8fd", b"deadbeefabcdefbeefdead1234566789beefeedd"]
|
|
336
|
+
>>> assert _parsed_multi_parent_commit.author_val == b"Suhas Krishna Srivastava <suhasCanDoSo@gmail.com> 1779643218 +0530"
|
|
337
|
+
>>> assert _parsed_multi_parent_commit.committer_val == b"Suhas Krishna Srivastava <suhasCanDoSo@gmail.com> 1779643218 +0530"
|
|
338
|
+
>>> assert _parsed_multi_parent_commit.signature_val is None
|
|
339
|
+
>>> assert _parsed_multi_parent_commit.commit_message == b"Introduce `Popen`\\n\\nMerge branch 'feat/run-and-popen/main' into develop\\n\\n- Introduced `Popen` interface in runner.\\n- Introduced `Popen` interface in UncheckedSubcmd.\\n"
|
|
340
|
+
|
|
341
|
+
GPG Signed commit:
|
|
342
|
+
|
|
343
|
+
>>> _gpg_signed_commit = b\"\"\"tree e26c61179203eea2b5c6a14dfff31621b74654c7
|
|
344
|
+
... parent d7d8d6f79017c5d574e3c4ac519c855b7cec33e2
|
|
345
|
+
... author Suhas Krishna Srivastava <suhasCanDoSo@gmail.com> 1779877779 +0530
|
|
346
|
+
... committer Suhas Krishna Srivastava <suhasCanDoSo@gmail.com> 1779877779 +0530
|
|
347
|
+
... gpgsig -----BEGIN PGP SIGNATURE-----
|
|
348
|
+
...
|
|
349
|
+
... iHUEABYKAB0WIQRBZelV8q2CQXy8Vg8vtl2qydHBMAUCahbHsAAKCRAvtl2qydHB
|
|
350
|
+
... MNrAAP9ZYoOfzSHJsnNsocCxqdf0K5Dh0mWbo9hUAYtsKlv/HQEAi8J/jYBqY9lz
|
|
351
|
+
... s6M7/A1YuKWMigDLTbMmjRg6uUgHOAM=
|
|
352
|
+
... =5rmu
|
|
353
|
+
... -----END PGP SIGNATURE-----
|
|
354
|
+
...
|
|
355
|
+
... Releasing `0.0.0.dev18`
|
|
356
|
+
...
|
|
357
|
+
... - `GitSession` improved.
|
|
358
|
+
... - `GitSession` reexported in `gitbolt`.
|
|
359
|
+
... - Popen no longer unbuffered.
|
|
360
|
+
... - Sessions by default are in `bytes` mode and pipe their `stdin` and `stdout`.
|
|
361
|
+
...
|
|
362
|
+
... - `GitSession` now returns `Self` on `__enter__()`. Glad caught early.
|
|
363
|
+
... - Utilities for git subprocess commands and Popen(s).
|
|
364
|
+
...
|
|
365
|
+
... Signed-off-by: Suhas Krishna Srivastava <suhas.srivastava@vaastav.tech>
|
|
366
|
+
... \"\"\"
|
|
367
|
+
>>> _parsed_gpg_sig_commit = parse_cat_file_commit_content(_gpg_signed_commit)
|
|
368
|
+
>>> assert _parsed_gpg_sig_commit.tree_val == b"e26c61179203eea2b5c6a14dfff31621b74654c7"
|
|
369
|
+
>>> assert _parsed_gpg_sig_commit.commit_parents_vals == [b"d7d8d6f79017c5d574e3c4ac519c855b7cec33e2"]
|
|
370
|
+
>>> assert _parsed_gpg_sig_commit.author_val == b"Suhas Krishna Srivastava <suhasCanDoSo@gmail.com> 1779877779 +0530"
|
|
371
|
+
>>> assert _parsed_gpg_sig_commit.committer_val == b"Suhas Krishna Srivastava <suhasCanDoSo@gmail.com> 1779877779 +0530"
|
|
372
|
+
>>> assert _parsed_gpg_sig_commit.signature_val == b"-----BEGIN PGP SIGNATURE-----\\n\\niHUEABYKAB0WIQRBZelV8q2CQXy8Vg8vtl2qydHBMAUCahbHsAAKCRAvtl2qydHB\\nMNrAAP9ZYoOfzSHJsnNsocCxqdf0K5Dh0mWbo9hUAYtsKlv/HQEAi8J/jYBqY9lz\\ns6M7/A1YuKWMigDLTbMmjRg6uUgHOAM=\\n=5rmu\\n-----END PGP SIGNATURE-----"
|
|
373
|
+
>>> assert _parsed_gpg_sig_commit.commit_message == b"Releasing `0.0.0.dev18`\\n\\n- `GitSession` improved.\\n- `GitSession` reexported in `gitbolt`.\\n- Popen no longer unbuffered.\\n- Sessions by default are in `bytes` mode and pipe their `stdin` and `stdout`.\\n\\n- `GitSession` now returns `Self` on `__enter__()`. Glad caught early.\\n- Utilities for git subprocess commands and Popen(s).\\n\\nSigned-off-by: Suhas Krishna Srivastava <suhas.srivastava@vaastav.tech>\\n"
|
|
374
|
+
|
|
375
|
+
No message commit:
|
|
376
|
+
|
|
377
|
+
>>> _no_msg_commit = b\"\"\"tree e26c61179203eea2b5c6a14dfff31621b74654c7
|
|
378
|
+
... parent d7d8d6f79017c5d574e3c4ac519c855b7cec33e2
|
|
379
|
+
... author Suhas Krishna Srivastava <suhasCanDoSo@gmail.com> 1779877779 +0530
|
|
380
|
+
... committer Suhas Krishna Srivastava <suhasCanDoSo@gmail.com> 1779877779 +0530
|
|
381
|
+
...
|
|
382
|
+
... \"\"\"
|
|
383
|
+
>>> _parsed_no_msg_commit = parse_cat_file_commit_content(_no_msg_commit)
|
|
384
|
+
>>> assert _parsed_no_msg_commit.tree_val == b"e26c61179203eea2b5c6a14dfff31621b74654c7"
|
|
385
|
+
>>> assert _parsed_no_msg_commit.commit_parents_vals == [b"d7d8d6f79017c5d574e3c4ac519c855b7cec33e2"]
|
|
386
|
+
>>> assert _parsed_no_msg_commit.author_val == b"Suhas Krishna Srivastava <suhasCanDoSo@gmail.com> 1779877779 +0530"
|
|
387
|
+
>>> assert _parsed_no_msg_commit.committer_val == b"Suhas Krishna Srivastava <suhasCanDoSo@gmail.com> 1779877779 +0530"
|
|
388
|
+
>>> assert _parsed_no_msg_commit.signature_val is None
|
|
389
|
+
>>> assert _parsed_no_msg_commit.commit_message == b""
|
|
390
|
+
|
|
391
|
+
No parent commit:
|
|
392
|
+
|
|
393
|
+
>>> _no_parent_commit = b\"\"\"tree 28f8877d1952f06b6e22c2d57522796dfa62e145
|
|
394
|
+
... author Suhas Krishna Srivastava <suhas.srivastava@vaastav.tech> 1747405354 +0530
|
|
395
|
+
... committer GitHub <noreply@github.com> 1747405354 +0530
|
|
396
|
+
... gpgsig -----BEGIN PGP SIGNATURE-----
|
|
397
|
+
...
|
|
398
|
+
... wsFcBAABCAAQBQJoJ0oqCRC1aQ7uu5UhlAAABbQQAKnq3HAThX2whGmo4+u5Czg/
|
|
399
|
+
... AybUlY396eoUd9JMoPF+E8+BVWe4ulVmbnWkPBW4iEUtqJzeNA9ULPNnMYMa1Dtu
|
|
400
|
+
... iu+IbMt3hh5FuLVtpA+92V9U+0RwpwYjX48dDeVfAWMz9QsI1z4SMUPZmyb7cV2J
|
|
401
|
+
... oAcStdGLPhZGa0RSRxFm+KuoZRCWSJ65a8HYpR0zI8sKvTZaFrS+ItxtvALkMBL7
|
|
402
|
+
... dsbdbTNtyPrdqYFhA7BNe7cisd5Avob01CMkV3aPwLFKtFdtS7WPLwwR0fXhOLZV
|
|
403
|
+
... DiYe7Tu+NrVcQJe1QLjkek4DnaN0ph4Xyi/K0CW4FJZ4vXIUI99Z9FP0QKiEX7bN
|
|
404
|
+
... XWyBchwwqFiE17PcSkLOLX0VUCIYdF0w6pj4AzNpSn8PY0jEg0FLbnfj+ZDfBhxl
|
|
405
|
+
... cUvh0obhnxdd4BafqzWSJPpzhCwx3NjkuNA1464BcerCTy7XkNxPtYyTDfLWD12x
|
|
406
|
+
... PVRKAqhm/z2yHyP93maRYY9R/Qvx7n5XJBFh+5w66e3Gvb+RaQQ8HcyJKbz+I/PJ
|
|
407
|
+
... tSRRAm1ROTnCLdWzjzimbOT4agKgwG53HqeaRLzqteu+lTr64LH5ZZZkAUd9v+C9
|
|
408
|
+
... cJb41OV3hHdP0+evTn471JU7YQlCYiQK+Ps04TsUX1MwRgoNq1+znk27PK7B1XUF
|
|
409
|
+
... 21sfF75JL4q7VsU/dpCU
|
|
410
|
+
... =0m4k
|
|
411
|
+
... -----END PGP SIGNATURE-----
|
|
412
|
+
...
|
|
413
|
+
...
|
|
414
|
+
... Initial commit\"\"\"
|
|
415
|
+
>>> _parsed_no_parent_commit = parse_cat_file_commit_content(_no_parent_commit)
|
|
416
|
+
>>> assert _parsed_no_parent_commit.tree_val == b"28f8877d1952f06b6e22c2d57522796dfa62e145"
|
|
417
|
+
>>> assert _parsed_no_parent_commit.commit_parents_vals == []
|
|
418
|
+
>>> assert _parsed_no_parent_commit.author_val == b"Suhas Krishna Srivastava <suhas.srivastava@vaastav.tech> 1747405354 +0530"
|
|
419
|
+
>>> assert _parsed_no_parent_commit.committer_val == b"GitHub <noreply@github.com> 1747405354 +0530"
|
|
420
|
+
>>> assert _parsed_no_parent_commit.commit_message == b"\\nInitial commit"
|
|
421
|
+
>>> assert _parsed_no_parent_commit.signature_val == b"-----BEGIN PGP SIGNATURE-----\\n\\nwsFcBAABCAAQBQJoJ0oqCRC1aQ7uu5UhlAAABbQQAKnq3HAThX2whGmo4+u5Czg/\\nAybUlY396eoUd9JMoPF+E8+BVWe4ulVmbnWkPBW4iEUtqJzeNA9ULPNnMYMa1Dtu\\niu+IbMt3hh5FuLVtpA+92V9U+0RwpwYjX48dDeVfAWMz9QsI1z4SMUPZmyb7cV2J\\noAcStdGLPhZGa0RSRxFm+KuoZRCWSJ65a8HYpR0zI8sKvTZaFrS+ItxtvALkMBL7\\ndsbdbTNtyPrdqYFhA7BNe7cisd5Avob01CMkV3aPwLFKtFdtS7WPLwwR0fXhOLZV\\nDiYe7Tu+NrVcQJe1QLjkek4DnaN0ph4Xyi/K0CW4FJZ4vXIUI99Z9FP0QKiEX7bN\\nXWyBchwwqFiE17PcSkLOLX0VUCIYdF0w6pj4AzNpSn8PY0jEg0FLbnfj+ZDfBhxl\\ncUvh0obhnxdd4BafqzWSJPpzhCwx3NjkuNA1464BcerCTy7XkNxPtYyTDfLWD12x\\nPVRKAqhm/z2yHyP93maRYY9R/Qvx7n5XJBFh+5w66e3Gvb+RaQQ8HcyJKbz+I/PJ\\ntSRRAm1ROTnCLdWzjzimbOT4agKgwG53HqeaRLzqteu+lTr64LH5ZZZkAUd9v+C9\\ncJb41OV3hHdP0+evTn471JU7YQlCYiQK+Ps04TsUX1MwRgoNq1+znk27PK7B1XUF\\n21sfF75JL4q7VsU/dpCU\\n=0m4k\\n-----END PGP SIGNATURE-----"
|
|
422
|
+
|
|
423
|
+
Note: strips commit message.
|
|
424
|
+
|
|
425
|
+
:param commit_cat_file_content: commit content as presented by ``git cat-file -p <commit-hash>``.
|
|
426
|
+
:returns: (tree-hash, commit-parents, author-info, committer-info, commit-signature, commit-message).
|
|
427
|
+
:raises GitExitingException: if required fields like ``tree``, ``author`` and ``committer`` are not found in the
|
|
428
|
+
commit contents.
|
|
429
|
+
"""
|
|
430
|
+
# TODO: Make sure commit message and signature new-lines match the original commits.
|
|
431
|
+
# This can be achieved by deliberately creating commits and matching their commit hashes.
|
|
432
|
+
commit_parents: list[bytes] = []
|
|
433
|
+
curr_bytes_ptr = 0
|
|
434
|
+
tree_found, tree_val, curr_bytes_ptr = query_bytes_range(commit_cat_file_content, curr_bytes_ptr, b"tree", b" ",
|
|
435
|
+
b"\n", False)
|
|
436
|
+
if not tree_val:
|
|
437
|
+
raise GitExitingException("No tree found on commit", exit_code=ERR_DATA_FORMAT_ERR) from KeyError("tree")
|
|
438
|
+
# region collect parent commit id(s)
|
|
439
|
+
while True:
|
|
440
|
+
parent_found, parent_val, curr_bytes_ptr = query_bytes_range(commit_cat_file_content, curr_bytes_ptr, b"parent",
|
|
441
|
+
b" ", b"\n", False)
|
|
442
|
+
if not parent_found or not parent_val:
|
|
443
|
+
break
|
|
444
|
+
commit_parents.append(parent_val)
|
|
445
|
+
# endregion
|
|
446
|
+
author_found, author_val, curr_bytes_ptr = query_bytes_range(commit_cat_file_content, curr_bytes_ptr, b"author",
|
|
447
|
+
b" ", b"\n", False)
|
|
448
|
+
if not author_val:
|
|
449
|
+
raise GitExitingException("No author data found in commit", exit_code=ERR_DATA_FORMAT_ERR) \
|
|
450
|
+
from KeyError("author")
|
|
451
|
+
committer_found, committer_val, curr_bytes_ptr = query_bytes_range(commit_cat_file_content, curr_bytes_ptr,
|
|
452
|
+
b"committer", b" ", b"\n", False)
|
|
453
|
+
if not committer_val:
|
|
454
|
+
raise GitExitingException("No committer data found in commit", exit_code=ERR_DATA_FORMAT_ERR) \
|
|
455
|
+
from KeyError("committer")
|
|
456
|
+
gpg_sig_found, gpg_sig_val, curr_bytes_ptr = query_bytes_range(commit_cat_file_content, curr_bytes_ptr, b"gpgsig",
|
|
457
|
+
b" -----BEGIN PGP SIGNATURE-----",
|
|
458
|
+
b"-----END PGP SIGNATURE-----\n", True)
|
|
459
|
+
gpg_sigval = sanitize_sig_bytes(gpg_sig_val) if gpg_sig_val else None
|
|
460
|
+
ssh_sig_found, ssh_sig_val, curr_bytes_ptr = query_bytes_range(commit_cat_file_content, curr_bytes_ptr, b"gpgsig",
|
|
461
|
+
b" -----BEGIN SSH SIGNATURE-----",
|
|
462
|
+
b"-----END SSH SIGNATURE-----\n", True)
|
|
463
|
+
ssh_sigval = sanitize_sig_bytes(ssh_sig_val) if ssh_sig_val else None
|
|
464
|
+
curr_bytes_ptr += 1 # include \n as commit message starts after that.
|
|
465
|
+
commit_message = commit_cat_file_content[curr_bytes_ptr:]
|
|
466
|
+
return RawBytesValsOfCommit(tree_val, commit_parents, author_val, committer_val, gpg_sigval or ssh_sigval,
|
|
467
|
+
commit_message, )
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def sanitize_sig_bytes(signature: bytes) -> bytes:
|
|
471
|
+
return b"\n".join(line.strip() for line in signature.splitlines())
|
|
472
|
+
|
|
473
|
+
def sanitize_gpg_signature(gpg_signature: bytes) -> GPGGitRawSignature:
|
|
474
|
+
return GPGGitRawSignature(sanitize_sig_bytes(gpg_signature))
|
|
475
|
+
|
|
476
|
+
def sanitize_ssh_signature(ssh_signature: bytes) -> SSHGitRawSignature:
|
|
477
|
+
return SSHGitRawSignature(sanitize_sig_bytes(ssh_signature))
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def query_bytes_range(bytes_content: bytes, curr_bytes_ptr: int, key_to_query: bytes,
|
|
481
|
+
val_begin: bytes, val_end: bytes, keep_ends: bool) -> tuple[bool, bytes | None, int]:
|
|
482
|
+
"""
|
|
483
|
+
Query the bytes content for value to a specific key. Is helpful to the caller either when key is found or when
|
|
484
|
+
not found by returning the next-pointer to look for.
|
|
485
|
+
|
|
486
|
+
:param bytes_content: contents to search keys into.
|
|
487
|
+
:param key_to_query: the key to query in the bytes content.
|
|
488
|
+
:param curr_bytes_ptr: integer index (or pointer) to start looking for the ``key_to_query``.
|
|
489
|
+
:param val_begin: the beginning bytes of the value.
|
|
490
|
+
:param val_end: the ending bytes of the value.
|
|
491
|
+
:param keep_ends: include the ``val_begin`` and ``val_end`` bytes in the returned and found value.
|
|
492
|
+
:returns: (found, value, pointer-after-value-ends) or (not-found, null, unchanged-passed-current-bytes-pointer)
|
|
493
|
+
"""
|
|
494
|
+
interest_bytes = bytes_content[curr_bytes_ptr:]
|
|
495
|
+
if not interest_bytes.startswith(key_to_query):
|
|
496
|
+
return False, None, curr_bytes_ptr
|
|
497
|
+
interest_bytes = interest_bytes[len(key_to_query):]
|
|
498
|
+
val_start_index = interest_bytes.find(val_begin) # including val start
|
|
499
|
+
val_end_index = interest_bytes.find(val_end) + len(val_end) # including val end
|
|
500
|
+
curr_bytes_ptr += len(key_to_query)+val_end_index
|
|
501
|
+
if not keep_ends:
|
|
502
|
+
val_start_index += len(val_begin) # excluding val start
|
|
503
|
+
val_end_index -= len(val_end) # excluding val end
|
|
504
|
+
value = interest_bytes[val_start_index: val_end_index]
|
|
505
|
+
return True, value, curr_bytes_ptr
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def query_line(key: bytes, line: bytes, kv_delim: bytes = b" ") -> tuple[bool, bytes]:
|
|
509
|
+
"""
|
|
510
|
+
Query a line from stream for a particular key and return the found value till the line ends.
|
|
511
|
+
|
|
512
|
+
:param key: key to query.
|
|
513
|
+
:param line: line to search key into.
|
|
514
|
+
:param kv_delim: delimiter between the key and the value.
|
|
515
|
+
:returns: (found, found-value) or (not-found, line)
|
|
516
|
+
"""
|
|
517
|
+
if line.startswith(key):
|
|
518
|
+
_, value = line.split(kv_delim, 1)
|
|
519
|
+
return True, value
|
|
520
|
+
else:
|
|
521
|
+
return False, line
|
|
522
|
+
# endregion
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
if __name__ == "__main__":
|
|
526
|
+
import gitbolt
|
|
527
|
+
import time
|
|
528
|
+
|
|
529
|
+
git = gitbolt.get_git_command()
|
|
530
|
+
# region GitSession approach
|
|
531
|
+
start = time.perf_counter()
|
|
532
|
+
with git.session(
|
|
533
|
+
cat_file1=["cat-file", "--batch"],
|
|
534
|
+
cat_file2=["cat-file", "--batch"],
|
|
535
|
+
mktree=["mktree", "--batch", "-z"]
|
|
536
|
+
) as ses:
|
|
537
|
+
for mode_, sha_, name_ in cat_file_tree_data(ses.commands.cat_file1, b"HEAD^{tree}", True):
|
|
538
|
+
print(mode_, sha_, name_)
|
|
539
|
+
print("+" * 40)
|
|
540
|
+
print(cat_file_blob_content(ses.commands.cat_file2, b"9854cb4d432a881f59d38582791cf2636e7819d9"))
|
|
541
|
+
|
|
542
|
+
print("*"*40)
|
|
543
|
+
tree_of_head: list[bytes] = []
|
|
544
|
+
for tree_line in cat_file_tree_content(ses.commands.cat_file1, b"HEAD^{tree}"):
|
|
545
|
+
print(tree_line.decode())
|
|
546
|
+
tree_of_head.append(tree_line)
|
|
547
|
+
tree_of_head_1: list[bytes] = []
|
|
548
|
+
for tree_line in cat_file_tree_content(ses.commands.cat_file1, b"HEAD~1^{tree}"):
|
|
549
|
+
tree_of_head_1.append(tree_line)
|
|
550
|
+
# make tree
|
|
551
|
+
print("#"*40)
|
|
552
|
+
_tree_hash = mktree_tree_make(ses.commands.mktree, b"\0".join(tree_of_head))
|
|
553
|
+
print("head tree hash: ", _tree_hash)
|
|
554
|
+
print("#"*40)
|
|
555
|
+
_tree_hash_1 = mktree_tree_make(ses.commands.mktree, b"\0".join(tree_of_head_1))
|
|
556
|
+
print("head~1 tree hash: ", _tree_hash_1)
|
|
557
|
+
|
|
558
|
+
end = time.perf_counter()
|
|
559
|
+
print(f"Session Elapsed time: {end - start:0.4f} seconds")
|
|
560
|
+
# endregion
|
|
561
|
+
|
|
562
|
+
print("="*40)
|
|
563
|
+
# region Per-process call approach
|
|
564
|
+
start = time.perf_counter()
|
|
565
|
+
head_tree_out = git.subcmd_unchecked.run(["cat-file", "-p", "HEAD^{tree}"], text=False).stdout.splitlines()
|
|
566
|
+
for head_tree in head_tree_out:
|
|
567
|
+
print(head_tree)
|
|
568
|
+
print("+" * 40)
|
|
569
|
+
print(git.subcmd_unchecked.run(["cat-file", "-p", "9854cb4d432a881f59d38582791cf2636e7819d9"], text=False).stdout)
|
|
570
|
+
end = time.perf_counter()
|
|
571
|
+
print(f"Subcmd Elapsed time: {end - start:0.4f} seconds")
|
|
572
|
+
|
|
573
|
+
with git.session(
|
|
574
|
+
cat_file=["cat-file", "--batch"],
|
|
575
|
+
for_each_ref_contains=["for-each-ref", "--stdin"],
|
|
576
|
+
rev_list_contains=["rev-list", "--stdin"],
|
|
577
|
+
) as ses:
|
|
578
|
+
print(cat_file_commit_data(ses.commands.cat_file, b"d7d8d6f79017c5d574e3c4ac519c855b7cec33e2"))
|
|
579
|
+
print(cat_file_commit_data(ses.commands.cat_file, b"HEAD"))
|
|
580
|
+
print(cat_file_commit_data(ses.commands.cat_file, b"a744fefd4eaf1a0139ae3c85f713a1a963563cc3"))
|
|
581
|
+
print(single_shot_command(ses.commands.rev_list_contains, b"develop\n\n", False))
|
|
582
|
+
# print(single_shot_command(ses.commands.rev_list_contains, b"develop\n\n", False))
|
|
583
|
+
# endregion
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitbolt
|
|
3
|
-
Version: 0.0.0.
|
|
3
|
+
Version: 0.0.0.dev20
|
|
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>
|
|
@@ -146,7 +146,7 @@ print(version_stdout)
|
|
|
146
146
|
|
|
147
147
|
#### 🧑💻 Modular at the programmatic level
|
|
148
148
|
|
|
149
|
-
Commands are designed to be passed around as objects. This makes them modular and thus users can opt to use only
|
|
149
|
+
Commands are designed to be passed around as objects. This makes them modular and thus users can opt to use only
|
|
150
150
|
particular commands.
|
|
151
151
|
|
|
152
152
|
```python
|
|
@@ -327,7 +327,7 @@ no_advice_reset_git = overridden_git.git_opts_override(no_advice=False)
|
|
|
327
327
|
|
|
328
328
|
At last, run unchecked commands in git.
|
|
329
329
|
|
|
330
|
-
Introduced in `0.0.0.dev4` to
|
|
330
|
+
Introduced in `0.0.0.dev4` to
|
|
331
331
|
- experiment.
|
|
332
332
|
- have consistent interfaced commands run until all subcommands are provided by the library.
|
|
333
333
|
|
|
@@ -377,32 +377,36 @@ Get long-running process sessions and communicate with them for batching and fas
|
|
|
377
377
|
|
|
378
378
|
```python
|
|
379
379
|
import gitbolt
|
|
380
|
-
from gitbolt.subprocess.utils.session import cat_file_blob_content, cat_file_tree_content
|
|
380
|
+
from gitbolt.subprocess.utils.session import cat_file_blob_content, cat_file_tree_data, cat_file_tree_content, \
|
|
381
|
+
mktree_tree_make
|
|
381
382
|
|
|
382
383
|
git = gitbolt.get_git_command()
|
|
383
384
|
with git.session(
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
385
|
+
cat_file1=["cat-file", "--batch"],
|
|
386
|
+
cat_file2=["cat-file", "--batch"],
|
|
387
|
+
cat_file3=lambda: git.subcmd_unchecked.popen(["cat-file", "--batch"]), # pass your own git subcmd Popen(s)
|
|
388
|
+
mktree=["mktree", "--batch", "-z"]
|
|
388
389
|
) as ses:
|
|
389
|
-
|
|
390
|
+
tree_data: list[bytes] = []
|
|
391
|
+
for mode_, sha_, name_ in cat_file_tree_data(ses.commands.cat_file2, b"HEAD^{tree}"):
|
|
390
392
|
print(mode_, sha_, name_)
|
|
393
|
+
for tree_entry in cat_file_tree_content(ses.commands.cat_file2, b"HEAD^{tree}"):
|
|
394
|
+
tree_data.append(tree_entry)
|
|
391
395
|
print("+" * 40)
|
|
392
396
|
print(cat_file_blob_content(ses.commands.cat_file3, b"9854cb4d432a881f59d38582791cf2636e7819d9"))
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
ses.commands.mktree.
|
|
396
|
-
|
|
397
|
+
print("~" * 40)
|
|
398
|
+
with ses: # is reentrant, if you want
|
|
399
|
+
tree_hash = mktree_tree_make(ses.commands.mktree, b"\0".join(tree_data)) # make tree
|
|
400
|
+
print("Obtained tree: ", tree_hash)
|
|
397
401
|
|
|
398
|
-
assert not ses.active
|
|
402
|
+
assert not ses.active # session no long active
|
|
399
403
|
```
|
|
400
404
|
|
|
401
405
|
#### 💻 Run commands received from CLI
|
|
402
406
|
|
|
403
407
|
Introduced in `0.0.0.dev11` is the ability to take commands from CLI and run it inside `gitbolt`.
|
|
404
408
|
|
|
405
|
-
While making a system it may be required to run cli commands as received from cli using gitbolt. An obvious example
|
|
409
|
+
While making a system it may be required to run cli commands as received from cli using gitbolt. An obvious example
|
|
406
410
|
would be to make a system that receives CLI commands and does certain modifications/additions inside `gitbolt` before
|
|
407
411
|
actually running them. An example:
|
|
408
412
|
|
|
@@ -1,134 +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
|
-
|
|
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_)
|
|
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
|
|
File without changes
|
|
File without changes
|