Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion cfbs/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def search_command(terms: List[str]):

from cfbs.cfbs_json import CFBSJson
from cfbs.cfbs_types import CFBSCommandExitCode, CFBSCommandGitResult
from cfbs.masterfiles.analyze import most_relevant_version
from cfbs.updates import ModuleUpdates, update_module
from cfbs.utils import (
CFBSNetworkError,
Expand Down Expand Up @@ -1211,7 +1212,54 @@ def cfbs_convert_git_commit(
% masterfiles_version
)
print(
"The next conversion step is to handle modified files and files from other versions of masterfiles."
"The next conversion step is to handle files from other versions of masterfiles."
)
if not prompt_user_yesno(non_interactive, "Do you want to continue?"):
raise CFBSExitError("User did not proceed, exiting.")
other_versions_files = analyzed_files.different
if len(other_versions_files) > 0:
print(
"The following files are taken from other versions of masterfiles (not %s):"
% masterfiles_version
)
other_versions_files = sorted(other_versions_files)
files_to_delete = []
for other_version_file, other_versions in other_versions_files:
# don't display all versions (which is an arbitrarily-shaped sequence), instead choose the most relevant one:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by arbitrarily-shaped sequence?

Copy link
Contributor Author

@jakub-nt jakub-nt Aug 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant that since the list of the multiple other versions might be incontiguous, it can't be displayed as a range, for example. So it's best to simply not display all the versions and instead choose one.

relevant_version_text = most_relevant_version(
other_versions, masterfiles_version
)
if len(other_versions) > 1:
relevant_version_text += ", ..."
print("-", other_version_file, "(%s)" % relevant_version_text)

files_to_delete.append(other_version_file)
print(
"This usually indicates that someone made mistakes during past upgrades and it's recommended to delete these."
)
print(
"Your policy set will include the versions from %s instead (if they exist)."
% masterfiles_version
)
print(
"(Deletions will be visible in Git history so you can review or revert later)."
)
if prompt_user_yesno(
non_interactive, "Delete files from other versions? (Recommended)"
):
print("Deleting %s files." % len(files_to_delete))
for file_d in files_to_delete:
rm(os.path.join(dir_name, file_d))

print(
"Creating Git commit with deletion of policy files from other versions..."
)
cfbs_convert_git_commit("Deleted policy files from other versions")
print("Done.", end=" ")
else:
print("There are no files from other versions of masterfiles.")
print(
"The next conversion step is to handle files which have custom modifications."
)
print("This is not implemented yet.")
return 0
Expand Down
47 changes: 46 additions & 1 deletion cfbs/masterfiles/analyze.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from collections import OrderedDict
import os
from typing import Iterable, List

from cfbs.utils import dict_sorted_by_key, file_sha256

Version = str
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could consider making this a class that inherrits string. You could overwrite the __lt__, __le__, etc. magic functions. Then you could use min() and max() directly on that type.

Copy link
Contributor Author

@jakub-nt jakub-nt Aug 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree, the intention was to make that change separately, though. Especially since other code uses functions using this type.



def initialize_vcf():
versions_dict = {"versions": {}}
Expand Down Expand Up @@ -141,5 +144,47 @@ def sort_versions(versions: list, reverse: bool = True):
)


def highest_version(versions):
def highest_version(versions: Iterable[Version]):
return max(versions, key=version_as_comparable_list, default=None)
Comment on lines +147 to 148
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ref the asserts and comments below;

Suggested change
def highest_version(versions: Iterable[Version]):
return max(versions, key=version_as_comparable_list, default=None)
def highest_version(versions: Iterable[Version]) -> Version:
result = max(versions, key=version_as_comparable_list, default=None)
assert result is not None, "highest_version() does not accept empty lists / iterables"
return result

And the same for lowest_version() - Then, the return type for these are simpler, and it's incorrect to call them with an empty list.



def lowest_version(versions: Iterable[Version]):
return min(versions, key=version_as_comparable_list, default=None)


def is_lower_version(version_a: Version, version_b: Version):
"""Returns `True` if and only if `version_a` is lower than `version_b`."""

return version_as_comparable_list(version_a) < version_as_comparable_list(version_b)


def most_relevant_version(
other_versions: List[Version], reference_version: Version
) -> Version:
"""
The most relevant version is the highest version among older other versions,
or if there are no older other versions, the lowest version among other versions.

`other_versions` is assumed to be non-empty and not contain `reference_version`."""
assert len(other_versions) > 0
assert reference_version not in other_versions

highest_other_version = highest_version(other_versions)
lowest_other_version = lowest_version(other_versions)
# Unfortunately, Pyright can not infer that these can't be `None` when `other_versions` is non-empty.
assert highest_other_version is not None
assert lowest_other_version is not None
Comment on lines +174 to +176
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These asserts are repeating, maybe it's more useful if we change highest_version and lowest_version to never return None, i.e. to not accept empty iterables? I checked the other places where highest_version() is used, and it doesn't seem like a bad change.


if is_lower_version(highest_other_version, reference_version):
# all other versions are older
return highest_other_version
if is_lower_version(reference_version, lowest_other_version):
# all other versions are newer
return lowest_other_version
# there are both older and newer versions
lower_other_versions = [
o_v for o_v in other_versions if is_lower_version(o_v, reference_version)
]
highest_lower = highest_version(lower_other_versions)
assert highest_lower is not None
return highest_lower