fix: use working clones for git repos

- Create normal working clones for remote repositories
- Initialize blank repositories with a main branch
- Align pull and branch helpers with unborn HEAD handling
- Gate fetch/pull on repositories with a remote

Quality gates: vitest repositories-settings-tab (passed); api pytest blocked by missing fastapi in environment
This commit is contained in:
2026-05-22 19:54:42 +02:00
parent 2525c58471
commit 4547105f3b
10 changed files with 215 additions and 38 deletions
+60 -29
View File
@@ -115,6 +115,62 @@ def _preflight_remote_repository(remote_url: str) -> None:
)
def _clone_working_repository(remote_url: str, repo_path: str) -> None:
try:
result = subprocess.run(
["git", "clone", remote_url, repo_path],
capture_output=True,
text=True,
timeout=300,
)
except subprocess.TimeoutExpired:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="clone operation timed out")
except FileNotFoundError:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="git command not found")
if result.returncode != 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"failed to clone repository: {result.stderr}",
)
def _init_working_repository(repo_path: str) -> None:
try:
result = subprocess.run(
["git", "init", "-b", "main", repo_path],
capture_output=True,
text=True,
)
except FileNotFoundError:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="git command not found")
if result.returncode == 0:
return
fallback = subprocess.run(
["git", "init", repo_path],
capture_output=True,
text=True,
)
if fallback.returncode != 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"failed to initialize repository: {fallback.stderr}",
)
ref_result = subprocess.run(
["git", "-C", repo_path, "symbolic-ref", "HEAD", "refs/heads/main"],
capture_output=True,
text=True,
)
if ref_result.returncode != 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"failed to set initial branch: {ref_result.stderr}",
)
class GitRepositoryCreate(BaseModel):
name: str
remote_url: str | None = None
@@ -243,7 +299,7 @@ async def parse_repository_url(data: URLParseRequest) -> URLParseResponse:
response_model=GitRepositoryResponse,
status_code=status.HTTP_201_CREATED,
summary="Create a repository",
description="Create a new git repository in a project. Can clone from remote or initialize bare.",
description="Create a new git repository in a project. Can clone from remote or initialize a working repository.",
)
async def create_repository(
project_id: uuid.UUID,
@@ -302,41 +358,16 @@ async def create_repository(
os.makedirs(os.path.dirname(repo_path), exist_ok=True)
if remote_url:
# Clone as mirror
try:
result = subprocess.run(
["git", "clone", "--mirror", remote_url, repo_path],
capture_output=True,
text=True,
timeout=300,
)
if result.returncode != 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"failed to clone repository: {result.stderr}",
)
except subprocess.TimeoutExpired:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="clone operation timed out")
except FileNotFoundError:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="git command not found")
_clone_working_repository(remote_url, repo_path)
else:
# Init bare repo
try:
result = subprocess.run(
["git", "init", "--bare", repo_path],
capture_output=True,
text=True,
check=True,
)
except FileNotFoundError:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="git command not found")
_init_working_repository(repo_path)
repo = GitRepository(
name=data.name,
path=repo_path,
project_id=project_id,
owner_id=user_id,
is_mirror=bool(remote_url),
is_mirror=False,
remote_url=remote_url,
)
session.add(repo)
+10 -3
View File
@@ -45,7 +45,10 @@ def get_status(repo_path: str) -> GitStatus:
try:
branch = _run_git_command(repo_path, "rev-parse", "--abbrev-ref", "HEAD").strip()
except RuntimeError:
branch = "HEAD"
try:
branch = _run_git_command(repo_path, "symbolic-ref", "--short", "HEAD").strip()
except RuntimeError:
branch = "HEAD"
status = GitStatus(branch=branch)
@@ -215,7 +218,8 @@ def pull(repo_path: str, branch: str | None = None) -> None:
"""
args = ["pull"]
if branch:
args.extend(["origin", branch])
args.append("origin")
args.append(branch)
_run_git_command(repo_path, *args)
@@ -279,4 +283,7 @@ def get_current_branch(repo_path: str) -> str:
Returns:
Current branch name
"""
return _run_git_command(repo_path, "rev-parse", "--abbrev-ref", "HEAD").strip()
try:
return _run_git_command(repo_path, "rev-parse", "--abbrev-ref", "HEAD").strip()
except RuntimeError:
return _run_git_command(repo_path, "symbolic-ref", "--short", "HEAD").strip()
+14 -1
View File
@@ -346,7 +346,20 @@ def list_branches(repo_path: str) -> tuple[list[BranchInfo], str]:
)
default_branch = branch_name
except RuntimeError:
pass
try:
output = _run_git_command(repo_path, "symbolic-ref", "--short", "HEAD")
branch_name = output.strip()
if branch_name:
branches.append(
BranchInfo(
name=branch_name,
is_default=True,
last_commit=None,
)
)
default_branch = branch_name
except RuntimeError:
pass
return branches, default_branch