-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgitignore_cleanup.py
More file actions
133 lines (104 loc) · 4.66 KB
/
Copy pathgitignore_cleanup.py
File metadata and controls
133 lines (104 loc) · 4.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#!/usr/bin/env python3
"""List paths ignored by a .gitignore file and optionally move them to the trash.
Usage:
python3.14 gitignore_cleanup.py ROOT [--gitignore FILE] [--delete] [--yes]
Install dependencies with:
python3.14 -m pip install pathspec Send2Trash
"""
from __future__ import annotations
import argparse
import os
import sys
from collections.abc import Callable, Sequence
from importlib import import_module
from pathlib import Path
from typing import Protocol, cast
class GitIgnoreSpecification(Protocol):
"""The subset of pathspec's GitIgnoreSpec used by this script."""
@classmethod
def from_lines(cls, lines: Sequence[str]) -> GitIgnoreSpecification: ...
def match_file(self, file: str) -> bool: ...
def parse_arguments(argv: Sequence[str] | None = None) -> argparse.Namespace:
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(
description="List paths ignored by a .gitignore file and optionally move them to the trash."
)
parser.add_argument("root", type=Path, metavar="ROOT", help="directory to scan")
parser.add_argument(
"--gitignore",
type=Path,
metavar="FILE",
help="path to the .gitignore file (defaults to ROOT/.gitignore)",
)
parser.add_argument("--delete", action="store_true", help="move matching paths to the system trash")
parser.add_argument("--yes", action="store_true", help="do not ask before moving paths to the trash")
return parser.parse_args(argv)
def find_ignored_paths(root: Path, ignore_file: Path) -> list[Path]:
"""Return ignored paths relative to *root*, collapsing ignored directories."""
root = root.resolve()
ignore_file = ignore_file.resolve()
pathspec_module = import_module("pathspec")
specification_class = cast(
type[GitIgnoreSpecification], pathspec_module.GitIgnoreSpec
)
specification = specification_class.from_lines(ignore_file.read_text(encoding="utf-8").splitlines())
ignored_paths: list[Path] = []
for current_directory, directory_names, file_names in os.walk(root, topdown=True, followlinks=False):
current_path = Path(current_directory)
directory_names[:] = sorted(name for name in directory_names if name != ".git")
file_names.sort()
for directory_name in directory_names[:]:
path = current_path / directory_name
relative_path = path.relative_to(root)
if specification.match_file(f"{relative_path.as_posix()}/"):
ignored_paths.append(relative_path)
directory_names.remove(directory_name)
for file_name in file_names:
path = current_path / file_name
if path.resolve() == ignore_file:
continue
relative_path = path.relative_to(root)
if specification.match_file(relative_path.as_posix()):
ignored_paths.append(relative_path)
return ignored_paths
def move_to_trash(paths: Sequence[Path], root: Path, send: Callable[[str], None]) -> None:
"""Move paths, specified relative to *root*, to the system trash."""
for relative_path in paths:
send(str(root / relative_path))
def confirm_deletion(count: int) -> bool:
"""Ask whether *count* paths should be moved to the system trash."""
response = input(f"Move {count} path(s) to the system trash? [y/N] ")
return response.strip().lower() in {"y", "yes"}
def main(argv: Sequence[str] | None = None) -> int:
"""Run the command-line interface."""
args = parse_arguments(argv)
root = args.root.resolve()
ignore_file = (args.gitignore or root / ".gitignore").resolve()
if not root.is_dir():
print(f"Root directory not found: {root}", file=sys.stderr)
return 2
if not ignore_file.is_file():
print(f".gitignore file not found: {ignore_file}", file=sys.stderr)
return 2
try:
ignored_paths = find_ignored_paths(root, ignore_file)
except (ImportError, OSError) as error:
print(f"Error: {error}", file=sys.stderr)
return 1
for relative_path in ignored_paths:
print(relative_path)
if not args.delete or not ignored_paths:
return 0
if not args.yes and not confirm_deletion(len(ignored_paths)):
print("Cancelled.", file=sys.stderr)
return 0
try:
send2trash_module = import_module("send2trash")
send = cast(Callable[[str], None], send2trash_module.send2trash)
move_to_trash(ignored_paths, root, send)
except (ImportError, OSError) as error:
print(f"Error moving paths to the system trash: {error}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())