chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from e2b.exceptions import InvalidArgumentException
|
||||
from e2b.sandbox._git.auth import strip_credentials, with_credentials
|
||||
from e2b.sandbox._git.parse import derive_repo_dir_from_url
|
||||
from e2b.sandbox._git.types import ClonePlan
|
||||
|
||||
|
||||
def shell_escape(value: str) -> str:
|
||||
"""
|
||||
Escape a string for safe use in a shell command.
|
||||
|
||||
:param value: Value to escape
|
||||
:return: Shell-escaped string
|
||||
"""
|
||||
return "'" + value.replace("'", "'\"'\"'") + "'"
|
||||
|
||||
|
||||
def build_git_command(args: List[str], repo_path: Optional[str] = None) -> str:
|
||||
"""
|
||||
Build a shell-safe git command string.
|
||||
|
||||
:param args: Git command arguments
|
||||
:param repo_path: Repository path for `git -C`, if provided
|
||||
:return: Shell-safe git command
|
||||
"""
|
||||
parts = ["git"]
|
||||
if repo_path:
|
||||
parts.extend(["-C", repo_path])
|
||||
parts.extend(args)
|
||||
return " ".join(shell_escape(part) for part in parts)
|
||||
|
||||
|
||||
def build_push_args(
|
||||
remote_name: Optional[str],
|
||||
*,
|
||||
remote: Optional[str],
|
||||
branch: Optional[str],
|
||||
set_upstream: bool,
|
||||
) -> List[str]:
|
||||
"""
|
||||
Build arguments for a git push command.
|
||||
|
||||
:param remote_name: Resolved remote name, if any
|
||||
:param remote: Remote name override
|
||||
:param branch: Branch name to push
|
||||
:param set_upstream: Whether to set upstream tracking
|
||||
:return: List of git push arguments
|
||||
"""
|
||||
args = ["push"]
|
||||
target_remote = remote_name or remote
|
||||
if set_upstream and target_remote:
|
||||
args.append("--set-upstream")
|
||||
if target_remote:
|
||||
args.append(target_remote)
|
||||
if branch:
|
||||
args.append(branch)
|
||||
return args
|
||||
|
||||
|
||||
def build_pull_args(
|
||||
remote: Optional[str],
|
||||
branch: Optional[str],
|
||||
remote_name: Optional[str] = None,
|
||||
) -> List[str]:
|
||||
"""
|
||||
Build arguments for a git pull command.
|
||||
|
||||
:param remote: Remote name override
|
||||
:param branch: Branch name to pull
|
||||
:param remote_name: Resolved remote name, if any
|
||||
:return: List of git pull arguments
|
||||
"""
|
||||
args = ["pull"]
|
||||
target_remote = remote_name or remote
|
||||
if target_remote:
|
||||
args.append(target_remote)
|
||||
if branch:
|
||||
args.append(branch)
|
||||
return args
|
||||
|
||||
|
||||
def build_remote_add_args(name: str, url: str, fetch: bool) -> List[str]:
|
||||
"""
|
||||
Build arguments for a git remote add command.
|
||||
|
||||
:param name: Remote name
|
||||
:param url: Remote URL
|
||||
:param fetch: Whether to fetch after adding the remote
|
||||
:return: List of git remote add arguments
|
||||
"""
|
||||
if not name or not url:
|
||||
raise InvalidArgumentException(
|
||||
"Both remote name and URL are required to add a git remote."
|
||||
)
|
||||
|
||||
args = ["remote", "add"]
|
||||
if fetch:
|
||||
args.append("-f")
|
||||
args.extend([name, url])
|
||||
return args
|
||||
|
||||
|
||||
def build_remote_add_shell_command(
|
||||
args: List[str],
|
||||
path: str,
|
||||
name: str,
|
||||
url: str,
|
||||
fetch: bool,
|
||||
) -> str:
|
||||
"""
|
||||
Build a shell command that adds or updates a remote and optionally fetches.
|
||||
|
||||
:param args: Base git remote add args
|
||||
:param path: Repository path
|
||||
:param name: Remote name
|
||||
:param url: Remote URL
|
||||
:param fetch: Whether to fetch after adding the remote
|
||||
:return: Shell command string
|
||||
"""
|
||||
add_cmd = build_git_command(args, path)
|
||||
set_url_cmd = build_git_command(build_remote_set_url_args(name, url), path)
|
||||
cmd = f"{add_cmd} || {set_url_cmd}"
|
||||
if fetch:
|
||||
fetch_cmd = build_git_command(["fetch", name], path)
|
||||
cmd = f"({cmd}) && {fetch_cmd}"
|
||||
return cmd
|
||||
|
||||
|
||||
def build_remote_get_url_args(name: str) -> List[str]:
|
||||
"""
|
||||
Build arguments for a git remote get-url command.
|
||||
"""
|
||||
return ["remote", "get-url", name]
|
||||
|
||||
|
||||
def build_remote_set_url_args(name: str, url: str) -> List[str]:
|
||||
"""
|
||||
Build arguments for a git remote set-url command.
|
||||
"""
|
||||
return ["remote", "set-url", name, url]
|
||||
|
||||
|
||||
def build_remote_get_command(path: str, name: str) -> str:
|
||||
"""
|
||||
Build a shell command that returns the remote URL or empty output.
|
||||
|
||||
:param path: Repository path
|
||||
:param name: Remote name
|
||||
:return: Shell command string
|
||||
"""
|
||||
if not name:
|
||||
raise InvalidArgumentException("Remote name is required.")
|
||||
|
||||
return f"{build_git_command(build_remote_get_url_args(name), path)} || true"
|
||||
|
||||
|
||||
def build_credential_approve_command(
|
||||
username: str,
|
||||
password: str,
|
||||
host: str,
|
||||
protocol: str,
|
||||
) -> str:
|
||||
"""
|
||||
Build a git credential approve command for the given credentials.
|
||||
"""
|
||||
target_host = host.strip() or "github.com"
|
||||
target_protocol = protocol.strip() or "https"
|
||||
credential_input = "\n".join(
|
||||
[
|
||||
f"protocol={target_protocol}",
|
||||
f"host={target_host}",
|
||||
f"username={username}",
|
||||
f"password={password}",
|
||||
"",
|
||||
"",
|
||||
]
|
||||
)
|
||||
return (
|
||||
f"printf %s {shell_escape(credential_input)} | "
|
||||
f"{build_git_command(['credential', 'approve'])}"
|
||||
)
|
||||
|
||||
|
||||
def build_has_upstream_args() -> List[str]:
|
||||
"""
|
||||
Build arguments for a git upstream check command.
|
||||
"""
|
||||
return ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]
|
||||
|
||||
|
||||
def build_status_args() -> List[str]:
|
||||
"""
|
||||
Build arguments for a git status command.
|
||||
"""
|
||||
return ["status", "--porcelain=1", "-b"]
|
||||
|
||||
|
||||
def build_branches_args() -> List[str]:
|
||||
"""
|
||||
Build arguments for a git branch listing command.
|
||||
"""
|
||||
return ["branch", "--format=%(refname:short)\t%(HEAD)"]
|
||||
|
||||
|
||||
def build_create_branch_args(branch: str) -> List[str]:
|
||||
"""
|
||||
Build arguments for a git checkout -b command.
|
||||
"""
|
||||
return ["checkout", "-b", branch]
|
||||
|
||||
|
||||
def build_checkout_branch_args(branch: str) -> List[str]:
|
||||
"""
|
||||
Build arguments for a git checkout command.
|
||||
"""
|
||||
return ["checkout", branch]
|
||||
|
||||
|
||||
def build_delete_branch_args(branch: str, force: bool) -> List[str]:
|
||||
"""
|
||||
Build arguments for a git branch delete command.
|
||||
"""
|
||||
return ["branch", "-D" if force else "-d", branch]
|
||||
|
||||
|
||||
def build_add_args(files: Optional[List[str]], all: bool) -> List[str]:
|
||||
"""
|
||||
Build arguments for a git add command.
|
||||
"""
|
||||
args = ["add"]
|
||||
if not files:
|
||||
args.append("-A" if all else ".")
|
||||
else:
|
||||
args.append("--")
|
||||
args.extend(files)
|
||||
return args
|
||||
|
||||
|
||||
def build_commit_args(
|
||||
message: str,
|
||||
author_name: Optional[str],
|
||||
author_email: Optional[str],
|
||||
allow_empty: bool,
|
||||
) -> List[str]:
|
||||
"""
|
||||
Build arguments for a git commit command.
|
||||
"""
|
||||
args = ["commit", "-m", message]
|
||||
if allow_empty:
|
||||
args.append("--allow-empty")
|
||||
author_args: List[str] = []
|
||||
if author_name:
|
||||
author_args.extend(["-c", f"user.name={author_name}"])
|
||||
if author_email:
|
||||
author_args.extend(["-c", f"user.email={author_email}"])
|
||||
if author_args:
|
||||
args = author_args + args
|
||||
return args
|
||||
|
||||
|
||||
def build_reset_args(
|
||||
mode: Optional[str],
|
||||
target: Optional[str],
|
||||
paths: Optional[List[str]],
|
||||
) -> List[str]:
|
||||
"""
|
||||
Build arguments for a git reset command.
|
||||
"""
|
||||
allowed_modes = ["soft", "mixed", "hard", "merge", "keep"]
|
||||
if mode and mode not in allowed_modes:
|
||||
raise InvalidArgumentException(
|
||||
f"Reset mode must be one of {', '.join(allowed_modes)}."
|
||||
)
|
||||
|
||||
args = ["reset"]
|
||||
if mode:
|
||||
args.append(f"--{mode}")
|
||||
if target:
|
||||
args.append(target)
|
||||
if paths:
|
||||
args.append("--")
|
||||
args.extend(paths)
|
||||
return args
|
||||
|
||||
|
||||
def build_restore_args(
|
||||
paths: List[str],
|
||||
staged: Optional[bool],
|
||||
worktree: Optional[bool],
|
||||
source: Optional[str],
|
||||
) -> List[str]:
|
||||
"""
|
||||
Build arguments for a git restore command.
|
||||
"""
|
||||
if not paths:
|
||||
raise InvalidArgumentException("At least one path is required.")
|
||||
|
||||
resolved_staged = staged
|
||||
resolved_worktree = worktree
|
||||
if staged is None and worktree is None:
|
||||
resolved_worktree = True
|
||||
elif staged is True and worktree is None:
|
||||
resolved_worktree = False
|
||||
elif staged is None and worktree is not None:
|
||||
resolved_staged = False
|
||||
|
||||
if resolved_staged is False and resolved_worktree is False:
|
||||
raise InvalidArgumentException(
|
||||
"At least one of staged or worktree must be true."
|
||||
)
|
||||
|
||||
args = ["restore"]
|
||||
if resolved_worktree:
|
||||
args.append("--worktree")
|
||||
if resolved_staged:
|
||||
args.append("--staged")
|
||||
if source:
|
||||
args.extend(["--source", source])
|
||||
args.append("--")
|
||||
args.extend(paths)
|
||||
return args
|
||||
|
||||
|
||||
def build_clone_plan(
|
||||
url: str,
|
||||
path: Optional[str],
|
||||
branch: Optional[str],
|
||||
depth: Optional[int],
|
||||
auth_username: Optional[str],
|
||||
auth_password: Optional[str],
|
||||
dangerously_store_credentials: bool,
|
||||
) -> ClonePlan:
|
||||
"""
|
||||
Build clone arguments and metadata for post-clone credential stripping.
|
||||
"""
|
||||
clone_url = (
|
||||
with_credentials(url, auth_username, auth_password)
|
||||
if auth_username and auth_password
|
||||
else url
|
||||
)
|
||||
sanitized_url = strip_credentials(clone_url)
|
||||
should_strip = not dangerously_store_credentials and sanitized_url != clone_url
|
||||
repo_path = path if not should_strip else path or derive_repo_dir_from_url(url)
|
||||
if should_strip and not repo_path:
|
||||
raise InvalidArgumentException(
|
||||
"A destination path is required when using credentials without storing them."
|
||||
)
|
||||
|
||||
args = ["clone", clone_url]
|
||||
if branch:
|
||||
args.extend(["--branch", branch, "--single-branch"])
|
||||
if depth:
|
||||
args.extend(["--depth", str(depth)])
|
||||
if path:
|
||||
args.append(path)
|
||||
|
||||
return ClonePlan(
|
||||
args=args,
|
||||
repo_path=repo_path,
|
||||
sanitized_url=sanitized_url if should_strip else None,
|
||||
should_strip=should_strip,
|
||||
)
|
||||
Reference in New Issue
Block a user