gitbolt 0.0.0.dev1__py3-none-any.whl

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.
@@ -0,0 +1,436 @@
1
+ #!/usr/bin/env python3
2
+ # coding=utf-8
3
+
4
+ """
5
+ Git command interfaces with default implementation using subprocess calls.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from abc import abstractmethod, ABC
11
+ from pathlib import Path
12
+ from typing import override, Protocol, Unpack, Self, overload, Literal
13
+
14
+ from vt.utils.commons.commons.core_py import is_unset, not_none_not_unset
15
+
16
+ from gitbolt import Git, Version, LsTree, GitSubCommand, HasGitUnderneath, Add
17
+ from gitbolt.git_subprocess.add import AddCLIArgsBuilder, IndividuallyOverridableACAB
18
+ from gitbolt.git_subprocess.ls_tree import (
19
+ LsTreeCLIArgsBuilder,
20
+ IndividuallyOverridableLTCAB,
21
+ )
22
+ from gitbolt.git_subprocess.runner import GitCommandRunner
23
+ from gitbolt.models import GitOpts, GitLsTreeOpts, GitAddOpts, GitEnvVars
24
+ from gitbolt.utils import merge_git_opts, merge_git_envs
25
+
26
+
27
+ class GitCommand(Git, ABC):
28
+ """
29
+ Runs git as a command.
30
+ """
31
+
32
+ def __init__(self, runner: GitCommandRunner):
33
+ """
34
+ :param runner: a ``GitCommandRunner`` which eventually runs the cli command in a subprocess.
35
+ """
36
+ self.runner: GitCommandRunner = runner
37
+ self._main_cmd_opts: GitOpts = {}
38
+ self._env_vars: GitEnvVars = {}
39
+
40
+ # region build_main_cmd_args
41
+ def build_main_cmd_args(self) -> list[str]:
42
+ """
43
+ Terminal operation to build and return CLI args for git main cli command.
44
+
45
+ For example, ``--no-pager --no-advice`` is the git main command in ``git --no-pager --no-advice log master -1``.
46
+
47
+ :return: CLI args for git main cli command.
48
+ """
49
+ return (
50
+ self._main_cmd_cap_c_args()
51
+ + self._main_cmd_small_c_args()
52
+ + self._main_cmd_config_env_args()
53
+ + self._main_cmd_exec_path_args()
54
+ + self._main_cmd_paginate_args()
55
+ + self._main_cmd_no_pager_args()
56
+ + self._main_cmd_git_dir_args()
57
+ + self._main_cmd_work_tree_args()
58
+ + self._main_cmd_namespace_args()
59
+ + self._main_cmd_bare_args()
60
+ + self._main_cmd_no_replace_objects_args()
61
+ + self._main_cmd_no_lazy_fetch_args()
62
+ + self._main_cmd_no_optional_locks_args()
63
+ + self._main_cmd_no_advice_args()
64
+ + self._main_cmd_literal_pathspecs_args()
65
+ + self._main_cmd_glob_pathspecs_args()
66
+ + self._main_cmd_noglob_pathspecs_args()
67
+ + self._main_cmd_icase_pathspecs_args()
68
+ + self._main_cmd_list_cmds_args()
69
+ + self._main_cmd_attr_source_args()
70
+ )
71
+
72
+ @override
73
+ def git_opts_override(self, **overrides: Unpack[GitOpts]) -> Self:
74
+ _git_cmd = self.clone()
75
+ _main_cmd_opts = merge_git_opts(overrides, self._main_cmd_opts)
76
+ _git_cmd._main_cmd_opts = _main_cmd_opts
77
+ return _git_cmd
78
+
79
+ def _main_cmd_cap_c_args(self) -> list[str]:
80
+ val = self._main_cmd_opts.get("C")
81
+ if not_none_not_unset(val):
82
+ return [item for path in val for item in ["-C", str(path)]]
83
+ return []
84
+
85
+ def _main_cmd_small_c_args(self) -> list[str]:
86
+ val = self._main_cmd_opts.get("c")
87
+ if not_none_not_unset(val):
88
+ args = []
89
+ for k, v in val.items():
90
+ if is_unset(v):
91
+ continue # explicitly skip unset keys
92
+ if v is True or v is None: # treat None as True
93
+ args += ["-c", k]
94
+ elif v is False:
95
+ args += ["-c", f"{k}="]
96
+ else:
97
+ args += ["-c", f"{k}={v}"]
98
+ return args
99
+ return []
100
+
101
+ def _main_cmd_config_env_args(self) -> list[str]:
102
+ val = self._main_cmd_opts.get("config_env")
103
+ if not_none_not_unset(val):
104
+ return [
105
+ item for k, v in val.items() for item in ["--config-env", f"{k}={v}"]
106
+ ]
107
+ return []
108
+
109
+ def _main_cmd_exec_path_args(self) -> list[str]:
110
+ val = self._main_cmd_opts.get("exec_path")
111
+ if not_none_not_unset(val):
112
+ return ["--exec-path", str(val)]
113
+ return []
114
+
115
+ def _main_cmd_paginate_args(self) -> list[str]:
116
+ val = self._main_cmd_opts.get("paginate")
117
+ if not_none_not_unset(val):
118
+ return ["--paginate"]
119
+ return []
120
+
121
+ def _main_cmd_no_pager_args(self) -> list[str]:
122
+ val = self._main_cmd_opts.get("no_pager")
123
+ if not_none_not_unset(val):
124
+ return ["--no-pager"]
125
+ return []
126
+
127
+ def _main_cmd_git_dir_args(self) -> list[str]:
128
+ val = self._main_cmd_opts.get("git_dir")
129
+ if not_none_not_unset(val):
130
+ return ["--git-dir", str(val)]
131
+ return []
132
+
133
+ def _main_cmd_work_tree_args(self) -> list[str]:
134
+ val = self._main_cmd_opts.get("work_tree")
135
+ if not_none_not_unset(val):
136
+ return ["--work-tree", str(val)]
137
+ return []
138
+
139
+ def _main_cmd_namespace_args(self) -> list[str]:
140
+ val = self._main_cmd_opts.get("namespace")
141
+ if not_none_not_unset(val):
142
+ return ["--namespace", val]
143
+ return []
144
+
145
+ def _main_cmd_bare_args(self) -> list[str]:
146
+ val = self._main_cmd_opts.get("bare")
147
+ if not_none_not_unset(val):
148
+ return ["--bare"]
149
+ return []
150
+
151
+ def _main_cmd_no_replace_objects_args(self) -> list[str]:
152
+ val = self._main_cmd_opts.get("no_replace_objects")
153
+ if not_none_not_unset(val):
154
+ return ["--no-replace-objects"]
155
+ return []
156
+
157
+ def _main_cmd_no_lazy_fetch_args(self) -> list[str]:
158
+ val = self._main_cmd_opts.get("no_lazy_fetch")
159
+ if not_none_not_unset(val):
160
+ return ["--no-lazy-fetch"]
161
+ return []
162
+
163
+ def _main_cmd_no_optional_locks_args(self) -> list[str]:
164
+ val = self._main_cmd_opts.get("no_optional_locks")
165
+ if not_none_not_unset(val):
166
+ return ["--no-optional-locks"]
167
+ return []
168
+
169
+ def _main_cmd_no_advice_args(self) -> list[str]:
170
+ val = self._main_cmd_opts.get("no_advice")
171
+ if not_none_not_unset(val):
172
+ return ["--no-advice"]
173
+ return []
174
+
175
+ def _main_cmd_literal_pathspecs_args(self) -> list[str]:
176
+ val = self._main_cmd_opts.get("literal_pathspecs")
177
+ if not_none_not_unset(val):
178
+ return ["--literal-pathspecs"]
179
+ return []
180
+
181
+ def _main_cmd_glob_pathspecs_args(self) -> list[str]:
182
+ val = self._main_cmd_opts.get("glob_pathspecs")
183
+ if not_none_not_unset(val):
184
+ return ["--glob-pathspecs"]
185
+ return []
186
+
187
+ def _main_cmd_noglob_pathspecs_args(self) -> list[str]:
188
+ val = self._main_cmd_opts.get("noglob_pathspecs")
189
+ if not_none_not_unset(val):
190
+ return ["--noglob-pathspecs"]
191
+ return []
192
+
193
+ def _main_cmd_icase_pathspecs_args(self) -> list[str]:
194
+ val = self._main_cmd_opts.get("icase_pathspecs")
195
+ if not_none_not_unset(val):
196
+ return ["--icase-pathspecs"]
197
+ return []
198
+
199
+ def _main_cmd_list_cmds_args(self) -> list[str]:
200
+ val = self._main_cmd_opts.get("list_cmds")
201
+ if not_none_not_unset(val):
202
+ return [item for cmd in val for item in ["--list-cmds", cmd]]
203
+ return []
204
+
205
+ def _main_cmd_attr_source_args(self) -> list[str]:
206
+ val = self._main_cmd_opts.get("attr_source")
207
+ if not_none_not_unset(val):
208
+ return ["--attr-source", val]
209
+ return []
210
+
211
+ # endregion
212
+
213
+ # region build_git_envs
214
+ def build_git_envs(self) -> dict[str, str]:
215
+ """
216
+ Terminal operation to build and return effective Git environment variables
217
+ from the merged ``GitEnvVars`` object.
218
+
219
+ Skips values that are ``Unset`` or ``None``-like using ``not_none_not_unset()``.
220
+ Converts ``Path`` and ``datetime`` instances to ``str``.
221
+
222
+ :return: A cleaned and normalized GitEnvVars dict suitable for use in subprocesses.
223
+ """
224
+ env: dict[str, str] = {}
225
+ for key, val in self._env_vars.items():
226
+ if not_none_not_unset(val):
227
+ env[key] = str(val)
228
+ return env
229
+
230
+ @override
231
+ def git_envs_override(self, **overrides: Unpack[GitEnvVars]) -> Self:
232
+ _git_cmd = self.clone()
233
+ _env_vars = merge_git_envs(overrides, self._env_vars)
234
+ _git_cmd._env_vars = _env_vars
235
+ return _git_cmd
236
+
237
+ # endregion
238
+
239
+ @override
240
+ @property
241
+ def html_path(self) -> Path:
242
+ html_path_str = "--html-path"
243
+ return self._get_path(html_path_str)
244
+
245
+ @override
246
+ @property
247
+ def info_path(self) -> Path:
248
+ info_path_str = "--info-path"
249
+ return self._get_path(info_path_str)
250
+
251
+ @override
252
+ @property
253
+ def man_path(self) -> Path:
254
+ man_path_str = "--man-path"
255
+ return self._get_path(man_path_str)
256
+
257
+ @override
258
+ @property
259
+ def exec_path(self) -> Path:
260
+ exec_path_str = "--exec-path"
261
+ return self._get_path(exec_path_str)
262
+
263
+ def _get_path(self, path_opt_str: str) -> Path:
264
+ main_opts = self.build_main_cmd_args()
265
+ main_opts.append(path_opt_str)
266
+ _path_str = self.runner.run_git_command(
267
+ main_opts, [], check=True, text=True, capture_output=True
268
+ ).stdout.strip()
269
+ return Path(_path_str)
270
+
271
+ @override
272
+ @property
273
+ @abstractmethod
274
+ def version_subcmd(self) -> VersionCommand: ...
275
+
276
+ @override
277
+ @property
278
+ @abstractmethod
279
+ def ls_tree_subcmd(self) -> LsTreeCommand: ...
280
+
281
+ @override
282
+ @property
283
+ @abstractmethod
284
+ def add_subcmd(self) -> AddCommand: ...
285
+
286
+
287
+ class GitSubcmdCommand(GitSubCommand, HasGitUnderneath["GitCommand"], Protocol):
288
+ """
289
+ A ``GitSubCommand`` that holds a reference to ``git`` and provides ``git_opts_override`` by default.
290
+ """
291
+
292
+ @override
293
+ def git_opts_override(self, **overrides: Unpack[GitOpts]) -> Self:
294
+ overridden_git = self.underlying_git.git_opts_override(**overrides)
295
+ self._set_underlying_git(overridden_git)
296
+ return self
297
+
298
+ @override
299
+ def git_envs_override(self, **overrides: Unpack[GitEnvVars]) -> Self:
300
+ overridden_git = self.underlying_git.git_envs_override(**overrides)
301
+ self._set_underlying_git(overridden_git)
302
+ return self
303
+
304
+ @abstractmethod
305
+ def _set_underlying_git(self, git: "GitCommand") -> None:
306
+ """
307
+ Protected. Designed to be overridden not called publicly.
308
+
309
+ Set the `_underlying_git` in the derived class.
310
+
311
+ :param git: git to override current class's `underlying_git` to.
312
+ """
313
+ ...
314
+
315
+
316
+ class VersionCommand(Version, GitSubcmdCommand, Protocol):
317
+ pass
318
+
319
+
320
+ class LsTreeCommand(LsTree, GitSubcmdCommand, Protocol):
321
+ """
322
+ A composable class for building arguments for the `git ls-tree` subcommand, which is run later in a subprocess.
323
+
324
+ Intended usage includes CLI tooling, scripting, or Git plumbing automation, especially in
325
+ contexts where it's useful to dynamically generate Git commands.
326
+ """
327
+
328
+ @override
329
+ def ls_tree(self, tree_ish: str, **ls_tree_opts: Unpack[GitLsTreeOpts]) -> str:
330
+ self.args_validator.validate(tree_ish, **ls_tree_opts)
331
+ sub_cmd_args = self.cli_args_builder.build(tree_ish, **ls_tree_opts)
332
+ main_cmd_args = self.underlying_git.build_main_cmd_args()
333
+
334
+ # Run the git command
335
+ result = self.underlying_git.runner.run_git_command(
336
+ main_cmd_args,
337
+ sub_cmd_args,
338
+ check=True,
339
+ text=True,
340
+ capture_output=True,
341
+ cwd=self.root_dir,
342
+ )
343
+
344
+ return result.stdout.strip()
345
+
346
+ @property
347
+ def cli_args_builder(self) -> LsTreeCLIArgsBuilder:
348
+ """
349
+ The builder assembles the subcommand CLI portion of the git command invocation, such as
350
+ in ``git --no-pager ls-tree -r HEAD``, where ``-r HEAD`` is the subcommand argument list.
351
+
352
+ :return: Builder the complete list of subcommand CLI arguments to be passed to ``git ls-tree`` subprocess.
353
+ """
354
+ return IndividuallyOverridableLTCAB()
355
+
356
+
357
+ class AddCommand(Add, GitSubcmdCommand, Protocol):
358
+ # TODO: check why PyCharm says that add() signature is incompatible with base class but mypy says okay.
359
+
360
+ @override
361
+ @overload
362
+ def add(
363
+ self, pathspec: str, *pathspecs: str, **add_opts: Unpack[GitAddOpts]
364
+ ) -> str: ...
365
+
366
+ @override
367
+ @overload
368
+ def add(
369
+ self,
370
+ *,
371
+ pathspec_from_file: Path,
372
+ pathspec_file_nul: bool = False,
373
+ **add_opts: Unpack[GitAddOpts],
374
+ ) -> str: ...
375
+
376
+ @override
377
+ @overload
378
+ def add(
379
+ self,
380
+ *,
381
+ pathspec_from_file: Literal["-"],
382
+ pathspec_stdin: str,
383
+ pathspec_file_nul: bool = False,
384
+ **add_opts: Unpack[GitAddOpts],
385
+ ) -> str: ...
386
+
387
+ @override
388
+ def add(
389
+ self,
390
+ pathspec: str | None = None,
391
+ *pathspecs: str,
392
+ pathspec_from_file: Path | Literal["-"] | None = None,
393
+ pathspec_stdin: str | None = None,
394
+ pathspec_file_nul: bool = False,
395
+ **add_opts: Unpack[GitAddOpts],
396
+ ) -> str:
397
+ self.args_validator.validate(
398
+ pathspec,
399
+ *pathspecs,
400
+ pathspec_from_file=pathspec_from_file,
401
+ pathspec_stdin=pathspec_stdin,
402
+ pathspec_file_nul=pathspec_file_nul,
403
+ **add_opts,
404
+ )
405
+ sub_cmd_args = self.cli_args_builder.build(
406
+ pathspec,
407
+ *pathspecs,
408
+ pathspec_from_file=pathspec_from_file,
409
+ pathspec_file_nul=pathspec_file_nul,
410
+ **add_opts,
411
+ )
412
+ main_cmd_args = self.underlying_git.build_main_cmd_args()
413
+
414
+ # Run the git command
415
+ result = self.underlying_git.runner.run_git_command(
416
+ main_cmd_args,
417
+ sub_cmd_args,
418
+ _input=pathspec_stdin,
419
+ check=True,
420
+ text=True,
421
+ capture_output=True,
422
+ cwd=self.root_dir,
423
+ )
424
+
425
+ return result.stdout.strip()
426
+
427
+ @property
428
+ def cli_args_builder(self) -> AddCLIArgsBuilder:
429
+ """
430
+ The builder assembles the subcommand CLI portion of the git command invocation, such as
431
+ in ``git --no-pager add --ignore-missing add-file.py``, where ``--ignore-missing add-file.py`` is the
432
+ subcommand argument list.
433
+
434
+ :return: Builder the complete list of subcommand CLI arguments to be passed to ``git add`` subprocess.
435
+ """
436
+ return IndividuallyOverridableACAB()
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env python3
2
+ # coding=utf-8
3
+
4
+ """
5
+ constants wrt of git commands using subprocess.
6
+ """
7
+
8
+ from typing import Final
9
+
10
+ GIT_CMD: Final[str] = "git"
11
+ VERSION_CMD: Final[str] = "version"
12
+ LS_TREE_CMD: Final[str] = "ls-tree"
13
+ ADD_CMD: Final[str] = "add"
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env python3
2
+ # coding=utf-8
3
+
4
+ """
5
+ Exceptions specific to git using subprocess.
6
+ """
7
+
8
+ from vt.utils.errors.error_specs.exceptions import VTCmdException
9
+
10
+ from gitbolt.exceptions import GitException
11
+
12
+
13
+ class GitCmdException(GitException, VTCmdException):
14
+ """
15
+ A ``GitException`` that is also a ``VTCmdException``.
16
+
17
+ Examples:
18
+
19
+ >>> from subprocess import CalledProcessError
20
+
21
+ * raise without message:
22
+
23
+ >>> raise GitCmdException(called_process_error=CalledProcessError(1, ['git', 'status'])) # always use `from` clause.
24
+ Traceback (most recent call last):
25
+ gitbolt.git_subprocess.exceptions.GitCmdException: CalledProcessError: Command '['git', 'status']' returned non-zero exit status 1.
26
+
27
+ * raise with a message:
28
+
29
+ >>> raise GitCmdException('Git failed', called_process_error=CalledProcessError(1, ['git', 'push'])) # always use `from` clause.
30
+ Traceback (most recent call last):
31
+ gitbolt.git_subprocess.exceptions.GitCmdException: CalledProcessError: Git failed
32
+
33
+ * raise with overridden exit code:
34
+
35
+ >>> raise GitCmdException('Git push failed', called_process_error=CalledProcessError(1, ['git', 'push']), exit_code=42) # always use `from` clause.
36
+ Traceback (most recent call last):
37
+ gitbolt.git_subprocess.exceptions.GitCmdException: CalledProcessError: Git push failed
38
+
39
+ * raise without message, override with stderr inside CalledProcessError:
40
+
41
+ >>> err = CalledProcessError(128, ['git', 'fetch'], stderr='fatal: not a git repository')
42
+ >>> raise GitCmdException(called_process_error=err) # always use `from` clause.
43
+ Traceback (most recent call last):
44
+ gitbolt.git_subprocess.exceptions.GitCmdException: CalledProcessError: Command '['git', 'fetch']' returned non-zero exit status 128.
45
+
46
+ * raise exception using `from` clause (chaining):
47
+
48
+ >>> try:
49
+ ... raise CalledProcessError(129, ['git', 'clone'], stderr='fatal: repo not found')
50
+ ... except CalledProcessError as e:
51
+ ... raise GitCmdException('Clone failed', called_process_error=e) from e
52
+ Traceback (most recent call last):
53
+ gitbolt.git_subprocess.exceptions.GitCmdException: CalledProcessError: Clone failed
54
+
55
+ * cause reflects original CalledProcessError when chained:
56
+
57
+ >>> try:
58
+ ... raise CalledProcessError(2, ['git', 'commit'])
59
+ ... except CalledProcessError as e:
60
+ ... try:
61
+ ... raise GitCmdException('Commit failed', called_process_error=e) from e
62
+ ... except GitCmdException as g:
63
+ ... isinstance(g.cause, CalledProcessError)
64
+ True
65
+
66
+ * cause falls back to `called_process_error` when not chained:
67
+
68
+ >>> e = CalledProcessError(3, ['git', 'diff'])
69
+ >>> g = GitCmdException('Diff fail', called_process_error=e)
70
+ >>> g.cause is g.called_process_error
71
+ True
72
+
73
+ * access exit code:
74
+
75
+ >>> e = CalledProcessError(100, ['git', 'log'])
76
+ >>> ex = GitCmdException('Failure', called_process_error=e)
77
+ >>> ex.exit_code
78
+ 100
79
+
80
+ * override exit code manually:
81
+
82
+ >>> GitCmdException('Overridden', called_process_error=e, exit_code=77).exit_code
83
+ 77
84
+
85
+ * access structured information:
86
+
87
+ >>> g = GitCmdException('Git structured', called_process_error=e)
88
+ >>> info = g.to_dict()
89
+ >>> info['type'], 'Git structured' in info['message']
90
+ ('GitCmdException', True)
91
+
92
+ * raise exception with extra metadata:
93
+
94
+ >>> e = CalledProcessError(4, ['git', 'tag'])
95
+ >>> x = GitCmdException('Failed tagging', called_process_error=e, context='tagging-op')
96
+ >>> x.kwargs['context']
97
+ 'tagging-op'
98
+
99
+ * demonstrate subclass relationship:
100
+
101
+ >>> isinstance(GitCmdException('x', called_process_error=e), VTCmdException)
102
+ True
103
+
104
+ >>> isinstance(GitCmdException('x', called_process_error=e), GitException)
105
+ True
106
+
107
+ ... rest examples mimic ``VTCmdException``.
108
+ """
109
+
110
+ pass
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env python3
2
+ # coding=utf-8
3
+
4
+ """
5
+ implementations of git commands using subprocess.
6
+ """