| 
 | 1 | +import * as fs from 'fs';  | 
 | 2 | +import * as path from 'path';  | 
 | 3 | + | 
 | 4 | +/**  | 
 | 5 | + * Represents a parsed gitignore pattern  | 
 | 6 | + */  | 
 | 7 | +export interface GitignorePattern {  | 
 | 8 | +  pattern: string;  | 
 | 9 | +  isNegated: boolean;  | 
 | 10 | +  isDirectory: boolean;  | 
 | 11 | +  isAbsolute: boolean;  | 
 | 12 | +}  | 
 | 13 | + | 
 | 14 | +/**  | 
 | 15 | + * Parses a .gitignore file and returns an array of patterns  | 
 | 16 | + * @param gitignorePath Path to the .gitignore file  | 
 | 17 | + * @returns Array of parsed gitignore patterns  | 
 | 18 | + */  | 
 | 19 | +export function parseGitignoreFile(gitignorePath: string): GitignorePattern[] {  | 
 | 20 | +  if (!fs.existsSync(gitignorePath)) {  | 
 | 21 | +    return [];  | 
 | 22 | +  }  | 
 | 23 | + | 
 | 24 | +  const content = fs.readFileSync(gitignorePath, 'utf8');  | 
 | 25 | +  return parseGitignoreContent(content);  | 
 | 26 | +}  | 
 | 27 | + | 
 | 28 | +/**  | 
 | 29 | + * Parses gitignore content and returns an array of patterns  | 
 | 30 | + * @param content Content of the .gitignore file  | 
 | 31 | + * @returns Array of parsed gitignore patterns  | 
 | 32 | + */  | 
 | 33 | +export function parseGitignoreContent(content: string): GitignorePattern[] {  | 
 | 34 | +  return content  | 
 | 35 | +    .split('\n')  | 
 | 36 | +    .map(line => line.trim())  | 
 | 37 | +    .filter(line => line && !line.startsWith('#')) // Remove empty lines and comments  | 
 | 38 | +    .map(line => {  | 
 | 39 | +      const isNegated = line.startsWith('!');  | 
 | 40 | +      const pattern = isNegated ? line.substring(1) : line;  | 
 | 41 | +      const isDirectory = pattern.endsWith('/');  | 
 | 42 | +      const isAbsolute = pattern.startsWith('/') || pattern.startsWith('./');  | 
 | 43 | + | 
 | 44 | +      return {  | 
 | 45 | +        pattern: isAbsolute ? pattern.substring(pattern.startsWith('./') ? 2 : 1) : pattern,  | 
 | 46 | +        isNegated,  | 
 | 47 | +        isDirectory,  | 
 | 48 | +        isAbsolute  | 
 | 49 | +      };  | 
 | 50 | +    });  | 
 | 51 | +}  | 
 | 52 | + | 
 | 53 | +/**  | 
 | 54 | + * Checks if a file or directory matches any of the gitignore patterns  | 
 | 55 | + * @param filePath Path to the file or directory (relative to the directory containing the .gitignore)  | 
 | 56 | + * @param patterns Array of gitignore patterns  | 
 | 57 | + * @param isDirectory Whether the path is a directory  | 
 | 58 | + * @returns True if the file or directory should be ignored  | 
 | 59 | + */  | 
 | 60 | +export function matchesGitignorePatterns(  | 
 | 61 | +  filePath: string,  | 
 | 62 | +  patterns: GitignorePattern[],  | 
 | 63 | +  isDirectory: boolean  | 
 | 64 | +): boolean {  | 
 | 65 | +  // Normalize path for matching  | 
 | 66 | +  const normalizedPath = filePath.replace(/\\/g, '/');  | 
 | 67 | + | 
 | 68 | +  // Start with not ignored, then apply patterns in order  | 
 | 69 | +  let ignored = false;  | 
 | 70 | + | 
 | 71 | +  for (const pattern of patterns) {  | 
 | 72 | +    // Skip directory-only patterns if this is a file  | 
 | 73 | +    if (pattern.isDirectory && !isDirectory) {  | 
 | 74 | +      continue;  | 
 | 75 | +    }  | 
 | 76 | + | 
 | 77 | +    if (matchesPattern(normalizedPath, pattern, isDirectory)) {  | 
 | 78 | +      // If pattern matches, set ignored based on whether it's negated  | 
 | 79 | +      ignored = !pattern.isNegated;  | 
 | 80 | +    }  | 
 | 81 | +  }  | 
 | 82 | + | 
 | 83 | +  return ignored;  | 
 | 84 | +}  | 
 | 85 | + | 
 | 86 | +/**  | 
 | 87 | + * Checks if a path matches a gitignore pattern  | 
 | 88 | + * @param normalizedPath Normalized path to check  | 
 | 89 | + * @param pattern Gitignore pattern  | 
 | 90 | + * @param isDirectory Whether the path is a directory  | 
 | 91 | + * @returns True if the path matches the pattern  | 
 | 92 | + */  | 
 | 93 | +function matchesPattern(  | 
 | 94 | +  normalizedPath: string,  | 
 | 95 | +  pattern: GitignorePattern,  | 
 | 96 | +  isDirectory: boolean  | 
 | 97 | +): boolean {  | 
 | 98 | +  const patternStr = pattern.pattern.replace(/\\/g, '/');  | 
 | 99 | + | 
 | 100 | +  // For directory patterns (ending with /), also check without the trailing slash  | 
 | 101 | +  // when matching against directories  | 
 | 102 | +  if (pattern.isDirectory && isDirectory && patternStr.endsWith('/')) {  | 
 | 103 | +    const patternWithoutSlash = patternStr.slice(0, -1);  | 
 | 104 | +    if (normalizedPath === patternWithoutSlash) {  | 
 | 105 | +      return true;  | 
 | 106 | +    }  | 
 | 107 | +  }  | 
 | 108 | + | 
 | 109 | +  // Handle exact matches  | 
 | 110 | +  if (!patternStr.includes('*')) {  | 
 | 111 | +    if (pattern.isAbsolute) {  | 
 | 112 | +      // For absolute patterns, match from the beginning  | 
 | 113 | +      return normalizedPath === patternStr ||   | 
 | 114 | +             (isDirectory && normalizedPath.startsWith(patternStr + '/'));  | 
 | 115 | +    } else {  | 
 | 116 | +      // For relative patterns, match anywhere in the path  | 
 | 117 | +      return normalizedPath === patternStr ||   | 
 | 118 | +             normalizedPath.endsWith('/' + patternStr) ||   | 
 | 119 | +             normalizedPath.includes('/' + patternStr + '/') ||  | 
 | 120 | +             (isDirectory && (  | 
 | 121 | +               normalizedPath.endsWith('/' + patternStr) ||   | 
 | 122 | +               normalizedPath.includes('/' + patternStr + '/')  | 
 | 123 | +             ));  | 
 | 124 | +    }  | 
 | 125 | +  }  | 
 | 126 | + | 
 | 127 | +  // Handle wildcard patterns  | 
 | 128 | +  const regexPattern = patternStr  | 
 | 129 | +    .replace(/\./g, '\\.') // Escape dots  | 
 | 130 | +    .replace(/\*/g, '.*')  // Convert * to .*  | 
 | 131 | +    .replace(/\?/g, '.');  // Convert ? to .  | 
 | 132 | + | 
 | 133 | +  const regex = pattern.isAbsolute  | 
 | 134 | +    ? new RegExp(`^${regexPattern}$`)  | 
 | 135 | +    : new RegExp(`(^|/)${regexPattern}$`);  | 
 | 136 | + | 
 | 137 | +  return regex.test(normalizedPath);  | 
 | 138 | +}  | 
 | 139 | + | 
 | 140 | +/**  | 
 | 141 | + * Collects gitignore patterns from a specific directory  | 
 | 142 | + * @param dirPath Path to the directory  | 
 | 143 | + * @returns Array of gitignore patterns from this directory  | 
 | 144 | + */  | 
 | 145 | +export function collectDirectoryGitignorePatterns(dirPath: string): GitignorePattern[] {  | 
 | 146 | +  const gitignorePath = path.join(dirPath, '.gitignore');  | 
 | 147 | +  if (fs.existsSync(gitignorePath)) {  | 
 | 148 | +    return parseGitignoreFile(gitignorePath);  | 
 | 149 | +  }  | 
 | 150 | +  return [];  | 
 | 151 | +}  | 
 | 152 | + | 
 | 153 | +/**  | 
 | 154 | + * Collects all gitignore patterns that apply to a given directory  | 
 | 155 | + * @param dirPath Path to the directory  | 
 | 156 | + * @returns Array of gitignore patterns that apply to the directory  | 
 | 157 | + */  | 
 | 158 | +export function collectGitignorePatterns(dirPath: string): GitignorePattern[] {  | 
 | 159 | +  const patterns: GitignorePattern[] = [];  | 
 | 160 | +  let currentDir = dirPath;  | 
 | 161 | + | 
 | 162 | +  // Collect patterns from all parent directories up to the root  | 
 | 163 | +  while (true) {  | 
 | 164 | +    const gitignorePath = path.join(currentDir, '.gitignore');  | 
 | 165 | +    if (fs.existsSync(gitignorePath)) {  | 
 | 166 | +      const dirPatterns = parseGitignoreFile(gitignorePath);  | 
 | 167 | +      patterns.push(...dirPatterns);  | 
 | 168 | +    }  | 
 | 169 | + | 
 | 170 | +    const parentDir = path.dirname(currentDir);  | 
 | 171 | +    if (parentDir === currentDir) {  | 
 | 172 | +      break; // Reached the root  | 
 | 173 | +    }  | 
 | 174 | +    currentDir = parentDir;  | 
 | 175 | +  }  | 
 | 176 | + | 
 | 177 | +  return patterns;  | 
 | 178 | +}  | 
 | 179 | + | 
 | 180 | +/**  | 
 | 181 | + * Determines if a file or directory should be excluded based on gitignore patterns  | 
 | 182 | + * @param fullPath Full path to the file or directory  | 
 | 183 | + * @param baseDir Base directory for relative path calculation  | 
 | 184 | + * @param patterns Gitignore patterns to check against  | 
 | 185 | + * @param isDirectory Whether the path is a directory  | 
 | 186 | + * @returns True if the file or directory should be excluded  | 
 | 187 | + */  | 
 | 188 | +export function shouldExcludeByGitignore(  | 
 | 189 | +  fullPath: string,  | 
 | 190 | +  baseDir: string,  | 
 | 191 | +  patterns: GitignorePattern[],  | 
 | 192 | +  isDirectory: boolean  | 
 | 193 | +): boolean {  | 
 | 194 | +  // Calculate path relative to the base directory  | 
 | 195 | +  const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, '/');  | 
 | 196 | + | 
 | 197 | +  // Check if the path matches any gitignore pattern  | 
 | 198 | +  return matchesGitignorePatterns(relativePath, patterns, isDirectory);  | 
 | 199 | +}  | 
0 commit comments