chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
from typing import List, Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from e2b.exceptions import InvalidArgumentException
|
||||
from e2b.sandbox._git.types import GitBranches, GitFileStatus, GitStatus
|
||||
|
||||
|
||||
def derive_repo_dir_from_url(url: str) -> Optional[str]:
|
||||
"""
|
||||
Derive the default repository directory name from a Git URL.
|
||||
|
||||
:param url: Git repository URL
|
||||
:return: Repository directory name, if it can be determined
|
||||
"""
|
||||
parsed = urlparse(url)
|
||||
if parsed.scheme not in ("http", "https"):
|
||||
return None
|
||||
trimmed_path = parsed.path.rstrip("/")
|
||||
if not trimmed_path:
|
||||
return None
|
||||
last_segment = trimmed_path.split("/")[-1]
|
||||
if not last_segment:
|
||||
return None
|
||||
return last_segment[:-4] if last_segment.endswith(".git") else last_segment
|
||||
|
||||
|
||||
def _parse_ahead_behind(segment: Optional[str]) -> tuple[int, int]:
|
||||
"""
|
||||
Parse the ahead/behind segment from porcelain branch info.
|
||||
|
||||
:param segment: Segment text like "ahead 2, behind 1"
|
||||
:return: Tuple of (ahead, behind)
|
||||
"""
|
||||
if not segment:
|
||||
return 0, 0
|
||||
ahead = 0
|
||||
behind = 0
|
||||
if "ahead" in segment:
|
||||
try:
|
||||
ahead = int(segment.split("ahead")[1].split(",")[0].strip())
|
||||
except Exception:
|
||||
ahead = 0
|
||||
if "behind" in segment:
|
||||
try:
|
||||
behind = int(segment.split("behind")[1].split(",")[0].strip())
|
||||
except Exception:
|
||||
behind = 0
|
||||
return ahead, behind
|
||||
|
||||
|
||||
def _normalize_branch_name(name: str) -> str:
|
||||
"""
|
||||
Normalize branch names from porcelain branch output.
|
||||
|
||||
:param name: Raw branch name section
|
||||
:return: Normalized branch name
|
||||
"""
|
||||
if name.startswith("HEAD (detached at "):
|
||||
return name.replace("HEAD (detached at ", "").rstrip(")")
|
||||
return (
|
||||
name.replace("HEAD (no branch)", "HEAD")
|
||||
.replace("No commits yet on ", "")
|
||||
.replace("Initial commit on ", "")
|
||||
)
|
||||
|
||||
|
||||
def _derive_status(index_status: str, working_status: str) -> str:
|
||||
"""
|
||||
Derive a normalized status label from porcelain status characters.
|
||||
|
||||
:param index_status: Index status character
|
||||
:param working_status: Working tree status character
|
||||
:return: Normalized status label
|
||||
"""
|
||||
statuses = {index_status, working_status}
|
||||
if "U" in statuses:
|
||||
return "conflict"
|
||||
if "R" in statuses:
|
||||
return "renamed"
|
||||
if "C" in statuses:
|
||||
return "copied"
|
||||
if "D" in statuses:
|
||||
return "deleted"
|
||||
if "A" in statuses:
|
||||
return "added"
|
||||
if "M" in statuses:
|
||||
return "modified"
|
||||
if "T" in statuses:
|
||||
return "typechange"
|
||||
if "?" in statuses:
|
||||
return "untracked"
|
||||
return "unknown"
|
||||
|
||||
|
||||
def parse_git_status(output: str) -> GitStatus:
|
||||
"""
|
||||
Parse `git status --porcelain=1 -b` output into a structured object.
|
||||
|
||||
:param output: Git status output
|
||||
:return: Parsed `GitStatus`
|
||||
"""
|
||||
lines = [line.rstrip() for line in output.split("\n") if line.strip()]
|
||||
current_branch: Optional[str] = None
|
||||
upstream: Optional[str] = None
|
||||
ahead = 0
|
||||
behind = 0
|
||||
detached = False
|
||||
file_status: List[GitFileStatus] = []
|
||||
|
||||
if not lines:
|
||||
return GitStatus(
|
||||
current_branch=current_branch,
|
||||
upstream=upstream,
|
||||
ahead=ahead,
|
||||
behind=behind,
|
||||
detached=detached,
|
||||
file_status=file_status,
|
||||
)
|
||||
|
||||
branch_line = lines[0]
|
||||
if branch_line.startswith("## "):
|
||||
branch_info = branch_line[3:]
|
||||
ahead_start = branch_info.find(" [")
|
||||
branch_part = branch_info if ahead_start == -1 else branch_info[:ahead_start]
|
||||
ahead_part = None if ahead_start == -1 else branch_info[ahead_start + 2 : -1]
|
||||
normalized_branch = _normalize_branch_name(branch_part)
|
||||
raw_branch = branch_part
|
||||
is_detached = raw_branch.startswith("HEAD (detached at ") or (
|
||||
"detached" in raw_branch
|
||||
)
|
||||
|
||||
if is_detached or normalized_branch.startswith("HEAD"):
|
||||
detached = True
|
||||
elif "..." in normalized_branch:
|
||||
branch, upstream_branch = normalized_branch.split("...")
|
||||
current_branch = branch or None
|
||||
upstream = upstream_branch or None
|
||||
else:
|
||||
current_branch = normalized_branch or None
|
||||
|
||||
ahead, behind = _parse_ahead_behind(ahead_part)
|
||||
|
||||
for line in lines[1:]:
|
||||
if line.startswith("?? "):
|
||||
name = line[3:]
|
||||
file_status.append(
|
||||
GitFileStatus(
|
||||
name=name,
|
||||
status="untracked",
|
||||
index_status="?",
|
||||
working_tree_status="?",
|
||||
staged=False,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if len(line) < 3:
|
||||
continue
|
||||
index_status = line[0]
|
||||
working_status = line[1]
|
||||
path = line[3:]
|
||||
renamed_from: Optional[str] = None
|
||||
name = path
|
||||
if " -> " in path:
|
||||
renamed_from, name = path.split(" -> ", 1)
|
||||
|
||||
file_status.append(
|
||||
GitFileStatus(
|
||||
name=name,
|
||||
status=_derive_status(index_status, working_status),
|
||||
index_status=index_status,
|
||||
working_tree_status=working_status,
|
||||
staged=index_status not in (" ", "?"),
|
||||
renamed_from=renamed_from,
|
||||
)
|
||||
)
|
||||
|
||||
return GitStatus(
|
||||
current_branch=current_branch,
|
||||
upstream=upstream,
|
||||
ahead=ahead,
|
||||
behind=behind,
|
||||
detached=detached,
|
||||
file_status=file_status,
|
||||
)
|
||||
|
||||
|
||||
def parse_git_branches(output: str) -> GitBranches:
|
||||
"""
|
||||
Parse `git branch --format=%(refname:short)\t%(HEAD)` output.
|
||||
|
||||
:param output: Git branch output
|
||||
:return: Parsed `GitBranches`
|
||||
"""
|
||||
branches: List[str] = []
|
||||
current_branch: Optional[str] = None
|
||||
|
||||
lines = [line.strip() for line in output.split("\n") if line.strip()]
|
||||
for line in lines:
|
||||
parts = line.split("\t")
|
||||
name = parts[0]
|
||||
branches.append(name)
|
||||
if len(parts) > 1 and parts[1] == "*":
|
||||
current_branch = name
|
||||
|
||||
return GitBranches(branches=branches, current_branch=current_branch)
|
||||
|
||||
|
||||
def parse_remote_url(output: str, remote: str) -> str:
|
||||
"""
|
||||
Parse a git remote URL output and validate it's present.
|
||||
|
||||
:param output: Git remote get-url output
|
||||
:param remote: Remote name for the error message
|
||||
:return: Remote URL
|
||||
"""
|
||||
url = output.strip()
|
||||
if not url:
|
||||
raise InvalidArgumentException(
|
||||
f'Remote "{remote}" URL not found in repository.'
|
||||
)
|
||||
return url
|
||||
Reference in New Issue
Block a user