@@ -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,141 @@ 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+
465457 for name in to_remove :
466458 if name .startswith (SUPPORTED_URI_SCHEMES ):
467459 matches = _get_modules_by_url (name )
468460 if not matches :
469461 raise CFBSExitError ("Could not find module with URL '%s'" % name )
470462 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
463+ all_affected = _get_all_dependents_recursive (module ["name" ])
464+ all_affected .discard (module ["name" ])
465+ if all_affected :
466+ prompt_msg = (
467+ "Module '%s' is a dependency of the following module%s: %s.\n "
468+ "Removing '%s' will also remove %s.\n "
469+ "Do you wish to continue?"
470+ ) % (
471+ module ["name" ],
472+ "s" if len (all_affected ) > 1 else "" ,
473+ ", " .join (sorted (all_affected )),
474+ module ["name" ],
475+ "these modules" if len (all_affected ) > 1 else "this module" ,
476+ )
477+ else :
478+ prompt_msg = "Do you wish to remove '%s'?" % module ["name" ]
479+
480+ if prompt_user_yesno (
481+ config .non_interactive ,
482+ prompt_msg ,
483+ ):
484+ modules_to_remove .add (module ["name" ])
485+ modules_to_remove .update (all_affected )
486+ else :
487+ print ("Skipping removal of '%s'" % module ["name" ])
476488 else :
477489 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 :
490+ if not module :
485491 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 )
492+ continue
497493
498- num_lines = len (msg .strip ().splitlines ())
499- changes_made = num_lines > 0
500- deps = get_unused_dependancies (config )
494+ all_affected = _get_all_dependents_recursive (module ["name" ])
495+ all_affected .discard (module ["name" ])
496+ if all_affected :
497+ prompt_msg = (
498+ "Module '%s' is a dependency of the following module%s: %s.\n "
499+ "Removing '%s' will also remove %s.\n "
500+ "Do you wish to continue?"
501+ ) % (
502+ module ["name" ],
503+ "s" if len (all_affected ) > 1 else "" ,
504+ ", " .join (sorted (all_affected )),
505+ module ["name" ],
506+ "these modules" if len (all_affected ) > 1 else "this module" ,
507+ )
508+ else :
509+ prompt_msg = "Do you wish to remove '%s'?" % module ["name" ]
510+
511+ if prompt_user_yesno (
512+ config .non_interactive ,
513+ prompt_msg ,
514+ ):
515+ modules_to_remove .add (module ["name" ])
516+ modules_to_remove .update (all_affected )
517+ else :
518+ print ("Skipping removal of '%s'" % module ["name" ])
501519
520+ removed_modules = []
521+ msg = ""
522+ files = []
523+
524+ while modules_to_remove :
525+ for module_name in modules_to_remove :
526+ dependents_in_set = [
527+ d for d in _get_dependents (module_name ) if d in modules_to_remove
528+ ]
529+ if not dependents_in_set :
530+ module = _get_module_by_name (module_name )
531+ if module :
532+ print ("Removing module '%s'" % module_name )
533+ modules .remove (module )
534+ removed_modules .append (module )
535+ msg += "\n - Removed module '%s'" % module_name
536+ modules_to_remove .remove (module_name )
537+
538+ input_path = os .path .join ("." , module_name , "input.json" )
539+ if os .path .isfile (input_path ) and prompt_user_yesno (
540+ config .non_interactive ,
541+ "Module '%s' has input data '%s'. Do you want to remove it?"
542+ % (module_name , input_path ),
543+ default = "no" ,
544+ ):
545+ rm (input_path )
546+ files .append (input_path )
547+ msg += "\n - Removed input data for module '%s'" % module_name
548+ log .debug ("Deleted module data '%s'" % input_path )
549+ break
550+
551+ # Remove orphans
552+ deps = get_unused_dependancies (config )
502553 if deps :
503554 print (
504555 "The following modules were added as dependencies but are no longer needed:"
@@ -513,11 +564,14 @@ def _get_modules_by_url(name) -> list:
513564 config .non_interactive ,
514565 "Do you wish to remove these modules?" ,
515566 ):
516- num_lines += len (deps )
517567 for dep in sorted (deps , key = lambda x : x ["name" ]):
518568 msg += "\n - Removed module '%s'" % dep ["name" ]
519- num_removed += 1
520569 modules .remove (dep )
570+ removed_modules .append (dep )
571+
572+ num_removed = len (removed_modules )
573+ num_lines = len (msg .strip ().splitlines ())
574+ changes_made = num_lines > 0
521575
522576 if num_lines > 1 :
523577 msg = "Removed %d modules\n " % num_removed + msg
0 commit comments