Skip to content

Commit 2b528f2

Browse files
Added option to delete repositories listed in a YAML file. (#40)
Signed-off-by: Leander Stephen D'Souza <[email protected]>
1 parent 5f4d271 commit 2b528f2

File tree

8 files changed

+222
-1
lines changed

8 files changed

+222
-1
lines changed

README.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,29 @@ For those two types the ``version`` key is optional.
107107
If specified only entries from the archive which are in the subfolder specified by the version value are being extracted.
108108

109109

110+
Delete set of repositories
111+
~~~~~~~~~~~~~~~~~~~~~~~~~~
112+
113+
The ``vcs delete`` command removes all directories of repositories which are passed in via ``stdin`` in YAML format.
114+
By default, the command performs a dry-run and only lists the directories which would be deleted.
115+
In addition, it would convey warnings for missing directories and skip invalid paths upon which no action is taken.
116+
To actually delete the directories the ``-f/--force`` argument must be passed::
117+
118+
.. code-block:: bash
119+
120+
$ vcs delete < test/list.repos
121+
122+
Warning: The following paths do not exist:
123+
./immutable/hash
124+
./immutable/hash_tar
125+
./immutable/hash_zip
126+
./immutable/tag
127+
./without_version
128+
The following paths will be deleted:
129+
./vcs2l
130+
Dry-run mode: No directories were deleted. Use -f/--force to actually delete them.
131+
132+
110133
Validate repositories file
111134
~~~~~~~~~~~~~~~~~~~~~~~~~~
112135

scripts/vcs-delete

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env python3
2+
3+
import sys
4+
5+
from vcs2l.commands.delete import main
6+
7+
sys.exit(main() or 0)

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
'vcs-branch = vcs2l.commands.branch:main',
6767
'vcs-bzr = vcs2l.commands.custom:bzr_main',
6868
'vcs-custom = vcs2l.commands.custom:main',
69+
'vcs-delete = vcs2l.commands.delete:main',
6970
'vcs-diff = vcs2l.commands.diff:main',
7071
'vcs-export = vcs2l.commands.export:main',
7172
'vcs-git = vcs2l.commands.custom:git_main',

test/commands.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
branch custom diff export import log pull push remotes status validate
1+
branch custom delete diff export import log pull push remotes status validate

test/delete.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
The following directories will be deleted:
2+
./immutable/hash
3+
./immutable/hash_tar
4+
./immutable/hash_zip
5+
./immutable/tag
6+
./vcs2l
7+
./without_version

test/test_commands.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,36 @@ def test_import_url(self):
328328
finally:
329329
rmtree(workdir)
330330

331+
def test_deletion(self):
332+
"""Test the delete command."""
333+
workdir = os.path.join(TEST_WORKSPACE, 'deletion')
334+
os.makedirs(workdir)
335+
try:
336+
run_command(
337+
'import', ['--input', REPOS_FILE_URL, '.'], subfolder='deletion'
338+
)
339+
output = run_command(
340+
'delete',
341+
['--force', '--input', REPOS_FILE_URL, '.'],
342+
subfolder='deletion',
343+
)
344+
expected = get_expected_output('delete')
345+
self.assertEqual(output, expected)
346+
347+
# check that repositories were actually deleted
348+
self.assertFalse(os.path.exists(os.path.join(workdir, 'immutable/hash')))
349+
self.assertFalse(
350+
os.path.exists(os.path.join(workdir, 'immutable/hash_tar'))
351+
)
352+
self.assertFalse(
353+
os.path.exists(os.path.join(workdir, 'immutable/hash_zip'))
354+
)
355+
self.assertFalse(os.path.exists(os.path.join(workdir, 'immutable/tag')))
356+
self.assertFalse(os.path.exists(os.path.join(workdir, 'vcs2l')))
357+
self.assertFalse(os.path.exists(os.path.join(workdir, 'without_version')))
358+
finally:
359+
rmtree(workdir)
360+
331361
def test_validate(self):
332362
output = run_command('validate', ['--input', REPOS_FILE])
333363
expected = get_expected_output('validate')

vcs2l/commands/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .branch import BranchCommand
22
from .custom import CustomCommand
3+
from .delete import DeleteCommand
34
from .diff import DiffCommand
45
from .export import ExportCommand
56
from .import_ import ImportCommand
@@ -13,6 +14,7 @@
1314
vcs2l_commands = []
1415
vcs2l_commands.append(BranchCommand)
1516
vcs2l_commands.append(CustomCommand)
17+
vcs2l_commands.append(DeleteCommand)
1618
vcs2l_commands.append(DiffCommand)
1719
vcs2l_commands.append(ExportCommand)
1820
vcs2l_commands.append(ImportCommand)

vcs2l/commands/delete.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
"""Command to delete directories of repositories listed in a YAML file."""
2+
3+
import argparse
4+
import os
5+
import sys
6+
import urllib.request as request
7+
from urllib.error import URLError
8+
9+
from vcs2l.commands.import_ import file_or_url_type, get_repositories
10+
from vcs2l.executor import ansi
11+
from vcs2l.streams import set_streams
12+
from vcs2l.util import rmtree
13+
14+
from .command import Command, existing_dir
15+
16+
17+
class DeleteCommand(Command):
18+
"""Delete directories of repositories listed in a YAML file."""
19+
20+
command = 'delete'
21+
help = 'Remove the directories indicated by the list of given repositories.'
22+
23+
24+
def get_parser():
25+
"""CLI parser for the 'delete' command."""
26+
_cls = DeleteCommand
27+
28+
parser = argparse.ArgumentParser(
29+
description=_cls.help, prog='vcs {}'.format(_cls.command)
30+
)
31+
group = parser.add_argument_group('Command parameters')
32+
group.add_argument(
33+
'--input',
34+
type=file_or_url_type,
35+
default='-',
36+
help='Where to read YAML from',
37+
metavar='FILE_OR_URL',
38+
)
39+
group.add_argument(
40+
'path',
41+
nargs='?',
42+
type=existing_dir,
43+
default=os.curdir,
44+
help='Base path to look for repositories',
45+
)
46+
group.add_argument(
47+
'-f',
48+
'--force',
49+
action='store_true',
50+
default=False,
51+
help='Do the deletion instead of a dry-run',
52+
)
53+
return parser
54+
55+
56+
def get_repository_paths(input_source, base_path):
57+
"""Get repository paths from input source."""
58+
try:
59+
if isinstance(input_source, request.Request):
60+
input_source = request.urlopen(input_source)
61+
repos = get_repositories(input_source)
62+
return [os.path.join(base_path, rel_path) for rel_path in repos]
63+
except (RuntimeError, URLError) as e:
64+
raise RuntimeError(f'Failed to read repositories: {e}') from e
65+
66+
67+
def validate_paths(paths):
68+
"""Validate that paths exist and are directories."""
69+
valid_paths = []
70+
missing_paths = []
71+
72+
for path in paths:
73+
if os.path.exists(path) and os.path.isdir(path):
74+
valid_paths.append(path)
75+
else:
76+
missing_paths.append(path)
77+
78+
return valid_paths, missing_paths
79+
80+
81+
def main(args=None, stdout=None, stderr=None):
82+
"""Entry point for the 'delete' command."""
83+
84+
set_streams(stdout=stdout, stderr=stderr)
85+
parser = get_parser()
86+
args = parser.parse_args(args)
87+
88+
try:
89+
paths = get_repository_paths(args.input, args.path)
90+
except RuntimeError as e:
91+
print(ansi('redf') + f'Error: {e}' + ansi('reset'), file=sys.stderr)
92+
return 1
93+
94+
if not paths:
95+
print(
96+
ansi('yellowf') + 'No repositories found to delete' + ansi('reset'),
97+
file=sys.stderr,
98+
)
99+
return 0
100+
101+
# Validate paths existence
102+
valid_paths, missing_paths = validate_paths(paths)
103+
104+
if not valid_paths:
105+
print(
106+
ansi('redf') + 'No valid directories to delete.' + ansi('reset'),
107+
file=sys.stderr,
108+
)
109+
return 1
110+
else:
111+
if missing_paths:
112+
print(
113+
ansi('yellowf')
114+
+ 'Warning: The following directories do not exist:'
115+
+ ansi('reset'),
116+
file=sys.stderr,
117+
)
118+
for path in missing_paths:
119+
print(f' {path}', file=sys.stderr)
120+
121+
print(
122+
ansi('cyanf')
123+
+ 'The following directories will be deleted:'
124+
+ ansi('reset'),
125+
file=sys.stderr,
126+
)
127+
for path in valid_paths:
128+
print(f' {path}', file=sys.stderr)
129+
130+
if not args.force:
131+
print(
132+
ansi('yellowf')
133+
+ 'Dry-run mode: No directories were deleted. Use -f/--force to delete them.'
134+
+ ansi('reset'),
135+
file=sys.stderr,
136+
)
137+
return 0
138+
139+
# Actual deletion
140+
for path in valid_paths:
141+
try:
142+
rmtree(path)
143+
except OSError as e:
144+
print(
145+
ansi('redf') + f'Failed to delete {path}: {e}' + ansi('reset'),
146+
file=sys.stderr,
147+
)
148+
149+
150+
if __name__ == '__main__':
151+
sys.exit(main())

0 commit comments

Comments
 (0)