diff --git a/git/config.py b/git/config.py index bf9c3ecbb..9bb82273c 100644 --- a/git/config.py +++ b/git/config.py @@ -550,9 +550,14 @@ def parse_value(value: str) -> str: while True: # We assume to read binary! - line = fp.readline().decode(defenc) - if not line: + raw_line = fp.readline() + if not raw_line: break + if lineno == 0 and raw_line.startswith(b"\xef\xbb\xbf"): + # A UTF-8 BOM is not part of the content. git skips it, so a + # config file written by a Windows editor still parses. + raw_line = raw_line[3:] + line = raw_line.decode(defenc) lineno = lineno + 1 # Comment or blank line? if line.strip() == "" or self.re_comment.match(line): diff --git a/test/test_config.py b/test/test_config.py index e00bde183..b0f7f72eb 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -354,6 +354,17 @@ def test_comment_backslash_does_not_continue_value(self, rw_dir): with GitConfigParser(config_path) as config: self.assertEqual(config.get_value("a", "x"), "two") + def test_utf8_bom_is_skipped_like_git(self): + """git skips a UTF-8 BOM at the start of a config file, so one written by + a Windows editor still parses. Expectations are what + `git config -f --list` prints on git 2.47.3.""" + content = b"\xef\xbb\xbf[core]\n\tbare = true\n" + config_file = io.BytesIO(content) + config_file.name = "bom.config" + config = GitConfigParser(config_file) + config.read() + self.assertIs(config.get_value("core", "bare"), True) + def test_config_value_with_trailing_new_line(self): config_content = b'[section-header]\nkey:"value\n"' config_file = io.BytesIO(config_content)