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.
Files changed (39) hide show
  1. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/PKG-INFO +19 -15
  2. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/README.md +18 -14
  3. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/pyproject.toml +1 -1
  4. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/__init__.py +1 -0
  5. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/base.py +20 -2
  6. gitbolt-0.0.0.dev20/src/gitbolt/subprocess/utils/models.py +97 -0
  7. gitbolt-0.0.0.dev20/src/gitbolt/subprocess/utils/session.py +583 -0
  8. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt.egg-info/PKG-INFO +19 -15
  9. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt.egg-info/SOURCES.txt +1 -0
  10. gitbolt-0.0.0.dev18/src/gitbolt/subprocess/utils/session.py +0 -134
  11. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/LICENSE +0 -0
  12. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/setup.cfg +0 -0
  13. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/__init__.py +0 -0
  14. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/_internal_init.py +0 -0
  15. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/add.py +0 -0
  16. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/base.py +0 -0
  17. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/constants.py +0 -0
  18. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/exceptions.py +0 -0
  19. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/ls_tree.py +0 -0
  20. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/models.py +0 -0
  21. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/py.typed +0 -0
  22. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/pytest_plugin.py +0 -0
  23. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/_internal_init.py +0 -0
  24. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/add.py +0 -0
  25. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/constants.py +0 -0
  26. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/exceptions.py +0 -0
  27. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/impl/__init__.py +0 -0
  28. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/impl/simple.py +0 -0
  29. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/ls_tree.py +0 -0
  30. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/runner/__init__.py +0 -0
  31. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/runner/base.py +0 -0
  32. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/runner/simple.py +0 -0
  33. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/utils/__init__.py +0 -0
  34. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/subprocess/utils/cmd.py +0 -0
  35. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt/utils.py +0 -0
  36. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt.egg-info/dependency_links.txt +0 -0
  37. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt.egg-info/entry_points.txt +0 -0
  38. {gitbolt-0.0.0.dev18 → gitbolt-0.0.0.dev20}/src/gitbolt.egg-info/requires.txt +0 -0
  39. {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.dev18
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
- 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"]
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
- for mode_, sha_, name_ in cat_file_tree_content(ses.commands.cat_file2, b"HEAD^{tree}"):
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
- 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
+ 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 # session no long 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
- 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"]
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
- for mode_, sha_, name_ in cat_file_tree_content(ses.commands.cat_file2, b"HEAD^{tree}"):
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
- 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
+ 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 # session no long 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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gitbolt"
3
- version = "0.0.0.dev18"
3
+ version = "0.0.0.dev20"
4
4
  description = "Fast, flexible and type-safe Git commands in Python."
5
5
  requires-python = ">=3.12"
6
6
  readme = "README.md"
@@ -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.dev18
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
- 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"]
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
- for mode_, sha_, name_ in cat_file_tree_content(ses.commands.cat_file2, b"HEAD^{tree}"):
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
- 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
+ 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 # session no long 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
 
@@ -32,4 +32,5 @@ src/gitbolt/subprocess/runner/base.py
32
32
  src/gitbolt/subprocess/runner/simple.py
33
33
  src/gitbolt/subprocess/utils/__init__.py
34
34
  src/gitbolt/subprocess/utils/cmd.py
35
+ src/gitbolt/subprocess/utils/models.py
35
36
  src/gitbolt/subprocess/utils/session.py
@@ -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