Coverage for little_loops / cli / migrate_status.py: 17%

64 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-06-26 17:38 -0500

1"""ll-migrate-status: Normalize non-canonical status: values in issue files to canonical ones.""" 

2 

3from __future__ import annotations 

4 

5import argparse 

6import re 

7import sys 

8from pathlib import Path 

9 

10from little_loops.cli_args import add_config_arg, add_dry_run_arg 

11from little_loops.frontmatter import STATUS_SYNONYMS 

12from little_loops.session_store import DEFAULT_DB_PATH, cli_event_context 

13 

14_STATUS_FIELD_RE = re.compile(r"^(status: )(.+)$", re.MULTILINE) 

15 

16 

17def _migrate_content(content: str) -> tuple[str, list[str]]: 

18 """Normalize status field in frontmatter. Returns (updated_content, list_of_changes).""" 

19 changes: list[str] = [] 

20 

21 def _replace(m: re.Match[str]) -> str: 

22 prefix, value = m.group(1), m.group(2) 

23 canonical = STATUS_SYNONYMS.get(value, value) 

24 if canonical != value: 

25 changes.append(f"status: {value!r} → status: {canonical!r}") 

26 return f"{prefix}{canonical}" 

27 return m.group(0) 

28 

29 updated = _STATUS_FIELD_RE.sub(_replace, content) 

30 return updated, changes 

31 

32 

33def main_migrate_status() -> int: 

34 """Entry point for ll-migrate-status command. 

35 

36 Normalizes non-canonical status: values in all .issues/**/*.md files to their 

37 canonical equivalents using STATUS_SYNONYMS from frontmatter.py. 

38 

39 Returns: 

40 Exit code (0 = success, 1 = error) 

41 """ 

42 with cli_event_context(DEFAULT_DB_PATH, "ll-migrate-status", sys.argv[1:]): 

43 parser = argparse.ArgumentParser( 

44 prog="ll-migrate-status", 

45 description=( 

46 "Normalize non-canonical status: frontmatter values to canonical ones " 

47 "(e.g. 'completed' → 'done', 'wip' → 'in_progress'). " 

48 "One-time migration for ENH-1551." 

49 ), 

50 formatter_class=argparse.RawDescriptionHelpFormatter, 

51 epilog=""" 

52Examples: 

53 %(prog)s --dry-run # Preview all planned normalizations (strongly advised first) 

54 %(prog)s # Execute migration 

55""", 

56 ) 

57 add_dry_run_arg(parser) 

58 add_config_arg(parser) 

59 args = parser.parse_args() 

60 

61 dry_run: bool = args.dry_run 

62 repo_root: Path = args.config or Path.cwd() 

63 

64 issues_dir = repo_root / ".issues" 

65 if not issues_dir.exists(): 

66 print(f"No .issues/ directory found at {repo_root}") 

67 return 1 

68 

69 if dry_run: 

70 print("[DRY RUN] No files will be modified.") 

71 

72 normalized = 0 

73 errors: list[str] = [] 

74 

75 for file_path in sorted(issues_dir.rglob("*.md")): 

76 try: 

77 content = file_path.read_text(encoding="utf-8") 

78 except OSError as exc: 

79 errors.append(str(file_path)) 

80 print(f" [ERROR] {file_path}: {exc}") 

81 continue 

82 

83 updated, changes = _migrate_content(content) 

84 if not changes: 

85 continue 

86 

87 prefix = "[DRY RUN] " if dry_run else "" 

88 rel = file_path.relative_to(repo_root) 

89 for change in changes: 

90 print(f" {prefix}NORMALIZE {rel}: {change}") 

91 

92 if not dry_run: 

93 try: 

94 file_path.write_text(updated, encoding="utf-8") 

95 normalized += 1 

96 except OSError as exc: 

97 errors.append(str(file_path)) 

98 print(f" [ERROR] {file_path}: {exc}") 

99 else: 

100 normalized += 1 

101 

102 print() 

103 print(f"Results: {normalized} files {'would be ' if dry_run else ''}updated.") 

104 if errors: 

105 print(f" Errors: {len(errors)}") 

106 return 1 

107 return 0