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

2

3 from __future__ import annotations

4

5 import argparse

6 import sys

7 from pathlib import Path

8

9 from little_loops.cli.output import print_json

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

11 from little_loops.git_operations import (

12 GitignorePattern,

13 add_patterns_to_gitignore,

14 suggest_gitignore_patterns,

15)

16 from little_loops.logger import Logger

17 from little_loops.session_store import DEFAULT_DB_PATH, cli_event_context

18

19

20 def 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="""

34 Examples:

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