Coverage for .claude/hooks/moai/lib/gitignore_parser.py: 0.00%

63 statements  

« prev     ^ index     » next       coverage.py v7.11.3, created at 2025-11-19 08:00 +0900

1#!/usr/bin/env python3 

2""".gitignore parser utility 

3 

4Parse .gitignore files to extract patterns for exclusion. 

5 

6Features: 

7- Parse .gitignore files 

8- Exclude comments and empty lines 

9- Normalize glob patterns 

10- Handle directory patterns 

11 

12Usage: 

13 from .utils.gitignore_parser import load_gitignore_patterns 

14 patterns = load_gitignore_patterns() 

15""" 

16 

17from pathlib import Path 

18from typing import List, Set 

19 

20 

21def load_gitignore_patterns(gitignore_path: str = ".gitignore") -> List[str]: 

22 """Load patterns from .gitignore 

23 

24 Args: 

25 gitignore_path: Path to .gitignore file (default: ".gitignore") 

26 

27 Returns: 

28 List of exclude patterns 

29 """ 

30 patterns = [] 

31 

32 try: 

33 gitignore_file = Path(gitignore_path) 

34 if not gitignore_file.exists(): 

35 return patterns 

36 

37 with open(gitignore_file, 'r', encoding='utf-8') as f: 

38 for line in f: 

39 line = line.strip() 

40 

41 # Exclude empty lines or comments 

42 if not line or line.startswith('#'): 

43 continue 

44 

45 # Exclude negation patterns (!) - not used in validation 

46 if line.startswith('!'): 

47 continue 

48 

49 # Normalize pattern 

50 pattern = normalize_pattern(line) 

51 if pattern: 

52 patterns.append(pattern) 

53 

54 except Exception: 

55 # Return empty list on file read failure 

56 pass 

57 

58 return patterns 

59 

60 

61def normalize_pattern(pattern: str) -> str: 

62 """Normalize pattern 

63 

64 Args: 

65 pattern: Original pattern 

66 

67 Returns: 

68 Normalized pattern 

69 """ 

70 # Remove leading and trailing whitespace 

71 pattern = pattern.strip() 

72 

73 # Absolute path pattern (starts with /) 

74 if pattern.startswith('/'): 

75 # Remove / and return 

76 return pattern[1:] 

77 

78 # Return regular pattern as-is (including directory slashes) 

79 return pattern 

80 

81 

82def is_path_ignored( 

83 file_path: str, 

84 ignore_patterns: List[str], 

85 protected_paths: List[str] = None 

86) -> bool: 

87 """Check if file path matches ignore patterns 

88 

89 Args: 

90 file_path: File path to check 

91 ignore_patterns: List of ignore patterns 

92 protected_paths: Paths that should be protected (not ignored) 

93 

94 Returns: 

95 True if matched 

96 """ 

97 import fnmatch 

98 

99 # Set default protected paths 

100 if protected_paths is None: 

101 protected_paths = [".moai/specs/"] 

102 

103 # Do not ignore if protected path 

104 for protected in protected_paths: 

105 if file_path.startswith(protected): 

106 return False 

107 

108 # Normalize path 

109 path_parts = Path(file_path).parts 

110 

111 for pattern in ignore_patterns: 

112 # Wildcard directory patterns (hooks_backup_*/, *_backup_*/) 

113 if '*' in pattern and pattern.endswith('/'): 

114 pattern_without_slash = pattern[:-1] 

115 # Match with each path part 

116 for part in path_parts: 

117 if fnmatch.fnmatch(part, pattern_without_slash): 

118 return True 

119 

120 # Wildcard pattern matching (*.ext, *backup*) 

121 elif '*' in pattern: 

122 # Match full path 

123 if fnmatch.fnmatch(file_path, pattern): 

124 return True 

125 # Match each path part 

126 for part in path_parts: 

127 if fnmatch.fnmatch(part, pattern): 

128 return True 

129 

130 # Directory pattern matching 

131 elif pattern.endswith('/'): 

132 dir_name = pattern[:-1] 

133 if dir_name in path_parts: 

134 return True 

135 

136 # Simple string matching (filename or directory name) 

137 else: 

138 if pattern in file_path: 

139 return True 

140 

141 return False 

142 

143 

144def get_combined_exclude_patterns( 

145 base_patterns: List[str], 

146 gitignore_path: str = ".gitignore" 

147) -> List[str]: 

148 """Combine base patterns with .gitignore patterns 

149 

150 Args: 

151 base_patterns: Base exclude patterns 

152 gitignore_path: Path to .gitignore file 

153 

154 Returns: 

155 Combined exclude pattern list (deduplicated) 

156 """ 

157 # Start with base patterns 

158 patterns_set: Set[str] = set(base_patterns) 

159 

160 # Add .gitignore patterns 

161 gitignore_patterns = load_gitignore_patterns(gitignore_path) 

162 patterns_set.update(gitignore_patterns) 

163 

164 # Sort and return 

165 return sorted(list(patterns_set)) 

166 

167 

168if __name__ == "__main__": 

169 # Test code 

170 patterns = load_gitignore_patterns() 

171 print(f"Loaded {len(patterns)} patterns from .gitignore") 

172 for pattern in patterns[:10]: 

173 print(f" - {pattern}")