@@ -50,7 +50,6 @@ def search_command(terms: List[str]):
5050import copy
5151import logging as log
5252import json
53- import functools
5453from typing import Callable , List , Optional , Union
5554from collections import OrderedDict
5655from 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
0 commit comments