Skip to content

Commit a44f8d9

Browse files
codexByron
andcommitted
feat: rewrite config parser to be consistent with Git
Previously it was based on an ini-parser, which depends on the Python version and also isn't actually dealing with the Git's specific grammar and rules. Now it's much more consistent, albeit probably also slower. --- agent Replace the line-oriented INI parsing logic in GitConfigParser with a character-stream parser modeled on Git's config.c implementation. The inherited configparser section matcher treats everything between the first opening bracket and the last closing bracket as one section name. Consequently, GitPython interpreted a header such as: [user] [other] as one section named "user] [other", while Git interpreted it as two successive section headers and assigned following values to "other". Writing opaque section names through the INI serializer could therefore change the configuration's meaning when Git subsequently read it. Implement Git's configuration grammar directly, following the behavior of git_parse_source(), get_base_var(), get_value(), and parse_value(). The new parser handles: - basic and quoted-subsection section headers - multiple section headers in the same character stream - comments introduced by '#' or ';' - quoted and unquoted values - supported backslash escapes - backslash-newline continuations - leading and trailing whitespace rules - CRLF input and UTF-8 byte-order marks - entries without values, represented semantically as boolean true - duplicate options while preserving their order Reject syntax that Git itself rejects, including colon delimiters, underscore-containing option names, malformed section headers, unknown value escapes, and unterminated quoted values. Replace the raw value writer with a canonical Git-config serializer. Values are always quoted, with backslashes, quotes, newlines, tabs, and backspaces escaped. Quoted subsection names are parsed and re-escaped canonically before being written. Comments and original whitespace remain intentionally unpreserved when a dirty configuration is flushed. Validate section names by parsing them as complete Git section headers. This ensures each supplied name identifies exactly one section and prevents unquoted closing brackets from injecting another section. Validate option names against Git's ASCII letter, digit, and hyphen grammar as well. Update fixtures that previously depended on INI syntax rejected by Git, and adjust expectations for decoded value escapes and valueless boolean entries. Add coverage for: - the differing interpretation of adjacent section headers - BOM, CRLF, comments, continuations, and valueless entries - malformed syntax rejected by Git - canonical value serialization and reparsing - real `git config` interoperability - closing brackets inside quoted subsection names - quotes and backslashes inside subsection names - invalid section and option names - duplicate-value behavior with Git-compatible option names Co-authored-by: Sebastian Thiel <sebastian.thiel@icloud.com>
1 parent 6a5eb6a commit a44f8d9

9 files changed

Lines changed: 846 additions & 388 deletions

File tree

git/config.py

Lines changed: 586 additions & 281 deletions
Large diffs are not rendered by default.

git/objects/submodule/base.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
__all__ = ["Submodule", "UpdateProgress"]
55

66
import gc
7+
from configparser import NoSectionError
78
from io import BytesIO
89
import logging
910
import os
@@ -16,7 +17,7 @@
1617
import git
1718
from git.cmd import Git
1819
from git.compat import defenc
19-
from git.config import GitConfigParser, SectionConstraint, cp
20+
from git.config import GitConfigParser, SectionConstraint
2021
from git.exc import (
2122
BadName,
2223
InvalidGitRepositoryError,
@@ -172,7 +173,7 @@ def _set_cache_(self, attr: str) -> None:
172173
# Default submodule values.
173174
try:
174175
self.path = reader.get("path")
175-
except cp.NoSectionError as e:
176+
except NoSectionError as e:
176177
if self.repo.working_tree_dir is not None:
177178
raise ValueError(
178179
"This submodule instance does not exist anymore in '%s' file"
@@ -1503,7 +1504,7 @@ def exists(self) -> bool:
15031504
if hasattr(self, attr):
15041505
loc[attr] = getattr(self, attr)
15051506
# END if we have the attribute cache
1506-
except (cp.NoSectionError, ValueError):
1507+
except (NoSectionError, ValueError):
15071508
# On PY3, this can happen apparently... don't know why this doesn't
15081509
# happen on PY2.
15091510
pass

git/objects/submodule/util.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def flush_to_index(self) -> None:
110110
# } END interface
111111

112112
# { Overridden Methods
113-
def write(self) -> None: # type: ignore[override]
113+
def write(self) -> None:
114114
rval: None = super().write()
115115
self.flush_to_index()
116116
return rval

git/remote.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88
__all__ = ["RemoteProgress", "PushInfo", "FetchInfo", "Remote"]
99

1010
import contextlib
11+
from configparser import NoOptionError, NoSectionError
1112
import logging
1213
import re
1314

1415
from git.cmd import Git, handle_process_output
1516
from git.compat import defenc, force_text
16-
from git.config import GitConfigParser, SectionConstraint, cp
17+
from git.config import GitConfigParser, SectionConstraint
1718
from git.exc import GitCommandError
1819
from git.refs import Head, Reference, RemoteReference, SymbolicReference, TagReference
1920
from git.util import (
@@ -577,7 +578,7 @@ def __getattr__(self, attr: str) -> Any:
577578
# though a slot of the same name exists.
578579
try:
579580
return self._config_reader.get(attr)
580-
except cp.NoOptionError:
581+
except NoOptionError:
581582
return super().__getattr__(attr)
582583
# END handle exception
583584

@@ -619,10 +620,10 @@ def exists(self) -> bool:
619620
try:
620621
self.config_reader.get("url")
621622
return True
622-
except cp.NoOptionError:
623+
except NoOptionError:
623624
# We have the section at least...
624625
return True
625-
except cp.NoSectionError:
626+
except NoSectionError:
626627
return False
627628

628629
@classmethod

test/fixtures/git_config

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@
2222
url = git://gitorious.org/~martin.marcher/git-python/serverhorror.git
2323
fetch = +refs/heads/*:refs/remotes/MartinMarcher/*
2424
# can handle comments - the section name is supposed to be stripped
25-
# causes stock git-config puke
26-
[ gui ]
25+
[gui]
2726
geometry = 1316x820+219+243 207 192
2827
[branch "mainline_performance"]
2928
remote = mainline
@@ -43,4 +42,3 @@
4342
# inclusions should be processed immediately
4443
[sec]
4544
var1 = value1_main
46-

test/fixtures/git_config_multiple

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
[section1]
55
option1 = value1a
66
option1 = value1b
7-
other_option1 = other_value1
7+
other-option1 = other_value1

test/fixtures/git_config_with_quotes_escapes

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,5 @@
33
hasbackslash = "foo\\bar"
44
hasquote = "ab\"cd"
55
hastrailingbackslash = "word\\"
6-
hasunrecognized = "p\qrs"
7-
hasunescapedquotes = "ab"cd"e"
86
ordinary = "hello world"
97
unquoted = good evening

0 commit comments

Comments
 (0)