diff --git a/README.md b/README.md index e7b4485b..4337b55f 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,7 @@ These commands are centered around a user making changes to a project (manually - `cfbs analyse`: Same as `cfbs analyze`. - `cfbs analyze`: Analyze the policy set specified by the given path. - `cfbs clean`: Remove modules which were added as dependencies, but are no longer needed. +- `cfbs convert`: Initialize a new CFEngine Build project based on an existing policy set. - `cfbs help`: Print the help menu. - `cfbs info`: Print information about a module. - `cfbs init`: Initialize a new CFEngine Build project. diff --git a/cfbs/analyze.py b/cfbs/analyze.py index 1e18bbac..1784d3b9 100644 --- a/cfbs/analyze.py +++ b/cfbs/analyze.py @@ -355,6 +355,7 @@ class AnalyzedFiles: def __init__(self, reference_version: Union[str, None]): self.reference_version = reference_version + self.unmodified = [] self.missing = [] self.modified = [] self.moved_or_renamed = [] @@ -373,6 +374,10 @@ def _denormalize_origin(origin, is_parentpath, masterfiles_dir): def denormalize(self, is_parentpath, masterfiles_dir): """Currently irreversible and meant to only be used once after all the files are analyzed.""" + self.unmodified = [ + mpf_denormalized_path(file, is_parentpath, masterfiles_dir) + for file in self.unmodified + ] self.missing = [ mpf_denormalized_path(file, is_parentpath, masterfiles_dir) for file in self.missing @@ -420,6 +425,7 @@ def denormalize(self, is_parentpath, masterfiles_dir): ] def sort(self): + self.unmodified = filepaths_sorted(self.unmodified) self.missing = filepaths_sorted(self.missing) self.modified = filepaths_sorted(self.modified) self.moved_or_renamed = filepaths_sorted(self.moved_or_renamed) @@ -430,9 +436,14 @@ def sort(self): ) self.not_from_any = filepaths_sorted(self.not_from_any) - def display(self): + def display(self, display_unmodified=False): print("Reference version:", self.reference_version, "\n") + if display_unmodified: + if len(self.unmodified) > 0: + print("Files unmodified from the version:") + filepaths_display(self.unmodified) + if len(self.missing) > 0: print("Files missing from the version:") elif self.reference_version is not None: @@ -479,6 +490,7 @@ def to_json_dict(self): json_dict["files"] = {} + json_dict["files"]["unmodified"] = self.unmodified json_dict["files"]["missing"] = self.missing json_dict["files"]["modified"] = self.modified json_dict["files"]["moved_or_renamed"] = self.moved_or_renamed @@ -682,6 +694,9 @@ def analyze_policyset( other_versions = mpf_checksums_dict[checksum][filepath] # since MPF data is sorted, so is `other_versions` analyzed_files.different.append((filepath, other_versions)) + else: + # 1A1B. the file is unmodified and present in the reference version + analyzed_files.unmodified.append(filepath) else: # 1A2. checksum is known but there's no matching filepath with that checksum: # therefore, it must be a rename/move diff --git a/cfbs/args.py b/cfbs/args.py index c25453cf..a90e3520 100644 --- a/cfbs/args.py +++ b/cfbs/args.py @@ -196,7 +196,7 @@ def get_arg_parser(whitespace_for_manual=False): ) parser.add_argument( "--offline", - help="Do not connect to the Internet to download the latest version of MPF release information during 'cfbs analyze'", + help="Do not connect to the Internet to download the latest version of MPF release information during 'cfbs analyze' and 'cfbs convert'", action="store_true", ) parser.add_argument( diff --git a/cfbs/cfbs.1 b/cfbs/cfbs.1 index 700b79e5..5001d286 100644 --- a/cfbs/cfbs.1 +++ b/cfbs/cfbs.1 @@ -9,7 +9,8 @@ CFEngine Build System. .TP \fBcmd\fR -The command to perform (pretty, init, status, search, add, remove, clean, update, validate, download, build, install, help, info, show, analyse, analyze, input, set\-input, get\-input, generate\-release\-information) +The command to perform (pretty, init, status, search, add, remove, clean, update, validate, download, build, install, help, info, show, +analyse, analyze, convert, input, set\-input, get\-input, generate\-release\-information) .TP \fBargs\fR diff --git a/cfbs/commands.py b/cfbs/commands.py index 923d73be..773fb66e 100644 --- a/cfbs/commands.py +++ b/cfbs/commands.py @@ -55,6 +55,7 @@ def search_command(terms: List[str]): from collections import OrderedDict from cfbs.analyze import analyze_policyset from cfbs.args import get_args +from typing import Iterable from cfbs.cfbs_json import CFBSJson from cfbs.cfbs_types import CFBSCommandExitCode, CFBSCommandGitResult @@ -92,6 +93,7 @@ def search_command(terms: List[str]): from cfbs.validate import ( validate_config, validate_config_raise_exceptions, + validate_module_name_content, validate_single_module, ) from cfbs.internal_file_management import ( @@ -1087,6 +1089,134 @@ def analyze_command( return 0 +@cfbs_command("convert") +def convert_command(non_interactive=False, offline=False): + def cfbs_convert_cleanup(): + os.unlink(cfbs_filename()) + rm(".git", missing_ok=True) + + def cfbs_convert_git_commit( + commit_message: str, add_scope: Union[str, Iterable[str]] = "all" + ): + try: + git_commit_maybe_prompt(commit_message, non_interactive, scope=add_scope) + except CFBSGitError: + cfbs_convert_cleanup() + raise + + dir_content = [f.name for f in os.scandir(".")] + + if not (len(dir_content) == 1 and dir_content[0].startswith("masterfiles-")): + raise CFBSUserError( + "cfbs convert must be run in a directory containing only one item, a subdirectory named masterfiles-" + ) + + dir_name = dir_content[0] + path_string = "./" + dir_name + "/" + + # validate the local module + validate_module_name_content(path_string) + + promises_cf_path = os.path.join(dir_name, "promises.cf") + if not os.path.isfile(promises_cf_path): + raise CFBSUserError( + "The file '" + + promises_cf_path + + "' does not exist - make sure '" + + path_string + + "' is a policy set based on masterfiles." + ) + + print( + "Found policy set in '%s' with 'promises.cf' in the expected location." + % path_string + ) + + print("Analyzing '" + path_string + "'...") + analyzed_files, _ = analyze_policyset( + path=dir_name, + is_parentpath=False, + reference_version=None, + masterfiles_dir=dir_name, + ignored_path_components=None, + offline=offline, + ) + + current_index = CFBSConfig.get_instance().index + default_version = current_index.get_module_object("masterfiles")["version"] + + reference_version = analyzed_files.reference_version + if reference_version is None: + print( + "Did not detect any version of masterfiles, proceeding using the default version (%s)." + % default_version + ) + masterfiles_version = default_version + else: + print("Detected version %s of masterfiles." % reference_version) + masterfiles_version = reference_version + + if not prompt_user_yesno( + non_interactive, + "Do you want to continue making a new CFEngine Build project based on masterfiles %s?" + % masterfiles_version, + ): + raise CFBSExitError("User did not proceed, exiting.") + + print("Initializing a new CFBS project...") + # since there should be no other files than the masterfiles-name directory, there shouldn't be a .git directory + assert not is_git_repo() + r = init_command(masterfiles="no", non_interactive=non_interactive, use_git=True) + # the cfbs-init should've created a Git repository + assert is_git_repo() + if r != 0: + print("Initializing a new CFBS project failed, aborting conversion.") + cfbs_convert_cleanup() + return r + + print("Adding masterfiles %s to the project..." % masterfiles_version) + masterfiles_to_add = ["masterfiles@%s" % masterfiles_version] + r = add_command(masterfiles_to_add, added_by="cfbs convert") + if r != 0: + print("Adding the masterfiles module failed, aborting conversion.") + cfbs_convert_cleanup() + return r + + print("Adding the policy files...") + local_module_to_add = [path_string] + r = add_command( + local_module_to_add, + added_by="cfbs convert", + explicit_build_steps=["copy ./ ./"], + ) + if r != 0: + print("Adding the policy files module failed, aborting conversion.") + cfbs_convert_cleanup() + return r + + # here, matching files are files that have identical (filepath, checksum) + if len(analyzed_files.unmodified) != 0: + print( + "Deleting matching files between masterfiles %s and '%s'..." + % (masterfiles_version, path_string) + ) + for unmodified_mpf_file in analyzed_files.unmodified: + rm(os.path.join(dir_name, unmodified_mpf_file)) + + print("Creating Git commit...") + cfbs_convert_git_commit("Deleted unmodified policy files") + + print( + "Your project is now functional, can be built, and will produce a version of masterfiles %s with your modifications." + % masterfiles_version + ) + print( + "The next conversion step is to handle modified files and files from other versions of masterfiles." + ) + print("This is not implemented yet.") + return 0 + + @cfbs_command("input") @commit_after_command("Added input for module%s", [PLURAL_S]) def input_command(args, input_from="cfbs input"): diff --git a/cfbs/main.py b/cfbs/main.py index ebcef324..92c225b7 100644 --- a/cfbs/main.py +++ b/cfbs/main.py @@ -132,7 +132,7 @@ def _main() -> int: % args.command ) - if args.offline and args.command not in ("analyze", "analyse"): + if args.offline and args.command not in ("analyze", "analyse", "convert"): raise CFBSUserError( "The option --offline is only for 'cfbs analyze', not 'cfbs %s'" % args.command @@ -184,6 +184,8 @@ def _main() -> int: args.offline, does_log_info(args.loglevel), ) + if args.command == "convert": + return commands.convert_command(args.non_interactive, args.offline) if args.command == "generate-release-information": return commands.generate_release_information_command( diff --git a/cfbs/module.py b/cfbs/module.py index af78958b..8cd27bb8 100644 --- a/cfbs/module.py +++ b/cfbs/module.py @@ -4,7 +4,7 @@ def is_module_added_manually(added_by: str): - return added_by in ("cfbs add", "cfbs init") + return added_by in ("cfbs add", "cfbs init", "cfbs convert") def is_module_local(name: str):