devs-common 2.0.5__tar.gz → 2.0.7__tar.gz
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.
- {devs_common-2.0.5 → devs_common-2.0.7}/PKG-INFO +1 -1
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/core/container.py +100 -5
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/Dockerfile +8 -3
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/devcontainer.json +2 -1
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/utils/__init__.py +4 -2
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/utils/docker_client.py +15 -0
- devs_common-2.0.7/devs_common/utils/repo_cache.py +227 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common.egg-info/PKG-INFO +1 -1
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common.egg-info/SOURCES.txt +2 -1
- {devs_common-2.0.5 → devs_common-2.0.7}/pyproject.toml +1 -1
- {devs_common-2.0.5 → devs_common-2.0.7}/LICENSE +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/README.md +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/__init__.py +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/config.py +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/core/__init__.py +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/core/project.py +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/core/workspace.py +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/devs_config.py +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/exceptions.py +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/scripts/copy-ssh.sh +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/scripts/post-create-wrapper.sh +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/scripts/python-venv-integration.sh +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/scripts/screenshot-webapp.sh +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/scripts/setup-workspace.sh +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/scripts/start-tunnel.sh +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/scripts/tunnel-status.sh +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/scripts/webapp-screenshot.py +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/sudo-scripts/init-firewall.sh +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/sudo-scripts/setup-devs-env.sh +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/sudo-scripts/setup-vscode-cache.sh +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/sudo-scripts/start-services.sh +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/vscode-settings-reference.md +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/utils/config_hash.py +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/utils/console.py +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/utils/devcontainer.py +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/utils/devcontainer_template.py +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/utils/file_utils.py +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/utils/git_utils.py +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common.egg-info/dependency_links.txt +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common.egg-info/requires.txt +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/devs_common.egg-info/top_level.txt +0 -0
- {devs_common-2.0.5 → devs_common-2.0.7}/setup.cfg +0 -0
|
@@ -293,9 +293,11 @@ class ContainerManager:
|
|
|
293
293
|
self.docker.stop_container(existing_container['name'])
|
|
294
294
|
self.docker.remove_container(existing_container['name'], force=True)
|
|
295
295
|
else:
|
|
296
|
-
# Container exists but not running,
|
|
297
|
-
console.print(f"
|
|
298
|
-
self.docker.
|
|
296
|
+
# Container exists but not running with matching config, just restart it
|
|
297
|
+
console.print(f" 🔄 Restarting stopped container...")
|
|
298
|
+
self.docker.start_container(existing_container['name'])
|
|
299
|
+
console.print(f" ✅ Container restarted successfully")
|
|
300
|
+
return True
|
|
299
301
|
else:
|
|
300
302
|
if rebuild_needed or force_rebuild:
|
|
301
303
|
console.print(f" 🔨 Rebuild needed (devcontainer files changed or force_rebuild)")
|
|
@@ -467,7 +469,41 @@ class ContainerManager:
|
|
|
467
469
|
|
|
468
470
|
except DockerError as e:
|
|
469
471
|
raise ContainerError(f"Failed to list containers: {e}")
|
|
470
|
-
|
|
472
|
+
|
|
473
|
+
@staticmethod
|
|
474
|
+
def list_all_containers() -> List[ContainerInfo]:
|
|
475
|
+
"""List all devs-managed containers across all projects.
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
List of ContainerInfo objects sorted by project name then dev name
|
|
479
|
+
"""
|
|
480
|
+
try:
|
|
481
|
+
docker_client = DockerClient()
|
|
482
|
+
containers = docker_client.find_containers_by_labels({"devs.managed": "true"})
|
|
483
|
+
|
|
484
|
+
result = []
|
|
485
|
+
for container_data in containers:
|
|
486
|
+
dev_name = container_data['labels'].get('devs.dev', 'unknown')
|
|
487
|
+
project_name = container_data['labels'].get('devs.project', 'unknown')
|
|
488
|
+
|
|
489
|
+
container_info = ContainerInfo(
|
|
490
|
+
name=container_data['name'],
|
|
491
|
+
dev_name=dev_name,
|
|
492
|
+
project_name=project_name,
|
|
493
|
+
status=container_data['status'],
|
|
494
|
+
container_id=container_data['id'],
|
|
495
|
+
created=_parse_docker_timestamp(container_data['created']),
|
|
496
|
+
labels=container_data['labels']
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
result.append(container_info)
|
|
500
|
+
|
|
501
|
+
result.sort(key=lambda c: (c.project_name, c.dev_name))
|
|
502
|
+
return result
|
|
503
|
+
|
|
504
|
+
except DockerError as e:
|
|
505
|
+
raise ContainerError(f"Failed to list containers: {e}")
|
|
506
|
+
|
|
471
507
|
def find_aborted_containers(self, all_projects: bool = False) -> List[ContainerInfo]:
|
|
472
508
|
"""Find aborted devs containers that failed during setup.
|
|
473
509
|
|
|
@@ -841,6 +877,65 @@ class ContainerManager:
|
|
|
841
877
|
|
|
842
878
|
return tunnel_name
|
|
843
879
|
|
|
880
|
+
def tunnel_auth(self, dev_name: str, workspace_dir: Path, debug: bool = False, live: bool = False, extra_env: Optional[Dict[str, str]] = None) -> None:
|
|
881
|
+
"""Authenticate VS Code tunnel (interactive, one-time setup).
|
|
882
|
+
|
|
883
|
+
Runs the device code flow interactively inside the container. Auth is
|
|
884
|
+
stored in a bind-mounted directory (~/.devs/vscode-cli/) so it persists
|
|
885
|
+
across container recreations and is shared across all containers.
|
|
886
|
+
|
|
887
|
+
Args:
|
|
888
|
+
dev_name: Development environment name
|
|
889
|
+
workspace_dir: Workspace directory path
|
|
890
|
+
debug: Show debug output for devcontainer operations
|
|
891
|
+
live: Whether the container is in live mode
|
|
892
|
+
extra_env: Additional environment variables to pass to container
|
|
893
|
+
|
|
894
|
+
Raises:
|
|
895
|
+
ContainerError: If authentication fails
|
|
896
|
+
"""
|
|
897
|
+
try:
|
|
898
|
+
container_name, container_workspace_dir = self._prepare_container_exec(
|
|
899
|
+
dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
tunnel_name = self._get_tunnel_name(dev_name)
|
|
903
|
+
|
|
904
|
+
console.print("🔐 Setting up VS Code tunnel authentication...")
|
|
905
|
+
console.print(f" Container: {container_name}")
|
|
906
|
+
console.print("")
|
|
907
|
+
console.print("[yellow]Follow the prompts to authenticate:[/yellow]")
|
|
908
|
+
console.print(" 1. A URL and code will be displayed")
|
|
909
|
+
console.print(" 2. Open the URL in your browser")
|
|
910
|
+
console.print(" 3. Enter the code to authenticate with GitHub/Microsoft")
|
|
911
|
+
console.print("")
|
|
912
|
+
|
|
913
|
+
cmd = [
|
|
914
|
+
'docker', 'exec', '-it',
|
|
915
|
+
container_name,
|
|
916
|
+
'/usr/local/bin/code', 'tunnel', 'user', 'login',
|
|
917
|
+
'--provider', 'github'
|
|
918
|
+
]
|
|
919
|
+
|
|
920
|
+
if debug:
|
|
921
|
+
console.print(f"[dim]Running: {' '.join(cmd)}[/dim]")
|
|
922
|
+
|
|
923
|
+
result = subprocess.run(cmd, check=False)
|
|
924
|
+
|
|
925
|
+
if result.returncode == 0:
|
|
926
|
+
console.print("")
|
|
927
|
+
console.print("[bold green]Authentication successful![/bold green]")
|
|
928
|
+
console.print(" Auth is shared across all containers on this machine.")
|
|
929
|
+
console.print("")
|
|
930
|
+
console.print(f" Start a tunnel: devs tunnel {dev_name}")
|
|
931
|
+
console.print(f" Then connect: code --remote tunnel+{tunnel_name} {container_workspace_dir}")
|
|
932
|
+
else:
|
|
933
|
+
console.print("")
|
|
934
|
+
console.print("[yellow]Authentication was cancelled or failed.[/yellow]")
|
|
935
|
+
|
|
936
|
+
except (DockerError, subprocess.SubprocessError) as e:
|
|
937
|
+
raise ContainerError(f"Failed to authenticate tunnel in {dev_name}: {e}")
|
|
938
|
+
|
|
844
939
|
def start_tunnel(self, dev_name: str, workspace_dir: Path, debug: bool = False, live: bool = False, extra_env: Optional[Dict[str, str]] = None) -> None:
|
|
845
940
|
"""Start a VS Code tunnel in the background.
|
|
846
941
|
|
|
@@ -877,7 +972,7 @@ class ContainerManager:
|
|
|
877
972
|
'docker', 'exec', '-d',
|
|
878
973
|
container_name,
|
|
879
974
|
'/bin/sh', '-c',
|
|
880
|
-
f'{tunnel_cmd} > {log_file} 2>&1'
|
|
975
|
+
f'VSCODE_CLI_DATA_DIR=/home/node/.vscode/cli {tunnel_cmd} > {log_file} 2>&1'
|
|
881
976
|
]
|
|
882
977
|
|
|
883
978
|
if debug:
|
|
@@ -119,8 +119,11 @@ RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/
|
|
|
119
119
|
-a "export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
|
|
120
120
|
-x
|
|
121
121
|
|
|
122
|
-
# Install Claude CLI
|
|
123
|
-
RUN
|
|
122
|
+
# Install Claude CLI via native installer
|
|
123
|
+
RUN curl -fsSL https://claude.ai/install.sh | bash
|
|
124
|
+
|
|
125
|
+
# Install OpenAI Codex CLI
|
|
126
|
+
RUN npm install -g @openai/codex
|
|
124
127
|
|
|
125
128
|
# Install VS Code CLI for tunnel support (needs root for /usr/local/bin)
|
|
126
129
|
# Using the standalone CLI which supports the 'tunnel' command
|
|
@@ -139,7 +142,9 @@ RUN ARCH=$(dpkg --print-architecture) && \
|
|
|
139
142
|
RUN for shell_rc in /home/node/.zshrc /home/node/.bashrc; do \
|
|
140
143
|
echo "export CLAUDE_CONFIG_DIR=\"/home/node/claudeconfig\"" >> "$shell_rc" && \
|
|
141
144
|
echo "export CODEX_CONFIG_HOME=\"/home/node/codexconfig\"" >> "$shell_rc" && \
|
|
142
|
-
echo "
|
|
145
|
+
echo "export VSCODE_CLI_DATA_DIR=\"/home/node/.vscode/cli\"" >> "$shell_rc" && \
|
|
146
|
+
echo "export PATH=\"/home/node/.local/bin:\$PATH\"" >> "$shell_rc" && \
|
|
147
|
+
echo "alias claude=\"claude --dangerously-skip-permissions\"" >> "$shell_rc" && \
|
|
143
148
|
echo "alias codex=\"/usr/local/share/npm-global/bin/codex --full-auto\"" >> "$shell_rc" && \
|
|
144
149
|
echo "" >> "$shell_rc" && \
|
|
145
150
|
echo "# Auto-activate Python venv if it exists" >> "$shell_rc" && \
|
|
@@ -96,7 +96,8 @@
|
|
|
96
96
|
"DEVS_DEBUG": "${localEnv:DEVS_DEBUG}",
|
|
97
97
|
"DEVS_LIVE_MODE": "${localEnv:DEVS_LIVE_MODE}",
|
|
98
98
|
"DEVS_ENV_MOUNT_PATH": "${localEnv:DEVS_ENV_MOUNT_PATH}",
|
|
99
|
-
"DEVS_BRIDGE_MOUNT_PATH": "${localEnv:DEVS_BRIDGE_MOUNT_PATH}"
|
|
99
|
+
"DEVS_BRIDGE_MOUNT_PATH": "${localEnv:DEVS_BRIDGE_MOUNT_PATH}",
|
|
100
|
+
"VSCODE_CLI_DATA_DIR": "/home/node/.vscode/cli"
|
|
100
101
|
},
|
|
101
102
|
"workspaceFolder": "/workspaces/${localEnv:WORKSPACE_FOLDER_NAME}",
|
|
102
103
|
"postCreateCommand": "/usr/local/bin/post-create-wrapper.sh",
|
|
@@ -16,10 +16,11 @@ from .git_utils import (
|
|
|
16
16
|
|
|
17
17
|
from .docker_client import DockerClient
|
|
18
18
|
from .devcontainer import DevContainerCLI
|
|
19
|
+
from .repo_cache import RepoCache
|
|
19
20
|
|
|
20
21
|
__all__ = [
|
|
21
22
|
"copy_file_list",
|
|
22
|
-
"copy_directory_tree",
|
|
23
|
+
"copy_directory_tree",
|
|
23
24
|
"safe_remove_directory",
|
|
24
25
|
"ensure_directory_exists",
|
|
25
26
|
"get_directory_size",
|
|
@@ -27,5 +28,6 @@ __all__ = [
|
|
|
27
28
|
"get_tracked_files",
|
|
28
29
|
"is_git_repository",
|
|
29
30
|
"DockerClient",
|
|
30
|
-
"DevContainerCLI"
|
|
31
|
+
"DevContainerCLI",
|
|
32
|
+
"RepoCache",
|
|
31
33
|
]
|
|
@@ -81,6 +81,21 @@ class DockerClient:
|
|
|
81
81
|
except DockerException as e:
|
|
82
82
|
raise DockerError(f"Error checking container status {name}: {e}")
|
|
83
83
|
|
|
84
|
+
def start_container(self, name: str) -> None:
|
|
85
|
+
"""Start a stopped container.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
name: Container name
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
DockerError: If starting fails
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
container = self.client.containers.get(name)
|
|
95
|
+
container.start()
|
|
96
|
+
except DockerException as e:
|
|
97
|
+
raise DockerError(f"Error starting container {name}: {e}")
|
|
98
|
+
|
|
84
99
|
def stop_container(self, name: str) -> None:
|
|
85
100
|
"""Stop a container.
|
|
86
101
|
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Repository cache for cloning GitHub repos locally."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
from ..exceptions import DevsError
|
|
10
|
+
from .console import get_console
|
|
11
|
+
|
|
12
|
+
console = get_console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RepoCache:
|
|
16
|
+
"""Manages a local cache of cloned GitHub repositories.
|
|
17
|
+
|
|
18
|
+
Repos are cloned into a cache directory (default: ~/.devs/repocache/)
|
|
19
|
+
using the naming convention org-repo (e.g. ideonate-devs).
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
cache_dir: Directory for cached repos. Defaults to ~/.devs/repocache/.
|
|
23
|
+
token: GitHub token for private repo access. If not provided, falls
|
|
24
|
+
back to GH_TOKEN / GITHUB_TOKEN environment variables.
|
|
25
|
+
default_branches: Ordered list of branch names to try when no explicit
|
|
26
|
+
branch is requested. Defaults to detecting via symbolic-ref, then
|
|
27
|
+
trying "main" and "master".
|
|
28
|
+
clean: If True, run ``git clean -fd`` after checkout to remove
|
|
29
|
+
untracked files from previous runs.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
cache_dir: Optional[Path] = None,
|
|
35
|
+
token: Optional[str] = None,
|
|
36
|
+
default_branches: Optional[List[str]] = None,
|
|
37
|
+
clean: bool = False,
|
|
38
|
+
) -> None:
|
|
39
|
+
self.cache_dir = cache_dir or Path.home() / ".devs" / "repocache"
|
|
40
|
+
self._token = token
|
|
41
|
+
self._default_branches = default_branches
|
|
42
|
+
self._clean = clean
|
|
43
|
+
|
|
44
|
+
def _repo_name_to_dir_name(self, repo_name: str) -> str:
|
|
45
|
+
"""Convert org/repo to org-repo directory name."""
|
|
46
|
+
return repo_name.replace("/", "-").lower()
|
|
47
|
+
|
|
48
|
+
def _get_token(self) -> Optional[str]:
|
|
49
|
+
"""Return the configured token, falling back to environment variables."""
|
|
50
|
+
if self._token:
|
|
51
|
+
return self._token
|
|
52
|
+
return os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
|
|
53
|
+
|
|
54
|
+
def _build_clone_url(self, repo_name: str) -> str:
|
|
55
|
+
"""Build clone URL, using token if available for private repos."""
|
|
56
|
+
token = self._get_token()
|
|
57
|
+
if token:
|
|
58
|
+
return f"https://x-access-token:{token}@github.com/{repo_name}.git"
|
|
59
|
+
return f"https://github.com/{repo_name}.git"
|
|
60
|
+
|
|
61
|
+
def get_repo_path(self, repo_name: str) -> Path:
|
|
62
|
+
"""Return the local cache path for a repo without cloning.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
repo_name: GitHub repo in org/repo format.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Path where the repo would be cached.
|
|
69
|
+
"""
|
|
70
|
+
return self.cache_dir / self._repo_name_to_dir_name(repo_name)
|
|
71
|
+
|
|
72
|
+
def ensure_repo(self, repo_name: str, branch: Optional[str] = None) -> Path:
|
|
73
|
+
"""Ensure a repository is cloned and up-to-date in the cache.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
repo_name: GitHub repo in org/repo format (e.g. "ideonate/devs")
|
|
77
|
+
branch: Optional branch to checkout. Defaults to repo's default branch.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Path to the cached repository directory.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
DevsError: If cloning or updating fails.
|
|
84
|
+
"""
|
|
85
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
|
|
87
|
+
dir_name = self._repo_name_to_dir_name(repo_name)
|
|
88
|
+
repo_path = self.cache_dir / dir_name
|
|
89
|
+
|
|
90
|
+
if repo_path.exists() and (repo_path / ".git").exists():
|
|
91
|
+
self._update_repo(repo_path, repo_name, branch)
|
|
92
|
+
else:
|
|
93
|
+
self._clone_repo(repo_path, repo_name, branch)
|
|
94
|
+
|
|
95
|
+
return repo_path
|
|
96
|
+
|
|
97
|
+
def _clone_repo(self, repo_path: Path, repo_name: str, branch: Optional[str] = None) -> None:
|
|
98
|
+
"""Clone a repository into the cache."""
|
|
99
|
+
# Remove directory if it exists but isn't a valid git repo
|
|
100
|
+
if repo_path.exists():
|
|
101
|
+
shutil.rmtree(repo_path)
|
|
102
|
+
|
|
103
|
+
clone_url = self._build_clone_url(repo_name)
|
|
104
|
+
console.print(f" Cloning {repo_name} into cache...")
|
|
105
|
+
|
|
106
|
+
repo_path.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
|
|
108
|
+
cmd = ["git", "clone", clone_url, str(repo_path)]
|
|
109
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
110
|
+
if result.returncode != 0:
|
|
111
|
+
raise DevsError(
|
|
112
|
+
f"Failed to clone {repo_name}: {result.stderr.strip()}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
target = branch or self._detect_branch(repo_path)
|
|
116
|
+
if target:
|
|
117
|
+
self._checkout_branch(repo_path, target)
|
|
118
|
+
|
|
119
|
+
def _update_repo(self, repo_path: Path, repo_name: str, branch: Optional[str] = None) -> None:
|
|
120
|
+
"""Fetch latest changes for an existing cached repo."""
|
|
121
|
+
console.print(f" Updating cached repo {repo_name}...")
|
|
122
|
+
|
|
123
|
+
# Update remote URL in case token changed
|
|
124
|
+
clone_url = self._build_clone_url(repo_name)
|
|
125
|
+
subprocess.run(
|
|
126
|
+
["git", "remote", "set-url", "origin", clone_url],
|
|
127
|
+
cwd=repo_path,
|
|
128
|
+
capture_output=True,
|
|
129
|
+
text=True,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
result = subprocess.run(
|
|
133
|
+
["git", "fetch", "--all"],
|
|
134
|
+
cwd=repo_path,
|
|
135
|
+
capture_output=True,
|
|
136
|
+
text=True,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if result.returncode != 0:
|
|
140
|
+
# Fetch failed – re-clone from scratch
|
|
141
|
+
console.print(f" Fetch failed, re-cloning {repo_name}...")
|
|
142
|
+
self._clone_repo(repo_path, repo_name, branch)
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
# Checkout the requested branch (or default)
|
|
146
|
+
target = branch or self._detect_branch(repo_path)
|
|
147
|
+
if target:
|
|
148
|
+
self._checkout_branch(repo_path, target)
|
|
149
|
+
|
|
150
|
+
def _checkout_branch(self, repo_path: Path, branch: str) -> None:
|
|
151
|
+
"""Checkout a specific branch, pulling latest changes."""
|
|
152
|
+
result = subprocess.run(
|
|
153
|
+
["git", "checkout", "-f", branch],
|
|
154
|
+
cwd=repo_path,
|
|
155
|
+
capture_output=True,
|
|
156
|
+
text=True,
|
|
157
|
+
)
|
|
158
|
+
if result.returncode != 0:
|
|
159
|
+
raise DevsError(
|
|
160
|
+
f"Failed to checkout branch '{branch}': {result.stderr.strip()}"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Reset to match remote so we pick up fetched changes
|
|
164
|
+
subprocess.run(
|
|
165
|
+
["git", "reset", "--hard", f"origin/{branch}"],
|
|
166
|
+
cwd=repo_path,
|
|
167
|
+
capture_output=True,
|
|
168
|
+
text=True,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if self._clean:
|
|
172
|
+
self._clean_untracked(repo_path)
|
|
173
|
+
|
|
174
|
+
def _clean_untracked(self, repo_path: Path) -> None:
|
|
175
|
+
"""Remove untracked files and directories from the repo."""
|
|
176
|
+
subprocess.run(
|
|
177
|
+
["git", "clean", "-fd"],
|
|
178
|
+
cwd=repo_path,
|
|
179
|
+
capture_output=True,
|
|
180
|
+
text=True,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def _detect_branch(self, repo_path: Path) -> Optional[str]:
|
|
184
|
+
"""Detect the default branch to checkout.
|
|
185
|
+
|
|
186
|
+
If ``default_branches`` was provided at construction, tries each in
|
|
187
|
+
order. Otherwise falls back to symbolic-ref detection then main/master.
|
|
188
|
+
"""
|
|
189
|
+
if self._default_branches:
|
|
190
|
+
for branch in self._default_branches:
|
|
191
|
+
result = subprocess.run(
|
|
192
|
+
["git", "rev-parse", "--verify", f"origin/{branch}"],
|
|
193
|
+
cwd=repo_path,
|
|
194
|
+
capture_output=True,
|
|
195
|
+
text=True,
|
|
196
|
+
)
|
|
197
|
+
if result.returncode == 0:
|
|
198
|
+
return branch
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
return self._get_default_branch(repo_path)
|
|
202
|
+
|
|
203
|
+
def _get_default_branch(self, repo_path: Path) -> Optional[str]:
|
|
204
|
+
"""Detect the default branch of a repo (main or master)."""
|
|
205
|
+
result = subprocess.run(
|
|
206
|
+
["git", "symbolic-ref", "refs/remotes/origin/HEAD"],
|
|
207
|
+
cwd=repo_path,
|
|
208
|
+
capture_output=True,
|
|
209
|
+
text=True,
|
|
210
|
+
)
|
|
211
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
212
|
+
# Output like "refs/remotes/origin/main"
|
|
213
|
+
ref = result.stdout.strip()
|
|
214
|
+
return ref.split("/")[-1]
|
|
215
|
+
|
|
216
|
+
# Fallback: try common branch names
|
|
217
|
+
for branch in ("main", "master"):
|
|
218
|
+
result = subprocess.run(
|
|
219
|
+
["git", "rev-parse", "--verify", f"origin/{branch}"],
|
|
220
|
+
cwd=repo_path,
|
|
221
|
+
capture_output=True,
|
|
222
|
+
text=True,
|
|
223
|
+
)
|
|
224
|
+
if result.returncode == 0:
|
|
225
|
+
return branch
|
|
226
|
+
|
|
227
|
+
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/scripts/post-create-wrapper.sh
RENAMED
|
File without changes
|
{devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/scripts/python-venv-integration.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/sudo-scripts/setup-devs-env.sh
RENAMED
|
File without changes
|
{devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/sudo-scripts/setup-vscode-cache.sh
RENAMED
|
File without changes
|
{devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/sudo-scripts/start-services.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|