π File detail
utils/git/gitConfigParser.ts
π― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes parseGitConfigValue and parseConfigString β mainly functions, hooks, or classes. Dependencies touch Node filesystem and Node path helpers. What the file header says: Lightweight parser for .git/config files. Verified against git's config.c: - Section names: case-insensitive, alphanumeric + hyphen - Subsection names (quoted): case-sensitive, backslash escapes (\\ and \") - Key names: case-insensitive, alphanumeric + hyphen - Values: optional q.
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
Lightweight parser for .git/config files. Verified against git's config.c: - Section names: case-insensitive, alphanumeric + hyphen - Subsection names (quoted): case-sensitive, backslash escapes (\\ and \") - Key names: case-insensitive, alphanumeric + hyphen - Values: optional quoting, inline comments (# or ;), backslash escapes
π€ Exports (heuristic)
parseGitConfigValueparseConfigString
π External import roots
Package roots from from "β¦" (relative paths omitted).
fspath
π₯οΈ Source preview
/**
* Lightweight parser for .git/config files.
*
* Verified against git's config.c:
* - Section names: case-insensitive, alphanumeric + hyphen
* - Subsection names (quoted): case-sensitive, backslash escapes (\\ and \")
* - Key names: case-insensitive, alphanumeric + hyphen
* - Values: optional quoting, inline comments (# or ;), backslash escapes
*/
import { readFile } from 'fs/promises'
import { join } from 'path'
/**
* Parse a single value from .git/config.
* Finds the first matching key under the given section/subsection.
*/
export async function parseGitConfigValue(
gitDir: string,
section: string,
subsection: string | null,
key: string,
): Promise<string | null> {
try {
const config = await readFile(join(gitDir, 'config'), 'utf-8')
return parseConfigString(config, section, subsection, key)
} catch {
return null
}
}
/**
* Parse a config value from an in-memory config string.
* Exported for testing.
*/
export function parseConfigString(
config: string,
section: string,
subsection: string | null,
key: string,
): string | null {
const lines = config.split('\n')
const sectionLower = section.toLowerCase()
const keyLower = key.toLowerCase()
let inSection = false
for (const line of lines) {
const trimmed = line.trim()
// Skip empty lines and comment-only lines
if (trimmed.length === 0 || trimmed[0] === '#' || trimmed[0] === ';') {
continue
}
// Section header
if (trimmed[0] === '[') {
inSection = matchesSectionHeader(trimmed, sectionLower, subsection)
continue
}
if (!inSection) {
continue
}
// Key-value line: find the key name
const parsed = parseKeyValue(trimmed)
if (parsed && parsed.key.toLowerCase() === keyLower) {
return parsed.value
}
}
return null
}
/**
* Parse a key = value line. Returns null if the line doesn't contain a valid key.
*/
function parseKeyValue(line: string): { key: string; value: string } | null {
// Read key: alphanumeric + hyphen, starting with alpha
let i = 0
while (i < line.length && isKeyChar(line[i]!)) {
i++
}
if (i === 0) {
return null
}
const key = line.slice(0, i)
// Skip whitespace
while (i < line.length && (line[i] === ' ' || line[i] === '\t')) {
i++
}
// Must have '='
if (i >= line.length || line[i] !== '=') {
// Boolean key with no value β not relevant for our use cases
return null
}
i++ // skip '='
// Skip whitespace after '='
while (i < line.length && (line[i] === ' ' || line[i] === '\t')) {
i++
}
const value = parseValue(line, i)
return { key, value }
}
/**
* Parse a config value starting at position i.
* Handles quoted strings, escape sequences, and inline comments.
*/
function parseValue(line: string, start: number): string {
let result = ''
let inQuote = false
let i = start
while (i < line.length) {
const ch = line[i]!
// Inline comments outside quotes end the value
if (!inQuote && (ch === '#' || ch === ';')) {
break
}
if (ch === '"') {
inQuote = !inQuote
i++
continue
}
if (ch === '\\' && i + 1 < line.length) {
const next = line[i + 1]!
if (inQuote) {
// Inside quotes: recognize escape sequences
switch (next) {
case 'n':
result += '\n'
break
case 't':
result += '\t'
break
case 'b':
result += '\b'
break
case '"':
result += '"'
break
case '\\':
result += '\\'
break
default:
// Git silently drops the backslash for unknown escapes
result += next
break
}
i += 2
continue
}
// Outside quotes: backslash at end of line = continuation (we don't
// handle multi-line since we split on \n, but handle \\ and others)
if (next === '\\') {
result += '\\'
i += 2
continue
}
// Fallthrough β treat backslash literally outside quotes
}
result += ch
i++
}
// Trim trailing whitespace from unquoted portions.
// Git trims trailing whitespace that isn't inside quotes, but since we
// process char-by-char and quotes toggle, the simplest correct approach
// for single-line values is to trim the result when not ending in a quote.
if (!inQuote) {
result = trimTrailingWhitespace(result)
}
return result
}
function trimTrailingWhitespace(s: string): string {
let end = s.length
while (end > 0 && (s[end - 1] === ' ' || s[end - 1] === '\t')) {
end--
}
return s.slice(0, end)
}
/**
* Check if a config line like `[remote "origin"]` matches the given section/subsection.
* Section matching is case-insensitive; subsection matching is case-sensitive.
*/
function matchesSectionHeader(
line: string,
sectionLower: string,
subsection: string | null,
): boolean {
// line starts with '['
let i = 1
// Read section name
while (
i < line.length &&
line[i] !== ']' &&
line[i] !== ' ' &&
line[i] !== '\t' &&
line[i] !== '"'
) {
i++
}
const foundSection = line.slice(1, i).toLowerCase()
if (foundSection !== sectionLower) {
return false
}
if (subsection === null) {
// Simple section: must end with ']'
return i < line.length && line[i] === ']'
}
// Skip whitespace before subsection quote
while (i < line.length && (line[i] === ' ' || line[i] === '\t')) {
i++
}
// Must have opening quote
if (i >= line.length || line[i] !== '"') {
return false
}
i++ // skip opening quote
// Read subsection β case-sensitive, handle \\ and \" escapes
let foundSubsection = ''
while (i < line.length && line[i] !== '"') {
if (line[i] === '\\' && i + 1 < line.length) {
const next = line[i + 1]!
if (next === '\\' || next === '"') {
foundSubsection += next
i += 2
continue
}
// Git drops the backslash for other escapes in subsections
foundSubsection += next
i += 2
continue
}
foundSubsection += line[i]
i++
}
// Must have closing quote followed by ']'
if (i >= line.length || line[i] !== '"') {
return false
}
i++ // skip closing quote
if (i >= line.length || line[i] !== ']') {
return false
}
return foundSubsection === subsection
}
function isKeyChar(ch: string): boolean {
return (
(ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') ||
ch === '-'
)
}