Skip to content

Commit 1099149

Browse files
authored
Merge pull request #306 from SimonThalvorsen/CFE-3948
CFE-3948: Changed remove to either remove all dependencies, else nothing
2 parents 8b269de + 7bc74e6 commit 1099149

6 files changed

Lines changed: 230 additions & 57 deletions

File tree

cfbs/commands.py

Lines changed: 97 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ def search_command(terms: List[str]):
5050
import copy
5151
import logging as log
5252
import json
53-
import functools
5453
from typing import Callable, List, Optional, Union
5554
from collections import OrderedDict
5655
from cfbs.analyze import analyze_policyset
@@ -416,89 +415,127 @@ def remove_command(to_remove: List[str]):
416415
modules = config["build"]
417416

418417
def _get_dependents(dependency) -> list:
418+
"""Get all modules that depend on the given module"""
419419
if len(modules) < 2:
420420
return []
421-
422-
def reduce_dependencies(a, b):
423-
result_b = [b["name"]] if dependency in b.get("dependencies", []) else []
424-
if type(a) is list:
425-
return a + result_b
426-
else:
427-
return (
428-
[a["name"]] if dependency in a.get("dependencies", []) else []
429-
) + result_b
430-
431-
return functools.reduce(reduce_dependencies, modules)
421+
dependents = []
422+
for module in modules:
423+
if dependency in module.get("dependencies", []):
424+
dependents.append(module["name"])
425+
return dependents
426+
427+
def _get_all_dependents_recursive(module_name, visited=None) -> set:
428+
"""Recursively get all modules that depend on this module"""
429+
if visited is None:
430+
visited = set()
431+
if module_name in visited:
432+
return visited
433+
visited.add(module_name)
434+
435+
direct_dependents = _get_dependents(module_name)
436+
for dependent in direct_dependents:
437+
_get_all_dependents_recursive(dependent, visited)
438+
return visited
432439

433440
def _get_module_by_name(name) -> Union[dict, None]:
434441
if not name.startswith("./") and name.endswith(".cf") and os.path.exists(name):
435442
name = "./" + name
436-
437443
for module in modules:
438444
if module["name"] == name:
439445
return module
440446
return None
441447

442-
def _remove_module_user_prompt(module):
443-
dependents = _get_dependents(module["name"])
444-
return prompt_user_yesno(
445-
config.non_interactive,
446-
"Do you wish to remove '%s'?" % module["name"]
447-
+ (
448-
" (The module is a dependency of the following module%s: %s)"
449-
% ("s" if len(dependents) > 1 else "", ", ".join(dependents))
450-
if dependents
451-
else ""
452-
),
453-
)
454-
455448
def _get_modules_by_url(name) -> list:
456449
r = []
457450
for module in modules:
458451
if "url" in module and module["url"] == name:
459452
r.append(module)
460453
return r
461454

462-
num_removed = 0
463-
msg = ""
464-
files = []
455+
modules_to_remove = set()
456+
457+
removal_msg = lambda module, all_affected: (
458+
(
459+
"Module '%s' is a dependency of the following module%s: %s.\n"
460+
"Removing '%s' will also remove %s.\n"
461+
"Do you wish to continue?"
462+
)
463+
% (
464+
module["name"],
465+
"s" if len(all_affected) > 1 else "",
466+
", ".join(sorted(all_affected)),
467+
module["name"],
468+
"these modules" if len(all_affected) > 1 else "this module",
469+
)
470+
if all_affected
471+
else "Do you wish to remove '%s'?" % module["name"]
472+
)
473+
465474
for name in to_remove:
466475
if name.startswith(SUPPORTED_URI_SCHEMES):
467476
matches = _get_modules_by_url(name)
468477
if not matches:
469478
raise CFBSExitError("Could not find module with URL '%s'" % name)
470479
for module in matches:
471-
if _remove_module_user_prompt(module):
472-
print("Removing module '%s'" % module["name"])
473-
modules.remove(module)
474-
msg += "\n - Removed module '%s'" % module["name"]
475-
num_removed += 1
480+
all_affected = _get_all_dependents_recursive(module["name"])
481+
all_affected.discard(module["name"])
482+
prompt_msg = removal_msg(module, all_affected)
483+
484+
if prompt_user_yesno(
485+
config.non_interactive,
486+
prompt_msg,
487+
):
488+
modules_to_remove.add(module["name"])
489+
modules_to_remove.update(all_affected)
490+
else:
491+
print("Skipping removal of '%s'" % module["name"])
476492
else:
477493
module = _get_module_by_name(name)
478-
if module:
479-
if _remove_module_user_prompt(module):
480-
print("Removing module '%s'" % name)
481-
modules.remove(module)
482-
msg += "\n - Removed module '%s'" % module["name"]
483-
num_removed += 1
484-
else:
494+
if not module:
485495
print("Module '%s' not found" % name)
486-
input_path = os.path.join(".", name, "input.json")
487-
if os.path.isfile(input_path) and prompt_user_yesno(
488-
config.non_interactive,
489-
"Module '%s' has input data '%s'. Do you want to remove it?"
490-
% (name, input_path),
491-
default="no",
492-
):
493-
rm(input_path)
494-
files.append(input_path)
495-
msg += "\n - Removed input data for module '%s'" % name
496-
log.debug("Deleted module data '%s'" % input_path)
496+
continue
497497

