Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f995e3d368 | ||
|
|
ffa9d9660a |
@@ -1,3 +1,8 @@
|
||||
## [1.8.2] - 2025-12-16
|
||||
|
||||
* * ***pkgmgr tools code*** is more robust and predictable: it now fails early with clear errors if VS Code is not installed or a repository is not yet identified.
|
||||
|
||||
|
||||
## [1.8.1] - 2025-12-16
|
||||
|
||||
* * Improved stability and consistency of all Git operations (clone, pull, push, release, branch handling) with clearer error messages and predictable preview behavior.
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
rec {
|
||||
pkgmgr = pyPkgs.buildPythonApplication {
|
||||
pname = "package-manager";
|
||||
version = "1.8.1";
|
||||
version = "1.8.2";
|
||||
|
||||
# Use the git repo as source
|
||||
src = ./.;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Maintainer: Kevin Veen-Birkenbach <info@veen.world>
|
||||
|
||||
pkgname=package-manager
|
||||
pkgver=1.8.1
|
||||
pkgver=1.8.2
|
||||
pkgrel=1
|
||||
pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)."
|
||||
arch=('any')
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
package-manager (1.8.2-1) unstable; urgency=medium
|
||||
|
||||
* * ***pkgmgr tools code*** is more robust and predictable: it now fails early with clear errors if VS Code is not installed or a repository is not yet identified.
|
||||
|
||||
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 16 Dec 2025 19:22:41 +0100
|
||||
|
||||
package-manager (1.8.1-1) unstable; urgency=medium
|
||||
|
||||
* * Improved stability and consistency of all Git operations (clone, pull, push, release, branch handling) with clearer error messages and predictable preview behavior.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: package-manager
|
||||
Version: 1.8.1
|
||||
Version: 1.8.2
|
||||
Release: 1%{?dist}
|
||||
Summary: Wrapper that runs Kevin's package-manager via Nix flake
|
||||
|
||||
@@ -74,6 +74,9 @@ echo ">>> package-manager removed. Nix itself was not removed."
|
||||
/usr/lib/package-manager/
|
||||
|
||||
%changelog
|
||||
* Tue Dec 16 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.8.2-1
|
||||
- * ***pkgmgr tools code*** is more robust and predictable: it now fails early with clear errors if VS Code is not installed or a repository is not yet identified.
|
||||
|
||||
* Tue Dec 16 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.8.1-1
|
||||
- * Improved stability and consistency of all Git operations (clone, pull, push, release, branch handling) with clearer error messages and predictable preview behavior.
|
||||
* Mirrors are now handled cleanly: only valid Git remotes are used for Git operations, while non-Git URLs (e.g. PyPI) are excluded, preventing broken or confusing repository configs.
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "kpmx"
|
||||
version = "1.8.1"
|
||||
version = "1.8.2"
|
||||
description = "Kevin's package-manager tool (pkgmgr)"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
|
||||
@@ -1,115 +1,41 @@
|
||||
from __future__ import annotations
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from pkgmgr .cli .context import CLIContext
|
||||
from pkgmgr .core .command .run import run_command
|
||||
from pkgmgr .core .repository .identifier import get_repo_identifier
|
||||
from pkgmgr .core .repository .dir import get_repo_dir
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from pkgmgr.cli.context import CLIContext
|
||||
from pkgmgr.cli.tools import open_vscode_workspace
|
||||
from pkgmgr.cli.tools.paths import resolve_repository_path
|
||||
from pkgmgr.core.command.run import run_command
|
||||
|
||||
Repository = Dict[str, Any]
|
||||
|
||||
|
||||
def _resolve_repository_path(repository: Repository, ctx: CLIContext) -> str:
|
||||
"""
|
||||
Resolve the filesystem path for a repository.
|
||||
|
||||
Priority:
|
||||
1. Use explicit keys if present (directory / path / workspace / workspace_dir).
|
||||
2. Fallback to get_repo_dir(...) using the repositories base directory
|
||||
from the CLI context.
|
||||
"""
|
||||
|
||||
# 1) Explicit path-like keys on the repository object
|
||||
for key in ("directory", "path", "workspace", "workspace_dir"):
|
||||
value = repository.get(key)
|
||||
if value:
|
||||
return value
|
||||
|
||||
# 2) Fallback: compute from base dir + repository metadata
|
||||
base_dir = (
|
||||
getattr(ctx, "repositories_base_dir", None)
|
||||
or getattr(ctx, "repositories_dir", None)
|
||||
)
|
||||
if not base_dir:
|
||||
raise RuntimeError(
|
||||
"Cannot resolve repositories base directory from context; "
|
||||
"expected ctx.repositories_base_dir or ctx.repositories_dir."
|
||||
)
|
||||
|
||||
return get_repo_dir(base_dir, repository)
|
||||
|
||||
|
||||
def handle_tools_command(
|
||||
args,
|
||||
ctx: CLIContext,
|
||||
selected: List[Repository],
|
||||
) -> None:
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# nautilus "explore" command
|
||||
# ------------------------------------------------------------------
|
||||
if args.command == "explore":
|
||||
for repository in selected:
|
||||
repo_path = _resolve_repository_path(repository, ctx)
|
||||
run_command(
|
||||
f'nautilus "{repo_path}" & disown'
|
||||
)
|
||||
return
|
||||
repo_path = resolve_repository_path(repository, ctx)
|
||||
run_command(f'nautilus "{repo_path}" & disown')
|
||||
return
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GNOME terminal command
|
||||
# ------------------------------------------------------------------
|
||||
if args.command == "terminal":
|
||||
for repository in selected:
|
||||
repo_path = _resolve_repository_path(repository, ctx)
|
||||
run_command(
|
||||
f'gnome-terminal --tab --working-directory="{repo_path}"'
|
||||
)
|
||||
return
|
||||
repo_path = resolve_repository_path(repository, ctx)
|
||||
run_command(f'gnome-terminal --tab --working-directory="{repo_path}"')
|
||||
return
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# VS Code workspace command
|
||||
# ------------------------------------------------------------------
|
||||
if args.command == "code":
|
||||
if not selected:
|
||||
print("No repositories selected.")
|
||||
return
|
||||
|
||||
identifiers = [
|
||||
get_repo_identifier(repo, ctx.all_repositories)
|
||||
for repo in selected
|
||||
]
|
||||
sorted_identifiers = sorted(identifiers)
|
||||
workspace_name = "_".join(sorted_identifiers) + ".code-workspace"
|
||||
|
||||
directories_cfg = ctx.config_merged.get("directories") or {}
|
||||
workspaces_dir = os.path.expanduser(
|
||||
directories_cfg.get("workspaces", "~/Workspaces")
|
||||
)
|
||||
os.makedirs(workspaces_dir, exist_ok=True)
|
||||
workspace_file = os.path.join(workspaces_dir, workspace_name)
|
||||
|
||||
folders = [
|
||||
{"path": _resolve_repository_path(repository, ctx)}
|
||||
for repository in selected
|
||||
]
|
||||
|
||||
workspace_data = {
|
||||
"folders": folders,
|
||||
"settings": {},
|
||||
}
|
||||
|
||||
if not os.path.exists(workspace_file):
|
||||
with open(workspace_file, "w", encoding="utf-8") as f:
|
||||
json.dump(workspace_data, f, indent=4)
|
||||
print(f"Created workspace file: {workspace_file}")
|
||||
else:
|
||||
print(f"Using existing workspace file: {workspace_file}")
|
||||
|
||||
run_command(f'code "{workspace_file}"')
|
||||
open_vscode_workspace(ctx, selected)
|
||||
return
|
||||
|
||||
5
src/pkgmgr/cli/tools/__init__.py
Normal file
5
src/pkgmgr/cli/tools/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .vscode import open_vscode_workspace
|
||||
|
||||
__all__ = ["open_vscode_workspace"]
|
||||
35
src/pkgmgr/cli/tools/paths.py
Normal file
35
src/pkgmgr/cli/tools/paths.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
from pkgmgr.cli.context import CLIContext
|
||||
from pkgmgr.core.repository.dir import get_repo_dir
|
||||
|
||||
Repository = Dict[str, Any]
|
||||
|
||||
|
||||
def resolve_repository_path(repository: Repository, ctx: CLIContext) -> str:
|
||||
"""
|
||||
Resolve the filesystem path for a repository.
|
||||
|
||||
Priority:
|
||||
1. Use explicit keys if present (directory / path / workspace / workspace_dir).
|
||||
2. Fallback to get_repo_dir(...) using the repositories base directory
|
||||
from the CLI context.
|
||||
"""
|
||||
for key in ("directory", "path", "workspace", "workspace_dir"):
|
||||
value = repository.get(key)
|
||||
if value:
|
||||
return value
|
||||
|
||||
base_dir = (
|
||||
getattr(ctx, "repositories_base_dir", None)
|
||||
or getattr(ctx, "repositories_dir", None)
|
||||
)
|
||||
if not base_dir:
|
||||
raise RuntimeError(
|
||||
"Cannot resolve repositories base directory from context; "
|
||||
"expected ctx.repositories_base_dir or ctx.repositories_dir."
|
||||
)
|
||||
|
||||
return get_repo_dir(base_dir, repository)
|
||||
102
src/pkgmgr/cli/tools/vscode.py
Normal file
102
src/pkgmgr/cli/tools/vscode.py
Normal file
@@ -0,0 +1,102 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from pkgmgr.cli.context import CLIContext
|
||||
from pkgmgr.cli.tools.paths import resolve_repository_path
|
||||
from pkgmgr.core.command.run import run_command
|
||||
from pkgmgr.core.repository.identifier import get_repo_identifier
|
||||
|
||||
Repository = Dict[str, Any]
|
||||
|
||||
|
||||
def _ensure_vscode_cli_available() -> None:
|
||||
"""
|
||||
Ensure that the VS Code CLI ('code') is available in PATH.
|
||||
"""
|
||||
if shutil.which("code") is None:
|
||||
raise RuntimeError(
|
||||
"VS Code CLI ('code') not found in PATH.\n\n"
|
||||
"Hint:\n"
|
||||
" Install Visual Studio Code and ensure the 'code' command is available.\n"
|
||||
" VS Code → Command Palette → 'Shell Command: Install code command in PATH'\n"
|
||||
)
|
||||
|
||||
|
||||
def _ensure_identifiers_are_filename_safe(identifiers: List[str]) -> None:
|
||||
"""
|
||||
Ensure identifiers can be used in a filename.
|
||||
|
||||
If an identifier contains '/', it likely means the repository has not yet
|
||||
been explicitly identified (no short identifier configured).
|
||||
"""
|
||||
invalid = [i for i in identifiers if "/" in i or os.sep in i]
|
||||
if invalid:
|
||||
raise RuntimeError(
|
||||
"Cannot create VS Code workspace.\n\n"
|
||||
"The following repositories are not yet identified "
|
||||
"(identifier contains '/'): \n"
|
||||
+ "\n".join(f" - {i}" for i in invalid)
|
||||
+ "\n\n"
|
||||
"Hint:\n"
|
||||
" The repository has no short identifier yet.\n"
|
||||
" Add an explicit identifier in your configuration before using `pkgmgr tools code`.\n"
|
||||
)
|
||||
|
||||
|
||||
def _resolve_workspaces_dir(ctx: CLIContext) -> str:
|
||||
directories_cfg = ctx.config_merged.get("directories") or {}
|
||||
return os.path.expanduser(directories_cfg.get("workspaces", "~/Workspaces"))
|
||||
|
||||
|
||||
def _build_workspace_filename(identifiers: List[str]) -> str:
|
||||
sorted_identifiers = sorted(identifiers)
|
||||
return "_".join(sorted_identifiers) + ".code-workspace"
|
||||
|
||||
|
||||
def _build_workspace_data(selected: List[Repository], ctx: CLIContext) -> Dict[str, Any]:
|
||||
folders = [{"path": resolve_repository_path(repo, ctx)} for repo in selected]
|
||||
return {
|
||||
"folders": folders,
|
||||
"settings": {},
|
||||
}
|
||||
|
||||
|
||||
def open_vscode_workspace(ctx: CLIContext, selected: List[Repository]) -> None:
|
||||
"""
|
||||
Create (if missing) and open a VS Code workspace for the selected repositories.
|
||||
|
||||
Policy:
|
||||
- Fail with a clear error if VS Code CLI is missing.
|
||||
- Fail with a clear error if any repository identifier contains '/', because that
|
||||
indicates the repo has not been explicitly identified (no short identifier).
|
||||
- Do NOT auto-sanitize identifiers and do NOT create subfolders under workspaces.
|
||||
"""
|
||||
if not selected:
|
||||
print("No repositories selected.")
|
||||
return
|
||||
|
||||
_ensure_vscode_cli_available()
|
||||
|
||||
identifiers = [get_repo_identifier(repo, ctx.all_repositories) for repo in selected]
|
||||
_ensure_identifiers_are_filename_safe(identifiers)
|
||||
|
||||
workspaces_dir = _resolve_workspaces_dir(ctx)
|
||||
os.makedirs(workspaces_dir, exist_ok=True)
|
||||
|
||||
workspace_name = _build_workspace_filename(identifiers)
|
||||
workspace_file = os.path.join(workspaces_dir, workspace_name)
|
||||
|
||||
workspace_data = _build_workspace_data(selected, ctx)
|
||||
|
||||
if not os.path.exists(workspace_file):
|
||||
with open(workspace_file, "w", encoding="utf-8") as f:
|
||||
json.dump(workspace_data, f, indent=4)
|
||||
print(f"Created workspace file: {workspace_file}")
|
||||
else:
|
||||
print(f"Using existing workspace file: {workspace_file}")
|
||||
|
||||
run_command(f'code "{workspace_file}"')
|
||||
@@ -1,11 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, Dict, List
|
||||
from unittest.mock import call, patch
|
||||
|
||||
from pkgmgr.cli.commands.tools import handle_tools_command
|
||||
|
||||
@@ -26,36 +24,23 @@ class TestHandleToolsCommand(unittest.TestCase):
|
||||
Unit tests for pkgmgr.cli.commands.tools.handle_tools_command.
|
||||
|
||||
We focus on:
|
||||
- Correct path resolution for repositories that have a 'directory' key.
|
||||
- Correct shell commands for 'explore' and 'terminal'.
|
||||
- Proper workspace creation and invocation of 'code' for the 'code' command.
|
||||
- Correct path resolution and shell commands for 'explore' and 'terminal'.
|
||||
- For 'code': delegation to pkgmgr.cli.tools.open_vscode_workspace.
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
# Two fake repositories with explicit 'directory' entries so that
|
||||
# _resolve_repository_path() does not need to call get_repo_dir().
|
||||
self.repos: List[Repository] = [
|
||||
{"alias": "repo1", "directory": "/tmp/repo1"},
|
||||
{"alias": "repo2", "directory": "/tmp/repo2"},
|
||||
]
|
||||
|
||||
# Minimal CLI context; only attributes used in tools.py are provided.
|
||||
self.ctx = SimpleNamespace(
|
||||
config_merged={"directories": {"workspaces": "~/Workspaces"}},
|
||||
all_repositories=self.repos,
|
||||
repositories_base_dir="/base/dir",
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Helper
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _patch_run_command(self):
|
||||
"""
|
||||
Convenience context manager for patching run_command in tools module.
|
||||
"""
|
||||
from unittest.mock import patch
|
||||
|
||||
return patch("pkgmgr.cli.commands.tools.run_command")
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
@@ -63,12 +48,6 @@ class TestHandleToolsCommand(unittest.TestCase):
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_explore_uses_directory_paths(self) -> None:
|
||||
"""
|
||||
The 'explore' command should call Nautilus with the resolved
|
||||
repository paths and use '& disown' as in the implementation.
|
||||
"""
|
||||
from unittest.mock import call
|
||||
|
||||
args = _Args(command="explore")
|
||||
|
||||
with self._patch_run_command() as mock_run_command:
|
||||
@@ -85,12 +64,6 @@ class TestHandleToolsCommand(unittest.TestCase):
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_terminal_uses_directory_paths(self) -> None:
|
||||
"""
|
||||
The 'terminal' command should open a GNOME Terminal tab with the
|
||||
repository as its working directory.
|
||||
"""
|
||||
from unittest.mock import call
|
||||
|
||||
args = _Args(command="terminal")
|
||||
|
||||
with self._patch_run_command() as mock_run_command:
|
||||
@@ -106,63 +79,10 @@ class TestHandleToolsCommand(unittest.TestCase):
|
||||
# Tests for 'code'
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_code_creates_workspace_and_calls_code(self) -> None:
|
||||
"""
|
||||
The 'code' command should:
|
||||
|
||||
- Build a workspace file name from sorted repository identifiers.
|
||||
- Resolve the repository paths into VS Code 'folders'.
|
||||
- Create the workspace file if it does not exist.
|
||||
- Call 'code "<workspace_file>"' via run_command.
|
||||
"""
|
||||
from unittest.mock import patch
|
||||
|
||||
def test_code_delegates_to_open_vscode_workspace(self) -> None:
|
||||
args = _Args(command="code")
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
# Patch expanduser so that the configured '~/Workspaces'
|
||||
# resolves into our temporary directory.
|
||||
with patch(
|
||||
"pkgmgr.cli.commands.tools.os.path.expanduser"
|
||||
) as mock_expanduser:
|
||||
mock_expanduser.return_value = tmpdir
|
||||
with patch("pkgmgr.cli.commands.tools.open_vscode_workspace") as m:
|
||||
handle_tools_command(args, self.ctx, self.repos)
|
||||
|
||||
# Patch get_repo_identifier so the resulting workspace file
|
||||
# name is deterministic and easy to assert.
|
||||
with patch(
|
||||
"pkgmgr.cli.commands.tools.get_repo_identifier"
|
||||
) as mock_get_identifier:
|
||||
mock_get_identifier.side_effect = ["repo-b", "repo-a"]
|
||||
|
||||
with self._patch_run_command() as mock_run_command:
|
||||
handle_tools_command(args, self.ctx, self.repos)
|
||||
|
||||
# The identifiers are ['repo-b', 'repo-a'], which are
|
||||
# sorted to ['repo-a', 'repo-b'] and joined with '_'.
|
||||
expected_workspace_name = "repo-a_repo-b.code-workspace"
|
||||
expected_workspace_file = os.path.join(
|
||||
tmpdir, expected_workspace_name
|
||||
)
|
||||
|
||||
# Workspace file should have been created.
|
||||
self.assertTrue(
|
||||
os.path.exists(expected_workspace_file),
|
||||
"Workspace file was not created.",
|
||||
)
|
||||
|
||||
# The content of the workspace must be valid JSON with
|
||||
# the expected folder paths.
|
||||
with open(expected_workspace_file, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
self.assertIn("folders", data)
|
||||
folder_paths = {f["path"] for f in data["folders"]}
|
||||
self.assertEqual(
|
||||
folder_paths,
|
||||
{"/tmp/repo1", "/tmp/repo2"},
|
||||
)
|
||||
|
||||
# And VS Code must have been invoked with that workspace.
|
||||
mock_run_command.assert_called_once_with(
|
||||
f'code "{expected_workspace_file}"'
|
||||
)
|
||||
m.assert_called_once_with(self.ctx, self.repos)
|
||||
|
||||
38
tests/unit/pkgmgr/cli/tools/test_paths.py
Normal file
38
tests/unit/pkgmgr/cli/tools/test_paths.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class TestResolveRepositoryPath(unittest.TestCase):
|
||||
def test_explicit_directory_key_wins(self) -> None:
|
||||
from pkgmgr.cli.tools.paths import resolve_repository_path
|
||||
|
||||
ctx = SimpleNamespace(repositories_base_dir="/base", repositories_dir="/base2")
|
||||
repo = {"directory": "/explicit/repo"}
|
||||
|
||||
self.assertEqual(resolve_repository_path(repo, ctx), "/explicit/repo")
|
||||
|
||||
def test_fallback_uses_get_repo_dir_with_repositories_base_dir(self) -> None:
|
||||
from pkgmgr.cli.tools.paths import resolve_repository_path
|
||||
|
||||
ctx = SimpleNamespace(repositories_base_dir="/base", repositories_dir="/base2")
|
||||
repo = {"provider": "github.com", "account": "acme", "repository": "demo"}
|
||||
|
||||
with patch("pkgmgr.cli.tools.paths.get_repo_dir", return_value="/computed/repo") as m:
|
||||
out = resolve_repository_path(repo, ctx)
|
||||
|
||||
self.assertEqual(out, "/computed/repo")
|
||||
m.assert_called_once_with("/base", repo)
|
||||
|
||||
def test_raises_if_no_base_dir_in_context(self) -> None:
|
||||
from pkgmgr.cli.tools.paths import resolve_repository_path
|
||||
|
||||
ctx = SimpleNamespace(repositories_base_dir=None, repositories_dir=None)
|
||||
repo = {"provider": "github.com", "account": "acme", "repository": "demo"}
|
||||
|
||||
with self.assertRaises(RuntimeError) as cm:
|
||||
resolve_repository_path(repo, ctx)
|
||||
|
||||
self.assertIn("Cannot resolve repositories base directory", str(cm.exception))
|
||||
131
tests/unit/pkgmgr/cli/tools/test_vscode.py
Normal file
131
tests/unit/pkgmgr/cli/tools/test_vscode.py
Normal file
@@ -0,0 +1,131 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, Dict, List
|
||||
from unittest.mock import patch
|
||||
|
||||
Repository = Dict[str, Any]
|
||||
|
||||
|
||||
class TestOpenVSCodeWorkspace(unittest.TestCase):
|
||||
def test_no_selected_repos_prints_message_and_returns(self) -> None:
|
||||
from pkgmgr.cli.tools.vscode import open_vscode_workspace
|
||||
|
||||
ctx = SimpleNamespace(config_merged={}, all_repositories=[])
|
||||
|
||||
with patch("builtins.print") as p:
|
||||
open_vscode_workspace(ctx, [])
|
||||
|
||||
p.assert_called_once()
|
||||
self.assertIn("No repositories selected.", str(p.call_args[0][0]))
|
||||
|
||||
def test_raises_if_code_cli_missing(self) -> None:
|
||||
from pkgmgr.cli.tools.vscode import open_vscode_workspace
|
||||
|
||||
ctx = SimpleNamespace(config_merged={}, all_repositories=[])
|
||||
selected: List[Repository] = [{"provider": "github.com", "account": "x", "repository": "y"}]
|
||||
|
||||
with patch("pkgmgr.cli.tools.vscode.shutil.which", return_value=None):
|
||||
with self.assertRaises(RuntimeError) as cm:
|
||||
open_vscode_workspace(ctx, selected)
|
||||
|
||||
self.assertIn("VS Code CLI ('code') not found", str(cm.exception))
|
||||
|
||||
def test_raises_if_identifier_contains_slash(self) -> None:
|
||||
from pkgmgr.cli.tools.vscode import open_vscode_workspace
|
||||
|
||||
ctx = SimpleNamespace(
|
||||
config_merged={"directories": {"workspaces": "~/Workspaces"}},
|
||||
all_repositories=[],
|
||||
)
|
||||
selected: List[Repository] = [{"provider": "github.com", "account": "x", "repository": "y"}]
|
||||
|
||||
with patch("pkgmgr.cli.tools.vscode.shutil.which", return_value="/usr/bin/code"), patch(
|
||||
"pkgmgr.cli.tools.vscode.get_repo_identifier",
|
||||
return_value="github.com/x/y",
|
||||
):
|
||||
with self.assertRaises(RuntimeError) as cm:
|
||||
open_vscode_workspace(ctx, selected)
|
||||
|
||||
msg = str(cm.exception)
|
||||
self.assertIn("not yet identified", msg)
|
||||
self.assertIn("identifier contains '/'", msg)
|
||||
|
||||
def test_creates_workspace_file_and_calls_code(self) -> None:
|
||||
from pkgmgr.cli.tools.vscode import open_vscode_workspace
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
workspaces_dir = os.path.join(tmp, "Workspaces")
|
||||
repo_path = os.path.join(tmp, "Repos", "dotlinker")
|
||||
|
||||
ctx = SimpleNamespace(
|
||||
config_merged={"directories": {"workspaces": workspaces_dir}},
|
||||
all_repositories=[],
|
||||
repositories_base_dir=os.path.join(tmp, "Repos"),
|
||||
)
|
||||
selected: List[Repository] = [
|
||||
{"provider": "github.com", "account": "kevin", "repository": "dotlinker"}
|
||||
]
|
||||
|
||||
with patch("pkgmgr.cli.tools.vscode.shutil.which", return_value="/usr/bin/code"), patch(
|
||||
"pkgmgr.cli.tools.vscode.get_repo_identifier",
|
||||
return_value="dotlinker",
|
||||
), patch(
|
||||
"pkgmgr.cli.tools.vscode.resolve_repository_path",
|
||||
return_value=repo_path,
|
||||
), patch(
|
||||
"pkgmgr.cli.tools.vscode.run_command"
|
||||
) as run_cmd:
|
||||
open_vscode_workspace(ctx, selected)
|
||||
|
||||
workspace_file = os.path.join(workspaces_dir, "dotlinker.code-workspace")
|
||||
self.assertTrue(os.path.exists(workspace_file))
|
||||
|
||||
with open(workspace_file, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
self.assertEqual(data["folders"], [{"path": repo_path}])
|
||||
self.assertEqual(data["settings"], {})
|
||||
|
||||
run_cmd.assert_called_once_with(f'code "{workspace_file}"')
|
||||
|
||||
def test_uses_existing_workspace_file_without_overwriting(self) -> None:
|
||||
from pkgmgr.cli.tools.vscode import open_vscode_workspace
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
workspaces_dir = os.path.join(tmp, "Workspaces")
|
||||
os.makedirs(workspaces_dir, exist_ok=True)
|
||||
|
||||
workspace_file = os.path.join(workspaces_dir, "dotlinker.code-workspace")
|
||||
original = {"folders": [{"path": "/original"}], "settings": {"x": 1}}
|
||||
with open(workspace_file, "w", encoding="utf-8") as f:
|
||||
json.dump(original, f)
|
||||
|
||||
ctx = SimpleNamespace(
|
||||
config_merged={"directories": {"workspaces": workspaces_dir}},
|
||||
all_repositories=[],
|
||||
)
|
||||
selected: List[Repository] = [
|
||||
{"provider": "github.com", "account": "kevin", "repository": "dotlinker"}
|
||||
]
|
||||
|
||||
with patch("pkgmgr.cli.tools.vscode.shutil.which", return_value="/usr/bin/code"), patch(
|
||||
"pkgmgr.cli.tools.vscode.get_repo_identifier",
|
||||
return_value="dotlinker",
|
||||
), patch(
|
||||
"pkgmgr.cli.tools.vscode.resolve_repository_path",
|
||||
return_value="/new/path",
|
||||
), patch(
|
||||
"pkgmgr.cli.tools.vscode.run_command"
|
||||
) as run_cmd:
|
||||
open_vscode_workspace(ctx, selected)
|
||||
|
||||
with open(workspace_file, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
self.assertEqual(data, original)
|
||||
run_cmd.assert_called_once_with(f'code "{workspace_file}"')
|
||||
Reference in New Issue
Block a user