loopix-sdk 2.30.0__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.
- loopix/__init__.py +260 -0
- loopix/api/__init__.py +287 -0
- loopix/api/client/__init__.py +8 -0
- loopix/api/client/api/__init__.py +1 -0
- loopix/api/client/api/sandboxes/__init__.py +1 -0
- loopix/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +161 -0
- loopix/api/client/api/sandboxes/get_sandboxes.py +176 -0
- loopix/api/client/api/sandboxes/get_sandboxes_metrics.py +173 -0
- loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +163 -0
- loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +199 -0
- loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +212 -0
- loopix/api/client/api/sandboxes/get_v2_sandboxes.py +230 -0
- loopix/api/client/api/sandboxes/get_v_2_sandboxes_sandbox_id_logs.py +254 -0
- loopix/api/client/api/sandboxes/post_sandboxes.py +172 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py +193 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +187 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +181 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +189 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_snapshots.py +195 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +193 -0
- loopix/api/client/api/sandboxes/put_sandboxes_sandbox_id_network.py +199 -0
- loopix/api/client/api/snapshots/__init__.py +1 -0
- loopix/api/client/api/snapshots/get_snapshots.py +202 -0
- loopix/api/client/api/tags/__init__.py +1 -0
- loopix/api/client/api/tags/delete_templates_tags.py +174 -0
- loopix/api/client/api/tags/get_templates_template_id_tags.py +172 -0
- loopix/api/client/api/tags/post_templates_tags.py +176 -0
- loopix/api/client/api/templates/__init__.py +1 -0
- loopix/api/client/api/templates/delete_templates_template_id.py +157 -0
- loopix/api/client/api/templates/get_templates.py +172 -0
- loopix/api/client/api/templates/get_templates_aliases_alias.py +167 -0
- loopix/api/client/api/templates/get_templates_template_id.py +195 -0
- loopix/api/client/api/templates/get_templates_template_id_builds_build_id_logs.py +272 -0
- loopix/api/client/api/templates/get_templates_template_id_builds_build_id_status.py +232 -0
- loopix/api/client/api/templates/get_templates_template_id_files_hash.py +180 -0
- loopix/api/client/api/templates/patch_templates_template_id.py +183 -0
- loopix/api/client/api/templates/patch_v_2_templates_template_id.py +185 -0
- loopix/api/client/api/templates/post_templates.py +172 -0
- loopix/api/client/api/templates/post_templates_template_id.py +181 -0
- loopix/api/client/api/templates/post_templates_template_id_builds_build_id.py +170 -0
- loopix/api/client/api/templates/post_v2_templates.py +172 -0
- loopix/api/client/api/templates/post_v3_templates.py +176 -0
- loopix/api/client/api/templates/post_v_2_templates_template_id_builds_build_id.py +192 -0
- loopix/api/client/api/volumes/__init__.py +1 -0
- loopix/api/client/api/volumes/delete_volumes_volume_id.py +161 -0
- loopix/api/client/api/volumes/get_volumes.py +140 -0
- loopix/api/client/api/volumes/get_volumes_volume_id.py +163 -0
- loopix/api/client/api/volumes/post_volumes.py +172 -0
- loopix/api/client/client.py +286 -0
- loopix/api/client/errors.py +16 -0
- loopix/api/client/models/__init__.py +185 -0
- loopix/api/client/models/admin_build_cancel_result.py +67 -0
- loopix/api/client/models/admin_sandbox_kill_result.py +67 -0
- loopix/api/client/models/assign_template_tags_request.py +67 -0
- loopix/api/client/models/assigned_template_tags.py +68 -0
- loopix/api/client/models/aws_registry.py +85 -0
- loopix/api/client/models/aws_registry_type.py +8 -0
- loopix/api/client/models/build_log_entry.py +89 -0
- loopix/api/client/models/build_status_reason.py +95 -0
- loopix/api/client/models/connect_sandbox.py +59 -0
- loopix/api/client/models/created_access_token.py +100 -0
- loopix/api/client/models/created_team_api_key.py +166 -0
- loopix/api/client/models/delete_template_tags_request.py +67 -0
- loopix/api/client/models/disk_metrics.py +91 -0
- loopix/api/client/models/error.py +67 -0
- loopix/api/client/models/gcp_registry.py +69 -0
- loopix/api/client/models/gcp_registry_type.py +8 -0
- loopix/api/client/models/general_registry.py +77 -0
- loopix/api/client/models/general_registry_type.py +8 -0
- loopix/api/client/models/identifier_masking_details.py +83 -0
- loopix/api/client/models/listed_sandbox.py +179 -0
- loopix/api/client/models/log_level.py +11 -0
- loopix/api/client/models/logs_direction.py +9 -0
- loopix/api/client/models/logs_source.py +9 -0
- loopix/api/client/models/machine_info.py +83 -0
- loopix/api/client/models/max_team_metric.py +78 -0
- loopix/api/client/models/mcp_type_0.py +44 -0
- loopix/api/client/models/new_access_token.py +59 -0
- loopix/api/client/models/new_sandbox.py +224 -0
- loopix/api/client/models/new_team_api_key.py +59 -0
- loopix/api/client/models/new_volume.py +59 -0
- loopix/api/client/models/node.py +160 -0
- loopix/api/client/models/node_detail.py +160 -0
- loopix/api/client/models/node_metrics.py +122 -0
- loopix/api/client/models/node_status.py +12 -0
- loopix/api/client/models/node_status_change.py +82 -0
- loopix/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +59 -0
- loopix/api/client/models/post_sandboxes_sandbox_id_snapshots_body.py +60 -0
- loopix/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +59 -0
- loopix/api/client/models/resumed_sandbox.py +68 -0
- loopix/api/client/models/sandbox.py +145 -0
- loopix/api/client/models/sandbox_auto_resume_config.py +60 -0
- loopix/api/client/models/sandbox_detail.py +267 -0
- loopix/api/client/models/sandbox_lifecycle.py +70 -0
- loopix/api/client/models/sandbox_log.py +70 -0
- loopix/api/client/models/sandbox_log_entry.py +93 -0
- loopix/api/client/models/sandbox_log_entry_fields.py +44 -0
- loopix/api/client/models/sandbox_logs.py +91 -0
- loopix/api/client/models/sandbox_logs_v2_response.py +73 -0
- loopix/api/client/models/sandbox_metric.py +126 -0
- loopix/api/client/models/sandbox_network_config.py +118 -0
- loopix/api/client/models/sandbox_network_config_rules.py +72 -0
- loopix/api/client/models/sandbox_network_rule.py +74 -0
- loopix/api/client/models/sandbox_network_transform.py +79 -0
- loopix/api/client/models/sandbox_network_transform_headers.py +47 -0
- loopix/api/client/models/sandbox_network_update_config.py +114 -0
- loopix/api/client/models/sandbox_network_update_config_rules.py +71 -0
- loopix/api/client/models/sandbox_on_timeout.py +9 -0
- loopix/api/client/models/sandbox_pause_request.py +62 -0
- loopix/api/client/models/sandbox_state.py +9 -0
- loopix/api/client/models/sandbox_volume_mount.py +67 -0
- loopix/api/client/models/sandboxes_with_metrics.py +59 -0
- loopix/api/client/models/snapshot_info.py +70 -0
- loopix/api/client/models/team.py +83 -0
- loopix/api/client/models/team_api_key.py +158 -0
- loopix/api/client/models/team_metric.py +86 -0
- loopix/api/client/models/team_user.py +75 -0
- loopix/api/client/models/template.py +225 -0
- loopix/api/client/models/template_alias_response.py +67 -0
- loopix/api/client/models/template_build.py +139 -0
- loopix/api/client/models/template_build_file_upload.py +70 -0
- loopix/api/client/models/template_build_info.py +126 -0
- loopix/api/client/models/template_build_logs_response.py +73 -0
- loopix/api/client/models/template_build_request.py +115 -0
- loopix/api/client/models/template_build_request_v2.py +88 -0
- loopix/api/client/models/template_build_request_v3.py +107 -0
- loopix/api/client/models/template_build_start_v2.py +184 -0
- loopix/api/client/models/template_build_status.py +11 -0
- loopix/api/client/models/template_legacy.py +207 -0
- loopix/api/client/models/template_request_response_v3.py +99 -0
- loopix/api/client/models/template_step.py +91 -0
- loopix/api/client/models/template_tag.py +78 -0
- loopix/api/client/models/template_update_request.py +59 -0
- loopix/api/client/models/template_update_response.py +59 -0
- loopix/api/client/models/template_with_builds.py +156 -0
- loopix/api/client/models/update_team_api_key.py +59 -0
- loopix/api/client/models/volume.py +67 -0
- loopix/api/client/models/volume_and_token.py +75 -0
- loopix/api/client/models/volume_token.py +59 -0
- loopix/api/client/py.typed +1 -0
- loopix/api/client/types.py +54 -0
- loopix/api/client_async/__init__.py +74 -0
- loopix/api/client_sync/__init__.py +73 -0
- loopix/api/metadata.py +14 -0
- loopix/connection_config.py +309 -0
- loopix/envd/api.py +170 -0
- loopix/envd/filesystem/filesystem_connect.py +193 -0
- loopix/envd/filesystem/filesystem_pb2.py +80 -0
- loopix/envd/filesystem/filesystem_pb2.pyi +272 -0
- loopix/envd/process/process_connect.py +174 -0
- loopix/envd/process/process_pb2.py +96 -0
- loopix/envd/process/process_pb2.pyi +316 -0
- loopix/envd/rpc.py +139 -0
- loopix/envd/versions.py +11 -0
- loopix/exceptions.py +133 -0
- loopix/io_utils.py +57 -0
- loopix/paginator.py +52 -0
- loopix/py.typed +0 -0
- loopix/sandbox/_git/__init__.py +85 -0
- loopix/sandbox/_git/args.py +363 -0
- loopix/sandbox/_git/auth.py +132 -0
- loopix/sandbox/_git/config.py +32 -0
- loopix/sandbox/_git/parse.py +222 -0
- loopix/sandbox/_git/types.py +149 -0
- loopix/sandbox/commands/command_handle.py +69 -0
- loopix/sandbox/commands/main.py +39 -0
- loopix/sandbox/filesystem/filesystem.py +337 -0
- loopix/sandbox/filesystem/watch_handle.py +70 -0
- loopix/sandbox/main.py +227 -0
- loopix/sandbox/mcp.py +1949 -0
- loopix/sandbox/network.py +8 -0
- loopix/sandbox/sandbox_api.py +624 -0
- loopix/sandbox/signature.py +47 -0
- loopix/sandbox/utils.py +34 -0
- loopix/sandbox_async/commands/command.py +396 -0
- loopix/sandbox_async/commands/command_handle.py +298 -0
- loopix/sandbox_async/commands/pty.py +257 -0
- loopix/sandbox_async/filesystem/filesystem.py +720 -0
- loopix/sandbox_async/filesystem/watch_handle.py +97 -0
- loopix/sandbox_async/git.py +1100 -0
- loopix/sandbox_async/main.py +987 -0
- loopix/sandbox_async/paginator.py +140 -0
- loopix/sandbox_async/sandbox_api.py +504 -0
- loopix/sandbox_async/utils.py +7 -0
- loopix/sandbox_domains.py +5 -0
- loopix/sandbox_sync/commands/command.py +420 -0
- loopix/sandbox_sync/commands/command_handle.py +239 -0
- loopix/sandbox_sync/commands/pty.py +279 -0
- loopix/sandbox_sync/filesystem/filesystem.py +710 -0
- loopix/sandbox_sync/filesystem/watch_handle.py +102 -0
- loopix/sandbox_sync/git.py +1077 -0
- loopix/sandbox_sync/main.py +975 -0
- loopix/sandbox_sync/paginator.py +140 -0
- loopix/sandbox_sync/sandbox_api.py +491 -0
- loopix/template/consts.py +45 -0
- loopix/template/dockerfile_parser.py +286 -0
- loopix/template/logger.py +232 -0
- loopix/template/main.py +1368 -0
- loopix/template/readycmd.py +144 -0
- loopix/template/types.py +194 -0
- loopix/template/utils.py +426 -0
- loopix/template_async/build_api.py +419 -0
- loopix/template_async/main.py +528 -0
- loopix/template_sync/build_api.py +409 -0
- loopix/template_sync/main.py +529 -0
- loopix/volume/client/__init__.py +8 -0
- loopix/volume/client/api/__init__.py +1 -0
- loopix/volume/client/api/volumes/__init__.py +1 -0
- loopix/volume/client/api/volumes/delete_volumecontent_volume_id_path.py +174 -0
- loopix/volume/client/api/volumes/get_volumecontent_volume_id_dir.py +204 -0
- loopix/volume/client/api/volumes/get_volumecontent_volume_id_file.py +179 -0
- loopix/volume/client/api/volumes/get_volumecontent_volume_id_path.py +176 -0
- loopix/volume/client/api/volumes/patch_volumecontent_volume_id_path.py +203 -0
- loopix/volume/client/api/volumes/post_volumecontent_volume_id_dir.py +239 -0
- loopix/volume/client/api/volumes/put_volumecontent_volume_id_file.py +259 -0
- loopix/volume/client/client.py +286 -0
- loopix/volume/client/errors.py +16 -0
- loopix/volume/client/models/__init__.py +13 -0
- loopix/volume/client/models/error.py +67 -0
- loopix/volume/client/models/patch_volumecontent_volume_id_path_body.py +77 -0
- loopix/volume/client/models/volume_entry_stat.py +145 -0
- loopix/volume/client/models/volume_entry_stat_type.py +11 -0
- loopix/volume/client/py.typed +1 -0
- loopix/volume/client/types.py +54 -0
- loopix/volume/client_async/__init__.py +88 -0
- loopix/volume/client_sync/__init__.py +80 -0
- loopix/volume/connection_config.py +145 -0
- loopix/volume/types.py +62 -0
- loopix/volume/utils.py +52 -0
- loopix/volume/volume_async.py +639 -0
- loopix/volume/volume_sync.py +639 -0
- loopix_connect/__init__.py +1 -0
- loopix_connect/client.py +534 -0
- loopix_connect/py.typed +0 -0
- loopix_sdk-2.30.0.dist-info/METADATA +98 -0
- loopix_sdk-2.30.0.dist-info/RECORD +238 -0
- loopix_sdk-2.30.0.dist-info/WHEEL +4 -0
- loopix_sdk-2.30.0.dist-info/licenses/LICENSE +9 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
from loopix.exceptions import InvalidArgumentException
|
|
4
|
+
from loopix.sandbox._git.auth import strip_credentials, with_credentials
|
|
5
|
+
from loopix.sandbox._git.parse import derive_repo_dir_from_url
|
|
6
|
+
from loopix.sandbox._git.types import ClonePlan
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def shell_escape(value: str) -> str:
|
|
10
|
+
"""
|
|
11
|
+
Escape a string for safe use in a shell command.
|
|
12
|
+
|
|
13
|
+
:param value: Value to escape
|
|
14
|
+
:return: Shell-escaped string
|
|
15
|
+
"""
|
|
16
|
+
return "'" + value.replace("'", "'\"'\"'") + "'"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_git_command(args: List[str], repo_path: Optional[str] = None) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Build a shell-safe git command string.
|
|
22
|
+
|
|
23
|
+
:param args: Git command arguments
|
|
24
|
+
:param repo_path: Repository path for `git -C`, if provided
|
|
25
|
+
:return: Shell-safe git command
|
|
26
|
+
"""
|
|
27
|
+
parts = ["git"]
|
|
28
|
+
if repo_path:
|
|
29
|
+
parts.extend(["-C", repo_path])
|
|
30
|
+
parts.extend(args)
|
|
31
|
+
return " ".join(shell_escape(part) for part in parts)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def build_push_args(
|
|
35
|
+
remote_name: Optional[str],
|
|
36
|
+
*,
|
|
37
|
+
remote: Optional[str],
|
|
38
|
+
branch: Optional[str],
|
|
39
|
+
set_upstream: bool,
|
|
40
|
+
) -> List[str]:
|
|
41
|
+
"""
|
|
42
|
+
Build arguments for a git push command.
|
|
43
|
+
|
|
44
|
+
:param remote_name: Resolved remote name, if any
|
|
45
|
+
:param remote: Remote name override
|
|
46
|
+
:param branch: Branch name to push
|
|
47
|
+
:param set_upstream: Whether to set upstream tracking
|
|
48
|
+
:return: List of git push arguments
|
|
49
|
+
"""
|
|
50
|
+
args = ["push"]
|
|
51
|
+
target_remote = remote_name or remote
|
|
52
|
+
if set_upstream and target_remote:
|
|
53
|
+
args.append("--set-upstream")
|
|
54
|
+
if target_remote:
|
|
55
|
+
args.append(target_remote)
|
|
56
|
+
if branch:
|
|
57
|
+
args.append(branch)
|
|
58
|
+
return args
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def build_pull_args(
|
|
62
|
+
remote: Optional[str],
|
|
63
|
+
branch: Optional[str],
|
|
64
|
+
remote_name: Optional[str] = None,
|
|
65
|
+
) -> List[str]:
|
|
66
|
+
"""
|
|
67
|
+
Build arguments for a git pull command.
|
|
68
|
+
|
|
69
|
+
:param remote: Remote name override
|
|
70
|
+
:param branch: Branch name to pull
|
|
71
|
+
:param remote_name: Resolved remote name, if any
|
|
72
|
+
:return: List of git pull arguments
|
|
73
|
+
"""
|
|
74
|
+
args = ["pull"]
|
|
75
|
+
target_remote = remote_name or remote
|
|
76
|
+
if target_remote:
|
|
77
|
+
args.append(target_remote)
|
|
78
|
+
if branch:
|
|
79
|
+
args.append(branch)
|
|
80
|
+
return args
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def build_remote_add_args(name: str, url: str, fetch: bool) -> List[str]:
|
|
84
|
+
"""
|
|
85
|
+
Build arguments for a git remote add command.
|
|
86
|
+
|
|
87
|
+
:param name: Remote name
|
|
88
|
+
:param url: Remote URL
|
|
89
|
+
:param fetch: Whether to fetch after adding the remote
|
|
90
|
+
:return: List of git remote add arguments
|
|
91
|
+
"""
|
|
92
|
+
if not name or not url:
|
|
93
|
+
raise InvalidArgumentException(
|
|
94
|
+
"Both remote name and URL are required to add a git remote."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
args = ["remote", "add"]
|
|
98
|
+
if fetch:
|
|
99
|
+
args.append("-f")
|
|
100
|
+
args.extend([name, url])
|
|
101
|
+
return args
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def build_remote_add_shell_command(
|
|
105
|
+
args: List[str],
|
|
106
|
+
path: str,
|
|
107
|
+
name: str,
|
|
108
|
+
url: str,
|
|
109
|
+
fetch: bool,
|
|
110
|
+
) -> str:
|
|
111
|
+
"""
|
|
112
|
+
Build a shell command that adds or updates a remote and optionally fetches.
|
|
113
|
+
|
|
114
|
+
:param args: Base git remote add args
|
|
115
|
+
:param path: Repository path
|
|
116
|
+
:param name: Remote name
|
|
117
|
+
:param url: Remote URL
|
|
118
|
+
:param fetch: Whether to fetch after adding the remote
|
|
119
|
+
:return: Shell command string
|
|
120
|
+
"""
|
|
121
|
+
add_cmd = build_git_command(args, path)
|
|
122
|
+
set_url_cmd = build_git_command(build_remote_set_url_args(name, url), path)
|
|
123
|
+
cmd = f"{add_cmd} || {set_url_cmd}"
|
|
124
|
+
if fetch:
|
|
125
|
+
fetch_cmd = build_git_command(["fetch", name], path)
|
|
126
|
+
cmd = f"({cmd}) && {fetch_cmd}"
|
|
127
|
+
return cmd
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def build_remote_get_url_args(name: str) -> List[str]:
|
|
131
|
+
"""
|
|
132
|
+
Build arguments for a git remote get-url command.
|
|
133
|
+
"""
|
|
134
|
+
return ["remote", "get-url", name]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def build_remote_set_url_args(name: str, url: str) -> List[str]:
|
|
138
|
+
"""
|
|
139
|
+
Build arguments for a git remote set-url command.
|
|
140
|
+
"""
|
|
141
|
+
return ["remote", "set-url", name, url]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def build_remote_get_command(path: str, name: str) -> str:
|
|
145
|
+
"""
|
|
146
|
+
Build a shell command that returns the remote URL or empty output.
|
|
147
|
+
|
|
148
|
+
:param path: Repository path
|
|
149
|
+
:param name: Remote name
|
|
150
|
+
:return: Shell command string
|
|
151
|
+
"""
|
|
152
|
+
if not name:
|
|
153
|
+
raise InvalidArgumentException("Remote name is required.")
|
|
154
|
+
|
|
155
|
+
return f"{build_git_command(build_remote_get_url_args(name), path)} || true"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def build_credential_approve_command(
|
|
159
|
+
username: str,
|
|
160
|
+
password: str,
|
|
161
|
+
host: str,
|
|
162
|
+
protocol: str,
|
|
163
|
+
) -> str:
|
|
164
|
+
"""
|
|
165
|
+
Build a git credential approve command for the given credentials.
|
|
166
|
+
"""
|
|
167
|
+
target_host = host.strip() or "github.com"
|
|
168
|
+
target_protocol = protocol.strip() or "https"
|
|
169
|
+
credential_input = "\n".join(
|
|
170
|
+
[
|
|
171
|
+
f"protocol={target_protocol}",
|
|
172
|
+
f"host={target_host}",
|
|
173
|
+
f"username={username}",
|
|
174
|
+
f"password={password}",
|
|
175
|
+
"",
|
|
176
|
+
"",
|
|
177
|
+
]
|
|
178
|
+
)
|
|
179
|
+
return (
|
|
180
|
+
f"printf %s {shell_escape(credential_input)} | "
|
|
181
|
+
f"{build_git_command(['credential', 'approve'])}"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def build_has_upstream_args() -> List[str]:
|
|
186
|
+
"""
|
|
187
|
+
Build arguments for a git upstream check command.
|
|
188
|
+
"""
|
|
189
|
+
return ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def build_status_args() -> List[str]:
|
|
193
|
+
"""
|
|
194
|
+
Build arguments for a git status command.
|
|
195
|
+
"""
|
|
196
|
+
return ["status", "--porcelain=1", "-b"]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def build_branches_args() -> List[str]:
|
|
200
|
+
"""
|
|
201
|
+
Build arguments for a git branch listing command.
|
|
202
|
+
"""
|
|
203
|
+
return ["branch", "--format=%(refname:short)\t%(HEAD)"]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def build_create_branch_args(branch: str) -> List[str]:
|
|
207
|
+
"""
|
|
208
|
+
Build arguments for a git checkout -b command.
|
|
209
|
+
"""
|
|
210
|
+
return ["checkout", "-b", branch]
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def build_checkout_branch_args(branch: str) -> List[str]:
|
|
214
|
+
"""
|
|
215
|
+
Build arguments for a git checkout command.
|
|
216
|
+
"""
|
|
217
|
+
return ["checkout", branch]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def build_delete_branch_args(branch: str, force: bool) -> List[str]:
|
|
221
|
+
"""
|
|
222
|
+
Build arguments for a git branch delete command.
|
|
223
|
+
"""
|
|
224
|
+
return ["branch", "-D" if force else "-d", branch]
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def build_add_args(files: Optional[List[str]], all: bool) -> List[str]:
|
|
228
|
+
"""
|
|
229
|
+
Build arguments for a git add command.
|
|
230
|
+
"""
|
|
231
|
+
args = ["add"]
|
|
232
|
+
if not files:
|
|
233
|
+
args.append("-A" if all else ".")
|
|
234
|
+
else:
|
|
235
|
+
args.append("--")
|
|
236
|
+
args.extend(files)
|
|
237
|
+
return args
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def build_commit_args(
|
|
241
|
+
message: str,
|
|
242
|
+
author_name: Optional[str],
|
|
243
|
+
author_email: Optional[str],
|
|
244
|
+
allow_empty: bool,
|
|
245
|
+
) -> List[str]:
|
|
246
|
+
"""
|
|
247
|
+
Build arguments for a git commit command.
|
|
248
|
+
"""
|
|
249
|
+
args = ["commit", "-m", message]
|
|
250
|
+
if allow_empty:
|
|
251
|
+
args.append("--allow-empty")
|
|
252
|
+
author_args: List[str] = []
|
|
253
|
+
if author_name:
|
|
254
|
+
author_args.extend(["-c", f"user.name={author_name}"])
|
|
255
|
+
if author_email:
|
|
256
|
+
author_args.extend(["-c", f"user.email={author_email}"])
|
|
257
|
+
if author_args:
|
|
258
|
+
args = author_args + args
|
|
259
|
+
return args
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def build_reset_args(
|
|
263
|
+
mode: Optional[str],
|
|
264
|
+
target: Optional[str],
|
|
265
|
+
paths: Optional[List[str]],
|
|
266
|
+
) -> List[str]:
|
|
267
|
+
"""
|
|
268
|
+
Build arguments for a git reset command.
|
|
269
|
+
"""
|
|
270
|
+
allowed_modes = ["soft", "mixed", "hard", "merge", "keep"]
|
|
271
|
+
if mode and mode not in allowed_modes:
|
|
272
|
+
raise InvalidArgumentException(
|
|
273
|
+
f"Reset mode must be one of {', '.join(allowed_modes)}."
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
args = ["reset"]
|
|
277
|
+
if mode:
|
|
278
|
+
args.append(f"--{mode}")
|
|
279
|
+
if target:
|
|
280
|
+
args.append(target)
|
|
281
|
+
if paths:
|
|
282
|
+
args.append("--")
|
|
283
|
+
args.extend(paths)
|
|
284
|
+
return args
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def build_restore_args(
|
|
288
|
+
paths: List[str],
|
|
289
|
+
staged: Optional[bool],
|
|
290
|
+
worktree: Optional[bool],
|
|
291
|
+
source: Optional[str],
|
|
292
|
+
) -> List[str]:
|
|
293
|
+
"""
|
|
294
|
+
Build arguments for a git restore command.
|
|
295
|
+
"""
|
|
296
|
+
if not paths:
|
|
297
|
+
raise InvalidArgumentException("At least one path is required.")
|
|
298
|
+
|
|
299
|
+
resolved_staged = staged
|
|
300
|
+
resolved_worktree = worktree
|
|
301
|
+
if staged is None and worktree is None:
|
|
302
|
+
resolved_worktree = True
|
|
303
|
+
elif staged is True and worktree is None:
|
|
304
|
+
resolved_worktree = False
|
|
305
|
+
elif staged is None and worktree is not None:
|
|
306
|
+
resolved_staged = False
|
|
307
|
+
|
|
308
|
+
if resolved_staged is False and resolved_worktree is False:
|
|
309
|
+
raise InvalidArgumentException(
|
|
310
|
+
"At least one of staged or worktree must be true."
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
args = ["restore"]
|
|
314
|
+
if resolved_worktree:
|
|
315
|
+
args.append("--worktree")
|
|
316
|
+
if resolved_staged:
|
|
317
|
+
args.append("--staged")
|
|
318
|
+
if source:
|
|
319
|
+
args.extend(["--source", source])
|
|
320
|
+
args.append("--")
|
|
321
|
+
args.extend(paths)
|
|
322
|
+
return args
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def build_clone_plan(
|
|
326
|
+
url: str,
|
|
327
|
+
path: Optional[str],
|
|
328
|
+
branch: Optional[str],
|
|
329
|
+
depth: Optional[int],
|
|
330
|
+
auth_username: Optional[str],
|
|
331
|
+
auth_password: Optional[str],
|
|
332
|
+
dangerously_store_credentials: bool,
|
|
333
|
+
) -> ClonePlan:
|
|
334
|
+
"""
|
|
335
|
+
Build clone arguments and metadata for post-clone credential stripping.
|
|
336
|
+
"""
|
|
337
|
+
clone_url = (
|
|
338
|
+
with_credentials(url, auth_username, auth_password)
|
|
339
|
+
if auth_username and auth_password
|
|
340
|
+
else url
|
|
341
|
+
)
|
|
342
|
+
sanitized_url = strip_credentials(clone_url)
|
|
343
|
+
should_strip = not dangerously_store_credentials and sanitized_url != clone_url
|
|
344
|
+
repo_path = path if not should_strip else path or derive_repo_dir_from_url(url)
|
|
345
|
+
if should_strip and not repo_path:
|
|
346
|
+
raise InvalidArgumentException(
|
|
347
|
+
"A destination path is required when using credentials without storing them."
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
args = ["clone", clone_url]
|
|
351
|
+
if branch:
|
|
352
|
+
args.extend(["--branch", branch, "--single-branch"])
|
|
353
|
+
if depth:
|
|
354
|
+
args.extend(["--depth", str(depth)])
|
|
355
|
+
if path:
|
|
356
|
+
args.append(path)
|
|
357
|
+
|
|
358
|
+
return ClonePlan(
|
|
359
|
+
args=args,
|
|
360
|
+
repo_path=repo_path,
|
|
361
|
+
sanitized_url=sanitized_url if should_strip else None,
|
|
362
|
+
should_strip=should_strip,
|
|
363
|
+
)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from urllib.parse import urlparse, urlunparse
|
|
3
|
+
|
|
4
|
+
from loopix.exceptions import InvalidArgumentException
|
|
5
|
+
from loopix.sandbox.commands.command_handle import CommandExitException
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def with_credentials(url: str, username: Optional[str], password: Optional[str]) -> str:
|
|
9
|
+
"""
|
|
10
|
+
Add HTTP(S) credentials to a Git URL.
|
|
11
|
+
|
|
12
|
+
:param url: Git repository URL
|
|
13
|
+
:param username: Username for HTTP(S) authentication
|
|
14
|
+
:param password: Password or token for HTTP(S) authentication
|
|
15
|
+
:return: URL with embedded credentials
|
|
16
|
+
"""
|
|
17
|
+
if not username and not password:
|
|
18
|
+
return url
|
|
19
|
+
if not username or not password:
|
|
20
|
+
raise InvalidArgumentException(
|
|
21
|
+
"Both username and password are required when using Git credentials."
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
parsed = urlparse(url)
|
|
25
|
+
if parsed.scheme not in ("http", "https"):
|
|
26
|
+
raise InvalidArgumentException(
|
|
27
|
+
"Only http(s) Git URLs support username/password credentials."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
netloc = f"{username}:{password}@{parsed.netloc}"
|
|
31
|
+
return urlunparse(parsed._replace(netloc=netloc))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def strip_credentials(url: str) -> str:
|
|
35
|
+
"""
|
|
36
|
+
Strip HTTP(S) credentials from a Git URL.
|
|
37
|
+
|
|
38
|
+
:param url: Git repository URL
|
|
39
|
+
:return: URL without embedded credentials
|
|
40
|
+
"""
|
|
41
|
+
parsed = urlparse(url)
|
|
42
|
+
if parsed.scheme not in ("http", "https"):
|
|
43
|
+
return url
|
|
44
|
+
if not parsed.username and not parsed.password:
|
|
45
|
+
return url
|
|
46
|
+
|
|
47
|
+
host = parsed.hostname or ""
|
|
48
|
+
if parsed.port:
|
|
49
|
+
host = f"{host}:{parsed.port}"
|
|
50
|
+
|
|
51
|
+
return urlunparse(parsed._replace(netloc=host))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def is_auth_failure(err: Exception) -> bool:
|
|
55
|
+
"""
|
|
56
|
+
Check whether a git command failed due to authentication issues.
|
|
57
|
+
|
|
58
|
+
:param err: Exception raised by a git command
|
|
59
|
+
:return: True when the error matches common authentication failures
|
|
60
|
+
"""
|
|
61
|
+
if not isinstance(err, CommandExitException):
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
message = f"{err.stderr}\n{err.stdout}".lower()
|
|
65
|
+
auth_snippets = [
|
|
66
|
+
"authentication failed",
|
|
67
|
+
"terminal prompts disabled",
|
|
68
|
+
"could not read username",
|
|
69
|
+
"invalid username or password",
|
|
70
|
+
"access denied",
|
|
71
|
+
"permission denied",
|
|
72
|
+
"not authorized",
|
|
73
|
+
]
|
|
74
|
+
return any(snippet in message for snippet in auth_snippets)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def is_missing_upstream(err: Exception) -> bool:
|
|
78
|
+
"""
|
|
79
|
+
Check whether a git command failed due to missing upstream tracking.
|
|
80
|
+
|
|
81
|
+
:param err: Exception raised by a git command
|
|
82
|
+
:return: True when the error matches common upstream failures
|
|
83
|
+
"""
|
|
84
|
+
if not isinstance(err, CommandExitException):
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
message = f"{err.stderr}\n{err.stdout}".lower()
|
|
88
|
+
upstream_snippets = [
|
|
89
|
+
"has no upstream branch",
|
|
90
|
+
"no upstream branch",
|
|
91
|
+
"no upstream configured",
|
|
92
|
+
"no tracking information for the current branch",
|
|
93
|
+
"no tracking information",
|
|
94
|
+
"set the remote as upstream",
|
|
95
|
+
"set the upstream branch",
|
|
96
|
+
"please specify which branch you want to merge with",
|
|
97
|
+
]
|
|
98
|
+
return any(snippet in message for snippet in upstream_snippets)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def build_auth_error_message(action: str, missing_password: bool) -> str:
|
|
102
|
+
"""
|
|
103
|
+
Build a git authentication error message for the given action.
|
|
104
|
+
|
|
105
|
+
:param action: Git action name
|
|
106
|
+
:param missing_password: Whether the password/token is missing
|
|
107
|
+
:return: Error message string
|
|
108
|
+
"""
|
|
109
|
+
if missing_password:
|
|
110
|
+
return f"Git {action} requires a password/token for private repositories."
|
|
111
|
+
return f"Git {action} requires credentials for private repositories."
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def build_upstream_error_message(action: str) -> str:
|
|
115
|
+
"""
|
|
116
|
+
Build a git upstream tracking error message for the given action.
|
|
117
|
+
|
|
118
|
+
:param action: Git action name
|
|
119
|
+
:return: Error message string
|
|
120
|
+
"""
|
|
121
|
+
if action == "push":
|
|
122
|
+
return (
|
|
123
|
+
"Git push failed because no upstream branch is configured. "
|
|
124
|
+
"Set upstream once with set_upstream=True (and optional remote/branch), "
|
|
125
|
+
"or pass remote and branch explicitly."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
"Git pull failed because no upstream branch is configured. "
|
|
130
|
+
"Pass remote and branch explicitly, or set upstream once (push with "
|
|
131
|
+
"set_upstream=True or run: git branch --set-upstream-to=origin/<branch> <branch>)."
|
|
132
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from loopix.exceptions import InvalidArgumentException
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def resolve_config_scope(
|
|
7
|
+
scope: Optional[str], path: Optional[str]
|
|
8
|
+
) -> tuple[str, Optional[str]]:
|
|
9
|
+
"""
|
|
10
|
+
Resolve a git config scope flag and repository path.
|
|
11
|
+
|
|
12
|
+
:param scope: Requested scope ("global", "local", "system")
|
|
13
|
+
:param path: Repository path for local scope
|
|
14
|
+
:return: Tuple of (scope flag, repository path)
|
|
15
|
+
"""
|
|
16
|
+
scope_name = (scope or "global").strip().lower()
|
|
17
|
+
if scope_name not in {"global", "local", "system"}:
|
|
18
|
+
raise InvalidArgumentException(
|
|
19
|
+
"Git config scope must be one of: global, local, system."
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if scope_name == "local":
|
|
23
|
+
if not path:
|
|
24
|
+
raise InvalidArgumentException(
|
|
25
|
+
"Repository path is required when scope is local."
|
|
26
|
+
)
|
|
27
|
+
return "--local", path
|
|
28
|
+
|
|
29
|
+
if scope_name == "system":
|
|
30
|
+
return "--system", None
|
|
31
|
+
|
|
32
|
+
return "--global", None
|