498-
num_lines = len(msg.strip().splitlines())
499-
changes_made = num_lines > 0
500-
deps = get_unused_dependancies(config)
498+
all_affected = _get_all_dependents_recursive(module["name"])
499+
all_affected.discard(module["name"])
500+
prompt_msg = removal_msg(module, all_affected)
501+
502+
if prompt_user_yesno(
503+
config.non_interactive,
504+
prompt_msg,
505+
):
506+
modules_to_remove.add(module["name"])
507+
modules_to_remove.update(all_affected)
508+
else:
509+
print("Skipping removal of '%s'" % module["name"])
510+
511+
removed_modules = []
512+
msg = ""
513+
files = []
514+
515+
while modules_to_remove:
516+
for module_name in list(modules_to_remove):
517+
module = _get_module_by_name(module_name)
518+
if module:
519+
print("Removing module '%s'" % module_name)
520+
modules.remove(module)
521+
removed_modules.append(module)
522+
msg += "\n - Removed module '%s'" % module_name
523+
modules_to_remove.remove(module_name)
524+
525+
input_path = os.path.join(".", module_name, "input.json")
526+
if os.path.isfile(input_path) and prompt_user_yesno(
527+
config.non_interactive,
528+
"Module '%s' has input data '%s'. Do you want to remove it?"
529+
% (module_name, input_path),
530+
default="no",
531+
):
532+
rm(input_path)
533+
files.append(input_path)
534+
msg += "\n - Removed input data for module '%s'" % module_name
535+
log.debug("Deleted module data '%s'" % input_path)
501536

537+
# Remove orphans
538+
deps = get_unused_dependancies(config)
502539
if deps:
503540
print(
504541
"The following modules were added as dependencies but are no longer needed:"
@@ -513,11 +550,14 @@ def _get_modules_by_url(name) -> list:
513550
config.non_interactive,
514551
"Do you wish to remove these modules?",
515552
):
516-
num_lines += len(deps)
517553
for dep in sorted(deps, key=lambda x: x["name"]):
518554
msg += "\n - Removed module '%s'" % dep["name"]
519-
num_removed += 1
520555
modules.remove(dep)
556+
removed_modules.append(dep)
557+
558+
num_removed = len(removed_modules)
559+
num_lines = len(msg.strip().splitlines())
560+
changes_made = num_lines > 0
521561

