Skip to content

Commit 68a1c74

Browse files
misteriaudclaude
andcommitted
Fix git worktree detection for dda env dev start
Previously, `dda env dev start` failed when run from git worktrees with arbitrary directory names because it assumed repository directories were named exactly as the repository (e.g., "datadog-agent"). This commit adds git-aware repository resolution that: - Detects git repositories using remote URL, working for both regular repos and worktrees regardless of directory name - Maintains full backward compatibility with non-git directory lookup - Handles edge cases: missing git, no remote, nested structures Implementation: - Added `_resolve_repository_path()` with multi-strategy resolution - Added `_is_matching_repository()` using git remote URL detection - Updated repository mounting to use new git-aware resolver - Added comprehensive tests for worktree scenarios Fixes issue where worktrees like `.worktrees/my_feature` would fail with "Local repository not found: datadog-agent" Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
1 parent 911d691 commit 68a1c74

File tree

2 files changed

+109
-7
lines changed

2 files changed

+109
-7
lines changed

src/dda/env/dev/types/linux_container.py

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -187,15 +187,9 @@ def start(self) -> None:
187187
command.extend(("--mount", mount.as_csv()))
188188

189189
if not self.config.clone:
190-
from dda.utils.fs import Path
191-
192-
repos_path = Path.cwd().parent
193190
for repo_spec in self.config.repos:
194191
repo = repo_spec.split("@")[0]
195-
repo_path = repos_path / repo
196-
if not repo_path.is_dir():
197-
self.app.abort(f"Local repository not found: {repo}")
198-
192+
repo_path = self._resolve_repository_path(repo_spec)
199193
command.extend(("-v", f"{repo_path}:{self.repo_path(repo)}"))
200194

201195
for mount_spec in self.config.extra_mount_specs:
@@ -450,6 +444,81 @@ def repo_path(self, repo: str | None) -> str:
450444

451445
return f"{self.home_dir}/repos/{repo}"
452446

447+
def _resolve_repository_path(self, repo_spec: str) -> Path:
448+
"""
449+
Resolve the local path for a repository specification.
450+
451+
Tries multiple strategies:
452+
1. Check if current directory is the requested repo (git-aware, handles worktrees)
453+
2. Check parent directory for repo (backward compatible)
454+
455+
Args:
456+
repo_spec: Repository specification (e.g., "datadog-agent" or "datadog-agent@branch")
457+
458+
Returns:
459+
Path to the repository
460+
461+
Raises:
462+
Aborts if repository cannot be found
463+
"""
464+
from dda.utils.fs import Path
465+
466+
repo_name = repo_spec.split("@")[0] # Strip @branch/@tag if present
467+
468+
# Strategy 1: Check if current directory is a git repository matching the repo name
469+
cwd = Path.cwd()
470+
if self._is_matching_repository(cwd, repo_name):
471+
return cwd
472+
473+
# Strategy 2: Check parent directory (existing behavior, backward compatible)
474+
parent_repo_path = cwd.parent / repo_name
475+
if parent_repo_path.is_dir():
476+
if self._is_matching_repository(parent_repo_path, repo_name):
477+
return parent_repo_path
478+
# Fallback: If not a git repo but directory exists, use it for backward compat
479+
return parent_repo_path
480+
481+
self.app.abort(f"Local repository not found: {repo_name}")
482+
483+
def _is_matching_repository(self, path: Path, expected_repo_name: str) -> bool:
484+
"""
485+
Check if the given path is a git repository matching the expected repository name.
486+
487+
Uses git remote URL to determine the repository name, which works for:
488+
- Regular repositories
489+
- Git worktrees (regardless of directory name)
490+
- Nested repository structures
491+
492+
Args:
493+
path: Path to check
494+
expected_repo_name: Expected repository name (e.g., "datadog-agent")
495+
496+
Returns:
497+
True if the path is a git repository matching the expected name
498+
"""
499+
if not path.is_dir():
500+
return False
501+
502+
git_dir = path / ".git"
503+
if not git_dir.exists():
504+
return False
505+
506+
# Use git to get the repository name from the remote URL
507+
try:
508+
# Change to the target directory temporarily to get its remote
509+
import os
510+
511+
original_cwd = os.getcwd()
512+
try:
513+
os.chdir(path)
514+
remote = self.app.tools.git.get_remote()
515+
return remote.repo == expected_repo_name
516+
finally:
517+
os.chdir(original_cwd)
518+
except Exception:
519+
# Not a git repository or no remote configured
520+
return False
521+
453522
def _container_cp(self, source: str, destination: str, *args: Any) -> None:
454523
"""Runs a `cp -r` command inside the context of the container"""
455524
self.run_command(["cp", "-r", f'"{source}"', f'"{destination}"', *args])

tests/env/dev/types/test_linux_container.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1415,6 +1415,39 @@ def test_directory_to_existing_file_fails(self, linux_container, test_target_dir
14151415
linux_container.export_path(source="/folder1", destination=existing_file)
14161416

14171417

1418+
class TestRepositoryResolution:
1419+
"""Tests for git-aware repository resolution logic."""
1420+
1421+
def test_git_worktree_current_directory(self, app, mocker, temp_dir):
1422+
"""Test git worktree with arbitrary directory name is detected as current directory."""
1423+
worktree_dir = temp_dir / "my_feature_branch"
1424+
worktree_dir.ensure_dir()
1425+
(worktree_dir / ".git").touch()
1426+
1427+
mock_remote = mocker.MagicMock()
1428+
mock_remote.repo = "datadog-agent"
1429+
mocker.patch.object(app.tools.git, "get_remote", return_value=mock_remote)
1430+
1431+
container = LinuxContainer(app=app, name="test", instance="test")
1432+
with worktree_dir.as_cwd():
1433+
resolved = container._resolve_repository_path("datadog-agent")
1434+
1435+
assert resolved == worktree_dir
1436+
1437+
def test_backward_compatibility_parent_directory(self, app, mocker, temp_dir):
1438+
"""Test non-git directory is found via parent/repo_name (backward compatible)."""
1439+
repo_dir = temp_dir / "datadog-agent"
1440+
repo_dir.ensure_dir()
1441+
1442+
mocker.patch.object(app.tools.git, "get_remote", side_effect=Exception("Not a git repo"))
1443+
1444+
container = LinuxContainer(app=app, name="test", instance="test")
1445+
with repo_dir.as_cwd():
1446+
resolved = container._resolve_repository_path("datadog-agent")
1447+
1448+
assert resolved == repo_dir
1449+
1450+
14181451
class TestImportFiles:
14191452
"""Basic import operations."""
14201453

0 commit comments

Comments
 (0)