dotx 3.3.0__tar.gz → 3.3.2__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.
- {dotx-3.3.0/src/dotx.egg-info → dotx-3.3.2}/PKG-INFO +1 -1
- {dotx-3.3.0 → dotx-3.3.2}/pyproject.toml +1 -1
- {dotx-3.3.0 → dotx-3.3.2}/src/dotx/commands/database.py +17 -4
- {dotx-3.3.0 → dotx-3.3.2}/src/dotx/commands/install_cmd.py +11 -4
- dotx-3.3.2/src/dotx/commands/uninstall_cmd.py +155 -0
- {dotx-3.3.0 → dotx-3.3.2/src/dotx.egg-info}/PKG-INFO +1 -1
- {dotx-3.3.0 → dotx-3.3.2}/tests/test_cli.py +66 -0
- {dotx-3.3.0 → dotx-3.3.2}/tests/test_cli_database.py +42 -1
- dotx-3.3.0/src/dotx/commands/uninstall_cmd.py +0 -75
- {dotx-3.3.0 → dotx-3.3.2}/LICENSE +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/MANIFEST.in +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/README.md +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/setup.cfg +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/src/dotx/__init__.py +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/src/dotx/always-create +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/src/dotx/cli.py +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/src/dotx/commands/__init__.py +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/src/dotx/commands/path_cmd.py +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/src/dotx/commands/progress.py +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/src/dotx/database.py +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/src/dotx/dotxignore +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/src/dotx/hierarchy.py +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/src/dotx/ignore.py +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/src/dotx/install.py +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/src/dotx/installed-schema.sql +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/src/dotx/options.py +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/src/dotx/plan.py +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/src/dotx/uninstall.py +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/src/dotx.egg-info/SOURCES.txt +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/src/dotx.egg-info/dependency_links.txt +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/src/dotx.egg-info/entry_points.txt +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/src/dotx.egg-info/requires.txt +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/src/dotx.egg-info/top_level.txt +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/tests/test_always_create.py +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/tests/test_ignore.py +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/tests/test_ignore_rules.py +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/tests/test_install.py +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/tests/test_options.py +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/tests/test_path_which.py +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/tests/test_plan.py +0 -0
- {dotx-3.3.0 → dotx-3.3.2}/tests/test_uninstall.py +0 -0
|
@@ -85,7 +85,7 @@ def register_commands(app: typer.Typer):
|
|
|
85
85
|
packages = db.get_all_packages()
|
|
86
86
|
|
|
87
87
|
if not packages:
|
|
88
|
-
console.print("[yellow]No packages
|
|
88
|
+
console.print("[yellow]No packages in database. Run `dotx sync --package-root <dir>` to rebuild from existing symlinks.[/yellow]")
|
|
89
89
|
return
|
|
90
90
|
|
|
91
91
|
if as_commands:
|
|
@@ -100,17 +100,30 @@ def register_commands(app: typer.Typer):
|
|
|
100
100
|
table.add_column("Package", style="cyan", no_wrap=True)
|
|
101
101
|
table.add_column("Files", justify="right", style="magenta")
|
|
102
102
|
table.add_column("Last Install", style="green")
|
|
103
|
+
table.add_column("Status", style="green")
|
|
103
104
|
|
|
105
|
+
missing_count = 0
|
|
104
106
|
for pkg in packages:
|
|
105
107
|
# Show full package path (package_root / package_name) as documented
|
|
106
|
-
package_path =
|
|
108
|
+
package_path = Path(pkg["package_root"]) / pkg["package_name"]
|
|
107
109
|
file_count = str(pkg["file_count"])
|
|
108
110
|
latest = _format_timestamp(pkg["latest_install"])
|
|
109
|
-
|
|
111
|
+
|
|
112
|
+
# Check if source still exists
|
|
113
|
+
if package_path.exists():
|
|
114
|
+
status = "[green]✓[/green]"
|
|
115
|
+
else:
|
|
116
|
+
status = "[yellow]⚠ source missing[/yellow]"
|
|
117
|
+
missing_count += 1
|
|
118
|
+
|
|
119
|
+
table.add_row(str(package_path), file_count, latest, status)
|
|
110
120
|
|
|
111
121
|
console.print()
|
|
112
122
|
console.print(table)
|
|
113
|
-
console.print(f"\n[bold]Total: {len(packages)} package(s)[/bold]
|
|
123
|
+
console.print(f"\n[bold]Total: {len(packages)} package(s)[/bold]")
|
|
124
|
+
if missing_count > 0:
|
|
125
|
+
console.print(f"[yellow]⚠ {missing_count} package(s) have missing source directories. Run `dotx verify` for details.[/yellow]")
|
|
126
|
+
console.print()
|
|
114
127
|
|
|
115
128
|
logger.info("list finished")
|
|
116
129
|
|
|
@@ -99,11 +99,18 @@ def register_command(app: typer.Typer):
|
|
|
99
99
|
if total_dirs:
|
|
100
100
|
summary_parts.append(f"{total_dirs} dir(s)")
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
if summary_parts:
|
|
103
|
+
summary = " and ".join(summary_parts)
|
|
104
|
+
if is_dry_run(ctx):
|
|
105
|
+
console.print(f"\n[yellow][DRY RUN] Would install {summary} from {len(sources)} package(s)[/yellow]")
|
|
106
|
+
else:
|
|
107
|
+
console.print(f"\n[green]✓ Installed {summary} from {len(sources)} package(s)[/green]")
|
|
105
108
|
else:
|
|
106
|
-
|
|
109
|
+
# Empty package(s) - nothing to install
|
|
110
|
+
if is_dry_run(ctx):
|
|
111
|
+
console.print(f"\n[yellow][DRY RUN] Would install nothing from {len(sources)} package(s) (nothing to record in database)[/yellow]")
|
|
112
|
+
else:
|
|
113
|
+
console.print(f"\n[green]✓ Installed nothing from {len(sources)} package(s) (nothing to record in database)[/green]")
|
|
107
114
|
else:
|
|
108
115
|
console.print("[red]✗ Refusing to install - conflicts detected[/red]")
|
|
109
116
|
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Uninstall command for dotx CLI."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from loguru import logger
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from dotx.commands.progress import execute_plans_with_progress
|
|
11
|
+
from dotx.database import InstallationDB
|
|
12
|
+
from dotx.options import is_dry_run, is_verbose_mode
|
|
13
|
+
from dotx.plan import Action, Plan, extract_plan, log_extracted_plan
|
|
14
|
+
from dotx.uninstall import plan_uninstall
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _uninstall_from_database(
|
|
18
|
+
package_path: Path,
|
|
19
|
+
db: InstallationDB,
|
|
20
|
+
console: Console,
|
|
21
|
+
dry_run: bool,
|
|
22
|
+
verbose: bool,
|
|
23
|
+
) -> int:
|
|
24
|
+
"""
|
|
25
|
+
Uninstall a package using database records when source directory is missing.
|
|
26
|
+
|
|
27
|
+
Returns the number of symlinks removed.
|
|
28
|
+
"""
|
|
29
|
+
package_root = package_path.parent
|
|
30
|
+
package_name = package_path.name
|
|
31
|
+
|
|
32
|
+
installations = db.get_installations(package_root, package_name)
|
|
33
|
+
|
|
34
|
+
if not installations:
|
|
35
|
+
console.print(f"[yellow]No installations found for {package_name}[/yellow]")
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
removed_count = 0
|
|
39
|
+
|
|
40
|
+
for entry in installations:
|
|
41
|
+
target_path = Path(entry["target_path"])
|
|
42
|
+
|
|
43
|
+
if dry_run:
|
|
44
|
+
if target_path.is_symlink():
|
|
45
|
+
console.print(f" rm {target_path}")
|
|
46
|
+
removed_count += 1
|
|
47
|
+
elif verbose:
|
|
48
|
+
console.print(f" [dim]skip (not a symlink): {target_path}[/dim]")
|
|
49
|
+
else:
|
|
50
|
+
if target_path.is_symlink():
|
|
51
|
+
try:
|
|
52
|
+
target_path.unlink()
|
|
53
|
+
db.remove_installation(target_path)
|
|
54
|
+
removed_count += 1
|
|
55
|
+
if verbose:
|
|
56
|
+
console.print(f" Removed: {target_path}")
|
|
57
|
+
except OSError as e:
|
|
58
|
+
logger.warning(f"Failed to remove {target_path}: {e}")
|
|
59
|
+
else:
|
|
60
|
+
# Not a symlink - just remove from database
|
|
61
|
+
db.remove_installation(target_path)
|
|
62
|
+
if verbose:
|
|
63
|
+
console.print(f" [dim]Cleaned db entry (not a symlink): {target_path}[/dim]")
|
|
64
|
+
|
|
65
|
+
return removed_count
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def register_command(app: typer.Typer):
|
|
69
|
+
"""Register the uninstall command with the Typer app."""
|
|
70
|
+
|
|
71
|
+
@app.command()
|
|
72
|
+
def uninstall(
|
|
73
|
+
ctx: typer.Context,
|
|
74
|
+
sources: Annotated[
|
|
75
|
+
list[Path],
|
|
76
|
+
typer.Argument(
|
|
77
|
+
help="Source package directories to uninstall (can be deleted)",
|
|
78
|
+
),
|
|
79
|
+
],
|
|
80
|
+
):
|
|
81
|
+
"""
|
|
82
|
+
Uninstall source packages from target directory.
|
|
83
|
+
|
|
84
|
+
If the source package directory still exists, uninstalls by scanning it.
|
|
85
|
+
If the source has been deleted, uninstalls using database records.
|
|
86
|
+
"""
|
|
87
|
+
logger.info("uninstall starting")
|
|
88
|
+
console = Console()
|
|
89
|
+
verbose = is_verbose_mode(ctx)
|
|
90
|
+
dry_run = is_dry_run(ctx)
|
|
91
|
+
|
|
92
|
+
# Get target from options
|
|
93
|
+
target_path = Path(ctx.obj.get("TARGET", Path.home())) if ctx.obj else Path.home()
|
|
94
|
+
|
|
95
|
+
if not sources:
|
|
96
|
+
logger.info("uninstall finished (no sources)")
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
# Partition sources using set math
|
|
100
|
+
sources_set = set(sources)
|
|
101
|
+
existing_sources = {s for s in sources_set if s.exists() and s.is_dir()}
|
|
102
|
+
missing_sources = sources_set - existing_sources
|
|
103
|
+
|
|
104
|
+
total_removed = 0
|
|
105
|
+
|
|
106
|
+
# Handle existing sources with plan-based uninstall
|
|
107
|
+
if existing_sources:
|
|
108
|
+
plans: list[tuple[Path, Plan]] = []
|
|
109
|
+
for source_package in existing_sources:
|
|
110
|
+
plan: Plan = plan_uninstall(source_package, target_path)
|
|
111
|
+
log_extracted_plan(
|
|
112
|
+
plan,
|
|
113
|
+
description=f"Actual plan to uninstall {source_package}",
|
|
114
|
+
actions_to_extract={Action.UNLINK},
|
|
115
|
+
)
|
|
116
|
+
plans.append((source_package, plan))
|
|
117
|
+
|
|
118
|
+
with InstallationDB() as db:
|
|
119
|
+
execute_plans_with_progress(
|
|
120
|
+
plans,
|
|
121
|
+
target_path,
|
|
122
|
+
{Action.UNLINK},
|
|
123
|
+
"Uninstalling",
|
|
124
|
+
console,
|
|
125
|
+
verbose,
|
|
126
|
+
db,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
total_removed += sum(
|
|
130
|
+
len(extract_plan(plan, {Action.UNLINK}))
|
|
131
|
+
for _, plan in plans
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Handle missing sources with database-based uninstall
|
|
135
|
+
if missing_sources:
|
|
136
|
+
if dry_run:
|
|
137
|
+
console.print("\n[yellow][DRY RUN] Would execute the equivalent of:[/yellow]")
|
|
138
|
+
|
|
139
|
+
with InstallationDB() as db:
|
|
140
|
+
for source_package in missing_sources:
|
|
141
|
+
if verbose or dry_run:
|
|
142
|
+
console.print(f"\n[cyan]Uninstalling {source_package.name} (source deleted)...[/cyan]")
|
|
143
|
+
|
|
144
|
+
removed = _uninstall_from_database(
|
|
145
|
+
source_package, db, console, dry_run, verbose
|
|
146
|
+
)
|
|
147
|
+
total_removed += removed
|
|
148
|
+
|
|
149
|
+
# Show summary
|
|
150
|
+
if dry_run:
|
|
151
|
+
console.print(f"\n[yellow][DRY RUN] Would remove {total_removed} symlink(s) from {len(sources)} package(s)[/yellow]")
|
|
152
|
+
else:
|
|
153
|
+
console.print(f"\n[green]✓ Removed {total_removed} symlink(s) from {len(sources)} package(s)[/green]")
|
|
154
|
+
|
|
155
|
+
logger.info("uninstall finished")
|
|
@@ -389,3 +389,69 @@ def test_cli_xdg_mode_install_and_uninstall_roundtrip(tmp_path, monkeypatch, iso
|
|
|
389
389
|
|
|
390
390
|
# File should be removed (not accessible anymore)
|
|
391
391
|
assert not config_file.exists(), "File should be removed after uninstall"
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def test_cli_uninstall_with_deleted_source(tmp_path, isolated_db):
|
|
395
|
+
"""Test that uninstall works even when source package has been deleted."""
|
|
396
|
+
import shutil
|
|
397
|
+
|
|
398
|
+
source = tmp_path / "source"
|
|
399
|
+
source.mkdir()
|
|
400
|
+
target = tmp_path / "target"
|
|
401
|
+
target.mkdir()
|
|
402
|
+
|
|
403
|
+
# Create source files
|
|
404
|
+
(source / "file1.txt").write_text("content1")
|
|
405
|
+
(source / "file2.txt").write_text("content2")
|
|
406
|
+
|
|
407
|
+
runner = CliRunner()
|
|
408
|
+
|
|
409
|
+
# Install the package
|
|
410
|
+
result = runner.invoke(app, [f"--target={target}", "install", str(source)])
|
|
411
|
+
assert result.exit_code == 0, f"Install failed: {result.output}"
|
|
412
|
+
|
|
413
|
+
# Verify files are installed
|
|
414
|
+
assert (target / "file1.txt").is_symlink()
|
|
415
|
+
assert (target / "file2.txt").is_symlink()
|
|
416
|
+
|
|
417
|
+
# Delete the source package
|
|
418
|
+
shutil.rmtree(source)
|
|
419
|
+
assert not source.exists()
|
|
420
|
+
|
|
421
|
+
# Uninstall should still work using database (use --verbose to see "source deleted" message)
|
|
422
|
+
result = runner.invoke(app, [f"--target={target}", "--verbose", "uninstall", str(source)])
|
|
423
|
+
assert result.exit_code == 0, f"Uninstall failed: {result.output}"
|
|
424
|
+
assert "source deleted" in result.output.lower()
|
|
425
|
+
|
|
426
|
+
# Symlinks should be removed (not just broken)
|
|
427
|
+
assert not (target / "file1.txt").is_symlink()
|
|
428
|
+
assert not (target / "file2.txt").is_symlink()
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def test_cli_uninstall_deleted_source_dry_run(tmp_path, isolated_db):
|
|
432
|
+
"""Test dry-run uninstall with deleted source shows correct output."""
|
|
433
|
+
import shutil
|
|
434
|
+
|
|
435
|
+
source = tmp_path / "source"
|
|
436
|
+
source.mkdir()
|
|
437
|
+
target = tmp_path / "target"
|
|
438
|
+
target.mkdir()
|
|
439
|
+
|
|
440
|
+
# Create and install
|
|
441
|
+
(source / "file1.txt").write_text("content")
|
|
442
|
+
|
|
443
|
+
runner = CliRunner()
|
|
444
|
+
result = runner.invoke(app, [f"--target={target}", "install", str(source)])
|
|
445
|
+
assert result.exit_code == 0
|
|
446
|
+
|
|
447
|
+
# Delete source
|
|
448
|
+
shutil.rmtree(source)
|
|
449
|
+
|
|
450
|
+
# Dry-run uninstall
|
|
451
|
+
result = runner.invoke(app, [f"--target={target}", "--dry-run", "uninstall", str(source)])
|
|
452
|
+
assert result.exit_code == 0
|
|
453
|
+
assert "[DRY RUN]" in result.output
|
|
454
|
+
assert "rm" in result.output
|
|
455
|
+
|
|
456
|
+
# Symlink should still exist after dry-run (even though it's broken)
|
|
457
|
+
assert (target / "file1.txt").is_symlink()
|
|
@@ -301,7 +301,7 @@ def test_cli_list_no_packages(tmp_path, monkeypatch):
|
|
|
301
301
|
result = runner.invoke(app, ["list"])
|
|
302
302
|
|
|
303
303
|
assert result.exit_code == 0
|
|
304
|
-
assert "No packages
|
|
304
|
+
assert "No packages in database" in result.output
|
|
305
305
|
|
|
306
306
|
|
|
307
307
|
def test_cli_list_with_packages(tmp_path, monkeypatch):
|
|
@@ -866,3 +866,44 @@ def test_list_shows_correct_package_names_after_install(tmp_path, monkeypatch):
|
|
|
866
866
|
result = runner.invoke(app, ["list"])
|
|
867
867
|
assert result.exit_code == 0, f"list failed: {result.output}"
|
|
868
868
|
assert "2 package(s)" in result.output # Should show 2 packages
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
def test_cli_list_shows_missing_source_status(tmp_path, monkeypatch):
|
|
872
|
+
"""Test 'dotx list' shows status indicator when source package is missing."""
|
|
873
|
+
import shutil
|
|
874
|
+
|
|
875
|
+
# Override XDG_DATA_HOME
|
|
876
|
+
data_dir = tmp_path / "config"
|
|
877
|
+
data_dir.mkdir()
|
|
878
|
+
monkeypatch.setenv("XDG_DATA_HOME", str(data_dir))
|
|
879
|
+
|
|
880
|
+
# Create and install packages
|
|
881
|
+
source1 = tmp_path / "source1"
|
|
882
|
+
source1.mkdir()
|
|
883
|
+
(source1 / "file1").write_text("content")
|
|
884
|
+
|
|
885
|
+
source2 = tmp_path / "source2"
|
|
886
|
+
source2.mkdir()
|
|
887
|
+
(source2 / "file2").write_text("content")
|
|
888
|
+
|
|
889
|
+
target = tmp_path / "target"
|
|
890
|
+
target.mkdir()
|
|
891
|
+
|
|
892
|
+
runner = CliRunner()
|
|
893
|
+
|
|
894
|
+
# Install both packages
|
|
895
|
+
result = runner.invoke(app, [f"--target={target}", "install", str(source1)])
|
|
896
|
+
assert result.exit_code == 0
|
|
897
|
+
result = runner.invoke(app, [f"--target={target}", "install", str(source2)])
|
|
898
|
+
assert result.exit_code == 0
|
|
899
|
+
|
|
900
|
+
# Delete one source package
|
|
901
|
+
shutil.rmtree(source1)
|
|
902
|
+
assert not source1.exists()
|
|
903
|
+
|
|
904
|
+
# List packages - should show warning for missing source
|
|
905
|
+
result = runner.invoke(app, ["list"])
|
|
906
|
+
assert result.exit_code == 0
|
|
907
|
+
# The warning message at the bottom should be visible
|
|
908
|
+
assert "1 package(s) have missing source directories" in result.output
|
|
909
|
+
assert "dotx verify" in result.output
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
"""Uninstall command for dotx CLI."""
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import Annotated
|
|
5
|
-
|
|
6
|
-
import typer
|
|
7
|
-
from loguru import logger
|
|
8
|
-
from rich.console import Console
|
|
9
|
-
|
|
10
|
-
from dotx.commands.progress import execute_plans_with_progress
|
|
11
|
-
from dotx.database import InstallationDB
|
|
12
|
-
from dotx.options import is_dry_run, is_verbose_mode
|
|
13
|
-
from dotx.plan import Action, Plan, extract_plan, log_extracted_plan
|
|
14
|
-
from dotx.uninstall import plan_uninstall
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def register_command(app: typer.Typer):
|
|
18
|
-
"""Register the uninstall command with the Typer app."""
|
|
19
|
-
|
|
20
|
-
@app.command()
|
|
21
|
-
def uninstall(
|
|
22
|
-
ctx: typer.Context,
|
|
23
|
-
sources: Annotated[
|
|
24
|
-
list[Path],
|
|
25
|
-
typer.Argument(
|
|
26
|
-
help="Source package directories to uninstall",
|
|
27
|
-
exists=True,
|
|
28
|
-
file_okay=False,
|
|
29
|
-
dir_okay=True,
|
|
30
|
-
readable=True,
|
|
31
|
-
),
|
|
32
|
-
],
|
|
33
|
-
):
|
|
34
|
-
"""Uninstall source packages from target directory."""
|
|
35
|
-
logger.info("uninstall starting")
|
|
36
|
-
console = Console()
|
|
37
|
-
verbose = is_verbose_mode(ctx)
|
|
38
|
-
|
|
39
|
-
# Get target from options
|
|
40
|
-
target_path = Path(ctx.obj.get("TARGET", Path.home())) if ctx.obj else Path.home()
|
|
41
|
-
|
|
42
|
-
if sources:
|
|
43
|
-
plans: list[tuple[Path, Plan]] = []
|
|
44
|
-
for source_package in sources:
|
|
45
|
-
plan: Plan = plan_uninstall(source_package, target_path)
|
|
46
|
-
log_extracted_plan(
|
|
47
|
-
plan,
|
|
48
|
-
description=f"Actual plan to uninstall {source_package}",
|
|
49
|
-
actions_to_extract={Action.UNLINK},
|
|
50
|
-
)
|
|
51
|
-
plans.append((source_package, plan))
|
|
52
|
-
|
|
53
|
-
# Open database and execute all plans with progress
|
|
54
|
-
with InstallationDB() as db:
|
|
55
|
-
execute_plans_with_progress(
|
|
56
|
-
plans,
|
|
57
|
-
target_path,
|
|
58
|
-
{Action.UNLINK},
|
|
59
|
-
"Uninstalling",
|
|
60
|
-
console,
|
|
61
|
-
verbose,
|
|
62
|
-
db,
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
# Show summary
|
|
66
|
-
total_removed = sum(
|
|
67
|
-
len(extract_plan(plan, {Action.UNLINK}))
|
|
68
|
-
for _, plan in plans
|
|
69
|
-
)
|
|
70
|
-
if is_dry_run(ctx):
|
|
71
|
-
console.print(f"\n[yellow][DRY RUN] Would remove {total_removed} symlink(s) from {len(sources)} package(s)[/yellow]")
|
|
72
|
-
else:
|
|
73
|
-
console.print(f"\n[green]✓ Removed {total_removed} symlink(s) from {len(sources)} package(s)[/green]")
|
|
74
|
-
|
|
75
|
-
logger.info("uninstall finished")
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|