Add GitFile for .gitconfig files

This commit is contained in:
Joscha 2025-11-09 23:38:39 +01:00
parent 37ad12c687
commit fe97d02469
2 changed files with 97 additions and 0 deletions

View file

@ -1,5 +1,6 @@
from .binary import BinaryFile from .binary import BinaryFile
from .file import TAG, File from .file import TAG, File
from .git import GitFile
from .json import JsonFile from .json import JsonFile
from .text import TextFile from .text import TextFile
from .toml import TomlFile from .toml import TomlFile
@ -8,6 +9,7 @@ __all__: list[str] = [
"TAG", "TAG",
"BinaryFile", "BinaryFile",
"File", "File",
"GitFile",
"JsonFile", "JsonFile",
"TextFile", "TextFile",
"TomlFile", "TomlFile",

95
pasch/file/git.py Normal file
View file

@ -0,0 +1,95 @@
from .file import File
from .text import TextFile
type GitSection = str | tuple[str, str]
type GitValue = bool | int | str
def _format_header(section: GitSection) -> str:
if isinstance(section, str):
title, subsection = section, None
else:
title, subsection = section
# Section names are case-insensitive. Only alphanumeric characters, `-` and
# `.` are allowed in section names. However, the `[section.subsection]`
# syntax is deprecated, so we don't allow `.` in section names.
assert title
assert all(c.isascii() and (c.isalnum() or c == "-") for c in title)
section = title.lower()
if subsection is None:
return f"[{section}]"
# Subsection names are case sensitive and can contain any characters
# except newline and the null byte. Doublequote `"` and backslash can
# be included by escaping them as `\"` and `\\`, respectively.
assert subsection
for c in subsection:
assert c not in {"\n", "\0"}
escaped = "".join({'"': '\\"', "\\": "\\\\"}.get(c, c) for c in subsection)
return f'[{section} "{escaped}"]'
def _format_name(name: str) -> str:
# The variable names are case-insensitive, allow only alphanumeric
# characters and `-`, and must start with an alphabetic character.
assert name
assert all(c.isascii() and (c.isalnum() or c == "-") for c in name)
assert name[0].isalpha()
return name
def _format_value(value: GitValue) -> str:
# https://git-scm.com/docs/git-config#_values
if isinstance(value, bool):
return str(value).lower()
if isinstance(value, int):
return str(value)
# If a value needs to contain leading or trailing whitespace characters, it
# must be enclosed in double quotation marks (`"`). Inside double quotation
# marks, double quote (`"`) and backslash (`\`) characters must be escaped:
# use `\"` for `"` and `\\` for `\`.
#
# The following escape sequences (beside `\"` and `\\`) are recognized: `\n`
# for newline character (NL), `\t` for horizontal tabulation (HT, TAB) and
# `\b` for backspace (BS). Other char escape sequences (including octal
# escape sequences) are invalid.
escapes = {'"': '\\"', "\\": "\\\\", "\n": "\\n", "\t": "\\t", "\b": "\\b"}
escaped = "".join(escapes.get(c, c) for c in value)
return f'"{escaped}"'
class GitFile(File):
"""
A `.gitconfig` file.
<https://git-scm.com/docs/git-config#_configuration_file>
"""
def __init__(self, data: dict[GitSection, dict[str, GitValue]] = {}) -> None:
self.data = data
def set(self, section: GitSection, name: str, value: GitValue) -> None:
self.data.setdefault(section, {})[name] = value
def to_text(self) -> TextFile:
file = TextFile()
for section, values in sorted(self.data.items()):
# Separate sections with an empty line
if file.data:
file.append("")
file.append(_format_header(section))
for name, value in sorted(values.items()):
file.append(f" {_format_name(name)} = {_format_value(value)}")
file.tag(comment="#")
return file
def to_bytes(self) -> bytes:
return self.to_text().to_bytes()