Coverage for little_loops / cli / gitignore.py: 23%

44 statements  

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

1"""ll-gitignore: Suggest and apply .gitignore patterns.""" 

2 

3from __future__ import annotations 

4 

5import argparse 

6import sys 

7from pathlib import Path 

8 

9from little_loops.cli.output import print_json 

10from little_loops.cli_args import add_config_arg, add_dry_run_arg, add_json_arg, add_quiet_arg 

11from little_loops.git_operations import ( 

12 GitignorePattern, 

13 add_patterns_to_gitignore, 

14 suggest_gitignore_patterns, 

15) 

16from little_loops.logger import Logger 

17from little_loops.session_store import DEFAULT_DB_PATH, cli_event_context 

18 

19 

20def main_gitignore() -> int: 

21 """Entry point for ll-gitignore command. 

22 

23 Scan for untracked files, suggest .gitignore patterns, and optionally apply them. 

24 

25 Returns: 

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

27 """ 

28 with cli_event_context(DEFAULT_DB_PATH, "ll-gitignore", sys.argv[1:]): 

29 parser = argparse.ArgumentParser( 

30 prog="ll-gitignore", 

31 description="Suggest and apply .gitignore patterns based on untracked files", 

32 formatter_class=argparse.RawDescriptionHelpFormatter, 

33 epilog=""" 

34Examples: 

35 %(prog)s # Show suggestions and apply approved patterns 

36 %(prog)s --dry-run # Preview suggestions without modifying .gitignore 

37 %(prog)s --quiet # Suppress non-essential output 

38""", 

39 ) 

40 

41 add_dry_run_arg(parser) 

42 add_quiet_arg(parser) 

43 add_config_arg(parser) 

44 add_json_arg(parser) 

45 

46 args = parser.parse_args() 

47 

48 repo_root = args.config or Path.cwd() 

49 logger = Logger(verbose=not args.quiet) 

50 dry_run: bool = args.dry_run 

51 

52 if dry_run: 

53 logger.info("[DRY RUN] Showing suggestions without modifying .gitignore") 

54 

55 suggestion = suggest_gitignore_patterns(repo_root=repo_root, logger=logger) 

56 

57 if args.json: 

58 print_json( 

59 { 

60 "has_suggestions": suggestion.has_suggestions, 

61 "summary": suggestion.summary, 

62 "suggestions": [ 

63 { 

64 "pattern": p.pattern, 

65 "category": p.category, 

66 "description": p.description, 

67 "files_matched": p.files_matched, 

68 "priority": p.priority, 

69 } 

70 for p in suggestion.patterns 

71 ], 

72 } 

73 ) 

74 return 0 

75 

76 if not suggestion.has_suggestions: 

77 logger.info("No .gitignore suggestions — your repo looks clean.") 

78 return 0 

79 

80 logger.info(suggestion.summary) 

81 

82 # Display categorized suggestions 

83 categories: dict[str, list[GitignorePattern]] = {} 

84 for pattern in suggestion.patterns: 

85 categories.setdefault(pattern.category, []).append(pattern) 

86 

87 for category, patterns in sorted(categories.items()): 

88 logger.info(f"\n [{category}]") 

89 for p in patterns: 

90 file_count = len(p.files_matched) 

91 files_label = f"{file_count} file{'s' if file_count != 1 else ''}" 

92 logger.info(f" {p.pattern:<30} {p.description} ({files_label})") 

93 

94 if dry_run: 

95 return 0 

96 

97 pattern_strings = [p.pattern for p in suggestion.patterns] 

98 success = add_patterns_to_gitignore(pattern_strings, repo_root=repo_root, logger=logger) 

99 

100 return 0 if success else 1