source dump of claude code
at main 277 lines 6.6 kB view raw
1/** 2 * Lightweight parser for .git/config files. 3 * 4 * Verified against git's config.c: 5 * - Section names: case-insensitive, alphanumeric + hyphen 6 * - Subsection names (quoted): case-sensitive, backslash escapes (\\ and \") 7 * - Key names: case-insensitive, alphanumeric + hyphen 8 * - Values: optional quoting, inline comments (# or ;), backslash escapes 9 */ 10 11import { readFile } from 'fs/promises' 12import { join } from 'path' 13 14/** 15 * Parse a single value from .git/config. 16 * Finds the first matching key under the given section/subsection. 17 */ 18export async function parseGitConfigValue( 19 gitDir: string, 20 section: string, 21 subsection: string | null, 22 key: string, 23): Promise<string | null> { 24 try { 25 const config = await readFile(join(gitDir, 'config'), 'utf-8') 26 return parseConfigString(config, section, subsection, key) 27 } catch { 28 return null 29 } 30} 31 32/** 33 * Parse a config value from an in-memory config string. 34 * Exported for testing. 35 */ 36export function parseConfigString( 37 config: string, 38 section: string, 39 subsection: string | null, 40 key: string, 41): string | null { 42 const lines = config.split('\n') 43 const sectionLower = section.toLowerCase() 44 const keyLower = key.toLowerCase() 45 46 let inSection = false 47 for (const line of lines) { 48 const trimmed = line.trim() 49 50 // Skip empty lines and comment-only lines 51 if (trimmed.length === 0 || trimmed[0] === '#' || trimmed[0] === ';') { 52 continue 53 } 54 55 // Section header 56 if (trimmed[0] === '[') { 57 inSection = matchesSectionHeader(trimmed, sectionLower, subsection) 58 continue 59 } 60 61 if (!inSection) { 62 continue 63 } 64 65 // Key-value line: find the key name 66 const parsed = parseKeyValue(trimmed) 67 if (parsed && parsed.key.toLowerCase() === keyLower) { 68 return parsed.value 69 } 70 } 71 72 return null 73} 74 75/** 76 * Parse a key = value line. Returns null if the line doesn't contain a valid key. 77 */ 78function parseKeyValue(line: string): { key: string; value: string } | null { 79 // Read key: alphanumeric + hyphen, starting with alpha 80 let i = 0 81 while (i < line.length && isKeyChar(line[i]!)) { 82 i++ 83 } 84 if (i === 0) { 85 return null 86 } 87 const key = line.slice(0, i) 88 89 // Skip whitespace 90 while (i < line.length && (line[i] === ' ' || line[i] === '\t')) { 91 i++ 92 } 93 94 // Must have '=' 95 if (i >= line.length || line[i] !== '=') { 96 // Boolean key with no value — not relevant for our use cases 97 return null 98 } 99 i++ // skip '=' 100 101 // Skip whitespace after '=' 102 while (i < line.length && (line[i] === ' ' || line[i] === '\t')) { 103 i++ 104 } 105 106 const value = parseValue(line, i) 107 return { key, value } 108} 109 110/** 111 * Parse a config value starting at position i. 112 * Handles quoted strings, escape sequences, and inline comments. 113 */ 114function parseValue(line: string, start: number): string { 115 let result = '' 116 let inQuote = false 117 let i = start 118 119 while (i < line.length) { 120 const ch = line[i]! 121 122 // Inline comments outside quotes end the value 123 if (!inQuote && (ch === '#' || ch === ';')) { 124 break 125 } 126 127 if (ch === '"') { 128 inQuote = !inQuote 129 i++ 130 continue 131 } 132 133 if (ch === '\\' && i + 1 < line.length) { 134 const next = line[i + 1]! 135 if (inQuote) { 136 // Inside quotes: recognize escape sequences 137 switch (next) { 138 case 'n': 139 result += '\n' 140 break 141 case 't': 142 result += '\t' 143 break 144 case 'b': 145 result += '\b' 146 break 147 case '"': 148 result += '"' 149 break 150 case '\\': 151 result += '\\' 152 break 153 default: 154 // Git silently drops the backslash for unknown escapes 155 result += next 156 break 157 } 158 i += 2 159 continue 160 } 161 // Outside quotes: backslash at end of line = continuation (we don't 162 // handle multi-line since we split on \n, but handle \\ and others) 163 if (next === '\\') { 164 result += '\\' 165 i += 2 166 continue 167 } 168 // Fallthrough — treat backslash literally outside quotes 169 } 170 171 result += ch 172 i++ 173 } 174 175 // Trim trailing whitespace from unquoted portions. 176 // Git trims trailing whitespace that isn't inside quotes, but since we 177 // process char-by-char and quotes toggle, the simplest correct approach 178 // for single-line values is to trim the result when not ending in a quote. 179 if (!inQuote) { 180 result = trimTrailingWhitespace(result) 181 } 182 183 return result 184} 185 186function trimTrailingWhitespace(s: string): string { 187 let end = s.length 188 while (end > 0 && (s[end - 1] === ' ' || s[end - 1] === '\t')) { 189 end-- 190 } 191 return s.slice(0, end) 192} 193 194/** 195 * Check if a config line like `[remote "origin"]` matches the given section/subsection. 196 * Section matching is case-insensitive; subsection matching is case-sensitive. 197 */ 198function matchesSectionHeader( 199 line: string, 200 sectionLower: string, 201 subsection: string | null, 202): boolean { 203 // line starts with '[' 204 let i = 1 205 206 // Read section name 207 while ( 208 i < line.length && 209 line[i] !== ']' && 210 line[i] !== ' ' && 211 line[i] !== '\t' && 212 line[i] !== '"' 213 ) { 214 i++ 215 } 216 const foundSection = line.slice(1, i).toLowerCase() 217 218 if (foundSection !== sectionLower) { 219 return false 220 } 221 222 if (subsection === null) { 223 // Simple section: must end with ']' 224 return i < line.length && line[i] === ']' 225 } 226 227 // Skip whitespace before subsection quote 228 while (i < line.length && (line[i] === ' ' || line[i] === '\t')) { 229 i++ 230 } 231 232 // Must have opening quote 233 if (i >= line.length || line[i] !== '"') { 234 return false 235 } 236 i++ // skip opening quote 237 238 // Read subsection — case-sensitive, handle \\ and \" escapes 239 let foundSubsection = '' 240 while (i < line.length && line[i] !== '"') { 241 if (line[i] === '\\' && i + 1 < line.length) { 242 const next = line[i + 1]! 243 if (next === '\\' || next === '"') { 244 foundSubsection += next 245 i += 2 246 continue 247 } 248 // Git drops the backslash for other escapes in subsections 249 foundSubsection += next 250 i += 2 251 continue 252 } 253 foundSubsection += line[i] 254 i++ 255 } 256 257 // Must have closing quote followed by ']' 258 if (i >= line.length || line[i] !== '"') { 259 return false 260 } 261 i++ // skip closing quote 262 263 if (i >= line.length || line[i] !== ']') { 264 return false 265 } 266 267 return foundSubsection === subsection 268} 269 270function isKeyChar(ch: string): boolean { 271 return ( 272 (ch >= 'a' && ch <= 'z') || 273 (ch >= 'A' && ch <= 'Z') || 274 (ch >= '0' && ch <= '9') || 275 ch === '-' 276 ) 277}