Skip to content

Commit f2f8807

Browse files
aclark4lifeCopilot
andcommitted
spec status: flag patches whose source files no longer exist in specs repo
Add `_find_removable_patches()` helper that checks each active patch file against the upstream specifications repo. A patch is flagged as removable when all the driver-side test files it references no longer exist in the specs source — meaning they've been deleted or renamed upstream and the patch is no longer needed after the next sync. `dbx spec status` now prints a 🗑 section listing any such patches with suggested `rm` commands, and includes verbose mode support to show the specific missing files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2548800 commit f2f8807

2 files changed

Lines changed: 157 additions & 0 deletions

File tree

src/dbx_python_cli/commands/spec.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,62 @@ def _branch_summary(repo_path: Path) -> tuple[list[str], int]:
484484
return [], 0
485485

486486

487+
def _find_removable_patches(
488+
patch_dir: Path,
489+
spec_map: dict[str, list[tuple[str, str]]],
490+
specs_source: Path,
491+
) -> list[tuple[Path, list[str]]]:
492+
"""Return patches whose source files no longer exist in the specs repo.
493+
494+
For each patch, maps driver-side paths (``test/<driver_dst>/...``) back to
495+
the corresponding specs-source paths using *spec_map*, then checks whether
496+
any of those source files are still present. A patch is considered
497+
removable when **none** of its referenced source files exist upstream —
498+
meaning after the next sync those files won't be copied in and there is
499+
nothing left to patch out.
500+
501+
Returns a list of ``(patch_path, [missing_file, ...])`` tuples.
502+
"""
503+
# Build reverse map: driver_dst -> specs_src (many-to-many, take first match)
504+
driver_to_specs: dict[str, str] = {}
505+
for mappings in spec_map.values():
506+
for specs_src, driver_dst in mappings:
507+
driver_to_specs.setdefault(driver_dst, specs_src)
508+
509+
removable: list[tuple[Path, list[str]]] = []
510+
for patch_path in _list_patches(patch_dir):
511+
patched_files = _parse_patch_files(patch_path)
512+
if not patched_files:
513+
continue
514+
515+
missing: list[str] = []
516+
for file_path in patched_files:
517+
parts = file_path.split("/")
518+
if len(parts) < 3 or parts[0] != "test":
519+
continue
520+
driver_dst = parts[1]
521+
rel_path = "/".join(parts[2:])
522+
specs_src = driver_to_specs.get(driver_dst)
523+
if specs_src is None:
524+
# Can't resolve — treat as missing to be conservative
525+
missing.append(file_path)
526+
continue
527+
src_file = specs_source / specs_src / rel_path
528+
if not src_file.exists():
529+
missing.append(file_path)
530+
531+
if missing and len(missing) == len(
532+
[
533+
f
534+
for f in patched_files
535+
if len(f.split("/")) >= 3 and f.split("/")[0] == "test"
536+
]
537+
):
538+
removable.append((patch_path, missing))
539+
540+
return removable
541+
542+
487543
def _spec_is_stale(
488544
specs_source: Path,
489545
driver_test: Path,
@@ -700,6 +756,29 @@ def _patches_for_spec(spec_name: str) -> list[str]:
700756
# --- Patches ----------------------------------------------------------- #
701757
patch_count = _show_patch_summary(driver_repo, verbose)
702758

759+
# --- Removable patches ------------------------------------------------- #
760+
removable = _find_removable_patches(
761+
_get_patch_dir(driver_repo), spec_map, specs_source
762+
)
763+
if removable:
764+
typer.echo(
765+
f"\n 🗑 {len(removable)} patch(es) may be removable"
766+
" (source files no longer exist in specs repo):\n"
767+
)
768+
for patch_path, missing_files in removable:
769+
typer.echo(f" • {patch_path.name}")
770+
if verbose:
771+
for f in missing_files:
772+
typer.echo(f" {f} [not found in specs]")
773+
typer.echo(
774+
"\n ℹ These patches target files that are no longer in the upstream specs."
775+
)
776+
typer.echo(
777+
" Review and remove them if the ticket has been resolved upstream:"
778+
)
779+
for patch_path, _ in removable:
780+
typer.echo(f" rm {patch_path}")
781+
703782
# --- What to do next --------------------------------------------------- #
704783
typer.echo("\n 🔍 To verify locally:\n")
705784
step = 1

tests/test_spec_command.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -954,3 +954,81 @@ def test_spec_status_annotates_patches(tmp_path):
954954
assert result.exit_code == 0
955955
assert "PYTHON-9999" in result.output
956956
assert "🩹" in result.output
957+
958+
959+
def test_spec_status_shows_removable_patches(tmp_path):
960+
"""Patches whose source files no longer exist in specs should be flagged."""
961+
_, cfg = _make_status_repos(tmp_path, content_same=True)
962+
963+
patch_dir = tmp_path / "repos" / "mongo-python-driver" / ".evergreen" / "spec-patch"
964+
# This patch references a file that does NOT exist in the specs repo
965+
(patch_dir / "PYTHON-GONE.patch").write_text(
966+
"diff --git a/test/auth/deleted-test.json b/test/auth/deleted-test.json\n"
967+
"index abc..def 100644\n"
968+
"--- a/test/auth/deleted-test.json\n"
969+
"+++ /dev/null\n"
970+
"@@ -1 +0,0 @@\n"
971+
"-{}\n"
972+
)
973+
974+
with (
975+
patch("dbx_python_cli.utils.repo.get_config_path", return_value=cfg),
976+
patch(
977+
"dbx_python_cli.commands.spec._get_current_branch",
978+
return_value="spec-resync-test",
979+
),
980+
patch(
981+
"dbx_python_cli.commands.spec._find_recent_resync_commits",
982+
return_value=["abc resyncing"],
983+
),
984+
patch(
985+
"dbx_python_cli.commands.spec._commit_relative_date",
986+
return_value="1 day ago",
987+
),
988+
patch("dbx_python_cli.commands.spec._commit_spec_dirs", return_value=[]),
989+
patch("dbx_python_cli.commands.spec._branch_summary", return_value=([], 0)),
990+
):
991+
result = runner.invoke(app, ["spec", "status"])
992+
993+
assert result.exit_code == 0
994+
assert "PYTHON-GONE.patch" in result.output
995+
assert "removable" in result.output.lower()
996+
997+
998+
def test_spec_status_no_removable_patches_when_files_exist(tmp_path):
999+
"""Patches whose source files still exist should NOT be flagged as removable."""
1000+
_, cfg = _make_status_repos(tmp_path, content_same=True)
1001+
1002+
patch_dir = tmp_path / "repos" / "mongo-python-driver" / ".evergreen" / "spec-patch"
1003+
# This patch references auth.json which DOES exist in the specs repo
1004+
(patch_dir / "PYTHON-KEEP.patch").write_text(
1005+
"diff --git a/test/auth/auth.json b/test/auth/auth.json\n"
1006+
"index abc..def 100644\n"
1007+
"--- a/test/auth/auth.json\n"
1008+
"+++ /dev/null\n"
1009+
"@@ -1 +0,0 @@\n"
1010+
'-{"spec": "auth"}\n'
1011+
)
1012+
1013+
with (
1014+
patch("dbx_python_cli.utils.repo.get_config_path", return_value=cfg),
1015+
patch(
1016+
"dbx_python_cli.commands.spec._get_current_branch",
1017+
return_value="spec-resync-test",
1018+
),
1019+
patch(
1020+
"dbx_python_cli.commands.spec._find_recent_resync_commits",
1021+
return_value=["abc resyncing"],
1022+
),
1023+
patch(
1024+
"dbx_python_cli.commands.spec._commit_relative_date",
1025+
return_value="1 day ago",
1026+
),
1027+
patch("dbx_python_cli.commands.spec._commit_spec_dirs", return_value=[]),
1028+
patch("dbx_python_cli.commands.spec._branch_summary", return_value=([], 0)),
1029+
):
1030+
result = runner.invoke(app, ["spec", "status"])
1031+
1032+
assert result.exit_code == 0
1033+
assert "may be removable" not in result.output
1034+
assert "PYTHON-KEEP.patch" not in result.output

0 commit comments

Comments
 (0)