522562
if num_lines > 1:
523563
msg = "Removed %d modules\n" % num_removed + msg
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
set -e
2+
set -x
3+
cd tests/
4+
mkdir -p ./tmp/
5+
cd ./tmp/
6+
touch cfbs.json && rm cfbs.json
7+
rm -rf .git
8+
9+
cp ../shell/048_remove_with_dependencies/example-cfbs.json cfbs.json
10+
cfbs validate
11+
12+
grep '"name": "example-module"' cfbs.json
13+
grep '"name": "example-dependency"' cfbs.json
14+
15+
cfbs --non-interactive remove example-module --non-interactive
16+
cfbs validate
17+
18+
! grep '"name": "example-module"' cfbs.json
19+
! grep '"name": "example-dependency"' cfbs.json
20+
21+
22+
23+
cp ../shell/048_remove_with_dependencies/example-cfbs.json cfbs.json
24+
cfbs validate
25+
26+
grep '"name": "example-module"' cfbs.json
27+
grep '"name": "example-dependency"' cfbs.json
28+
29+
cfbs --non-interactive remove example-dependency --non-interactive
30+
cfbs validate
31+
32+
grep '"name": "example-module"' cfbs.json
33+
! grep '"name": "example-dependency"' cfbs.json
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "example-project",
3+
"description": "Example description",
4+
"type": "policy-set",
5+
"build": [
6+
{
7+
"name": "masterfiles-example-module",
8+
"description": "Example",
9+
"url": "https://github.com/example/",
10+
"commit": "0000000000000000000000000000000000000000",
11+
"added_by": "cfbs add",
12+
"steps": ["run this"]
13+
},
14+
{
15+
"name": "example-module",
16+
"description": "Example",
17+
"url": "https://github.com/example/",
18+
"commit": "0000000000000000000000000000000000000000",
19+
"added_by": "cfbs add",
20+
"steps": ["delete this"]
21+
},
22+
{
23+
"name": "example-dependency",
24+
"description": "Example",
25+
"url": "https://github.com/example/",
26+
"commit": "0000000000000000000000000000000000000000",
27+
"dependencies": ["example-module"],
28+
"added_by": "cfbs add",
29+
"steps": ["delete this"]
30+
}
31+
]
32+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
set -e
2+
set -x
3+
cd tests/
4+
mkdir -p ./tmp/
5+
cd ./tmp/
6+
touch cfbs.json && rm cfbs.json
7+
rm -rf .git
8+
9+
cp ../shell/049_remove_with_circular_dependencies/example-cfbs.json cfbs.json
10+
cfbs validate
11+
12+
grep '"name": "example-module"' cfbs.json
13+
grep '"name": "example-dependency"' cfbs.json
14+
15+
cfbs --non-interactive remove example-module --non-interactive
16+
cfbs validate
17+
18+
! grep '"name": "example-module"' cfbs.json
19+
! grep '"name": "example-dependency"' cfbs.json
20+
21+
22+
23+
cp ../shell/049_remove_with_circular_dependencies/example-cfbs.json cfbs.json
24+
cfbs validate
25+
26+
grep '"name": "example-module"' cfbs.json
27+
grep '"name": "example-dependency"' cfbs.json
28+
29+
cfbs --non-interactive remove example-dependency --non-interactive
30+
cfbs validate
31+
32+
! grep '"name": "example-module"' cfbs.json
33+
! grep '"name": "example-dependency"' cfbs.json
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "example-project",
3+
"description": "Example description",
4+
"type": "policy-set",
5+
"build": [
6+
{
7+
"name": "masterfiles-example-module",
8+
"description": "Example",
9+
"url": "https://github.com/example/",
10+
"commit": "0000000000000000000000000000000000000000",
11+
"added_by": "cfbs add",
12+
"steps": ["run this"]
13+
},
14+
{
15+
"name": "example-module",
16+
"description": "Example",
17+
"url": "https://github.com/example/",
18+
"commit": "0000000000000000000000000000000000000000",
19+
"dependencies": ["example-dependency"],
20+
"added_by": "cfbs add",
21+
"steps": ["delete this"]
22+
},
23+
{
24+
"name": "example-dependency",
25+
"description": "Example",
26+
"url": "https://github.com/example/",
27+
"commit": "0000000000000000000000000000000000000000",
28+
"dependencies": ["example-module"],
29+
"added_by": "cfbs add",
30+
"steps": ["delete this"]
31+
}
32+
]
33+
}

tests/shell/all.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ run_test tests/shell/044_replace.sh
9191
run_test tests/shell/045_update_from_url_branch_uptodate.sh
9292
run_test tests/shell/046_update_from_url_branch.sh
9393
run_test tests/shell/047_absolute_path_modules.sh
94+
run_test tests/shell/048_remove_with_dependencies.sh
95+
run_test tests/shell/049_remove_with_circular_dependencies.sh
9496

9597
# Summary
9698
_suite_end=$(date +%s)

0 commit comments

Comments
 (0)