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
16 changes: 15 additions & 1 deletion cvelib/wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,18 @@ def _getHighestSeverity(alerts: List[Dict[str, Any]]) -> str:
return priority


def _repoGroupSortKey(item: Tuple[str, Dict[str, Any]]) -> Tuple[int, str]:
"""Sort key for grouped repo items keyed by 'org/repo'.

Repos whose name starts with 'dependabot-for-' sort after all others,
preserving lexicographic order within each group.
"""
repo_key, _ = item
repo_name: str = repo_key.split("/", 1)[-1]
is_dependabot_for: bool = repo_name.startswith("dependabot-for-")
return (1 if is_dependabot_for else 0, repo_key)


def _groupAlertsByRepo(alerts_data: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
"""Group alerts by repository"""
grouped: Dict[str, Dict[str, Any]] = {}
Expand Down Expand Up @@ -1498,7 +1510,9 @@ def _runWizard(
print(f"Found alerts for {len(grouped)} repository(ies)")

# Process each repo
for i, (repo_key, repo_data) in enumerate(sorted(grouped.items()), 1):
for i, (repo_key, repo_data) in enumerate(
sorted(grouped.items(), key=_repoGroupSortKey), 1
):
org: str = repo_data["org"]
repo: str = repo_data["repo"]

Expand Down
76 changes: 76 additions & 0 deletions tests/test_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -1306,6 +1306,82 @@ def test__groupAlertsByRepo_edge_cases(self):
self.assertEqual(len(result), 1)
self.assertIn("test-org/single-repo", result)

def test__repoGroupSortKey(self):
"""Test _repoGroupSortKey assigns the correct bucket prefix"""
# Non-dependabot-for repo: bucket 0
self.assertEqual(
cvelib.wizard._repoGroupSortKey(("org/corge", {})),
(0, "org/corge"),
)
# dependabot-for-* repo: bucket 1
self.assertEqual(
cvelib.wizard._repoGroupSortKey(("org/dependabot-for-alpha", {})),
(1, "org/dependabot-for-alpha"),
)
# 'dependabot-for-' string anywhere other than the start of the repo
# name must not match
self.assertEqual(
cvelib.wizard._repoGroupSortKey(("org/my-dependabot-for-thing", {})),
(0, "org/my-dependabot-for-thing"),
)
# Org prefix containing 'dependabot-for-' must not match (split is on
# the first '/' so the org is excluded from the check)
self.assertEqual(
cvelib.wizard._repoGroupSortKey(("dependabot-for-org/corge", {})),
(0, "dependabot-for-org/corge"),
)
# Key without an org prefix still works
self.assertEqual(
cvelib.wizard._repoGroupSortKey(("dependabot-for-alpha", {})),
(1, "dependabot-for-alpha"),
)

def test__repoGroupSortKey_sorting_order(self):
"""Test _repoGroupSortKey produces the expected end-to-end order"""
grouped = {
"org/corge": {},
"org/dapper": {},
"org/dependabot-for-alpha": {},
"org/dependabot-for-beta": {},
"org/diva": {},
"org/earnest": {},
}
ordered = [
k for k, _ in sorted(grouped.items(), key=cvelib.wizard._repoGroupSortKey)
]
self.assertEqual(
ordered,
[
"org/corge",
"org/dapper",
"org/diva",
"org/earnest",
"org/dependabot-for-alpha",
"org/dependabot-for-beta",
],
)

def test__repoGroupSortKey_sorting_order_mixed_orgs(self):
"""Test _repoGroupSortKey buckets dominate across different orgs"""
grouped = {
"acme/corge": {},
"zeta/dapper": {},
"acme/dependabot-for-alpha": {},
"zeta/dependabot-for-beta": {},
}
ordered = [
k for k, _ in sorted(grouped.items(), key=cvelib.wizard._repoGroupSortKey)
]
self.assertEqual(
ordered,
[
"acme/corge",
"zeta/dapper",
"acme/dependabot-for-alpha",
"zeta/dependabot-for-beta",
],
)

@mock.patch("subprocess.run")
def test__isGhCliAvailable_failure(self, mock_subprocess):
"""Test is_gh_cli_available when gh command fails"""
Expand Down
Loading