# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
#
# This module is part of GitPython and is released under the
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/

import glob
import io
import os
import os.path as osp
import subprocess
import sys
from unittest import mock

import pytest

from git import GitConfigParser
from git.compat import defenc
from git.config import _OMD, cp
from git.util import cwd, rmfile
from test.lib import SkipTest, TestCase, fixture_path, with_rw_directory

_tc_lock_fpaths = osp.join(osp.dirname(__file__), "fixtures/*.lock")


def _rm_lock_files():
    for lfp in glob.glob(_tc_lock_fpaths):
        rmfile(lfp)


class TestBase(TestCase):
    def setUp(self):
        _rm_lock_files()

    def tearDown(self):
        for lfp in glob.glob(_tc_lock_fpaths):
            if osp.isfile(lfp):
                raise AssertionError("Previous TC left hanging git-lock file: {}".format(lfp))

    def _to_memcache(self, file_path):
        with open(file_path, "rb") as fp:
            sio = io.BytesIO(fp.read())
        sio.name = file_path
        return sio

    def test_read_write(self):
        # The writer must create the exact same file as the one read before.
        for filename in ("git_config", "git_config_global"):
            file_obj = self._to_memcache(fixture_path(filename))
            with GitConfigParser(file_obj, read_only=False) as w_config:
                w_config.read()  # Enforce reading.
                assert w_config._sections
                w_config.write()  # Enforce writing.

                # We stripped lines when reading, so the results differ.
                assert file_obj.getvalue()
                self.assertEqual(
                    file_obj.getvalue(),
                    self._to_memcache(fixture_path(filename)).getvalue(),
                )

                # Creating an additional config writer must fail due to exclusive
                # access.
                with self.assertRaises(IOError):
                    GitConfigParser(file_obj, read_only=False)

                # Should still have a lock and be able to make changes.
                assert w_config._lock._has_lock()

                # Changes should be written right away.
                sname = "my_section"
                oname = "mykey"
                val = "myvalue"
                w_config.add_section(sname)
                assert w_config.has_section(sname)
                w_config.set(sname, oname, val)
                assert w_config.has_option(sname, oname)
                assert w_config.get(sname, oname) == val

                sname_new = "new_section"
                oname_new = "new_key"
                ival = 10
                w_config.set_value(sname_new, oname_new, ival)
                assert w_config.get_value(sname_new, oname_new) == ival

                file_obj.seek(0)
                r_config = GitConfigParser(file_obj, read_only=True)
                assert r_config.has_section(sname)
                assert r_config.has_option(sname, oname)
                assert r_config.get(sname, oname) == val
        # END for each filename

    def test_includes_order(self):
        with GitConfigParser(list(map(fixture_path, ("git_config", "git_config_global")))) as r_config:
            r_config.read()  # Enforce reading.
            # Simple inclusions, again checking them taking precedence.
            assert r_config.get_value("sec", "var0") == "value0_included"
            # This one should take the git_config_global value since included values
            # must be considered as soon as they get them.
            assert r_config.get_value("diff", "tool") == "meld"
            try:
                # FIXME: Split this assertion out somehow and mark it xfail (or fix it).
                assert r_config.get_value("sec", "var1") == "value1_main"
            except AssertionError as e:
                raise SkipTest("Known failure -- included values are not in effect right away") from e

    @with_rw_directory
    def test_lock_reentry(self, rw_dir):
        fpl = osp.join(rw_dir, "l")
        gcp = GitConfigParser(fpl, read_only=False)
        with gcp as cw:
            cw.set_value("include", "some_value", "a")
        # Entering again locks the file again...
        with gcp as cw:
            cw.set_value("include", "some_other_value", "b")
            # ...so creating an additional config writer must fail due to exclusive
            # access.
            with self.assertRaises(IOError):
                GitConfigParser(fpl, read_only=False)
        # but work when the lock is removed
        with GitConfigParser(fpl, read_only=False):
            assert osp.exists(fpl)
            # Reentering with an existing lock must fail due to exclusive access.
            with self.assertRaises(IOError):
                gcp.__enter__()

    def test_multi_line_config(self):
        file_obj = self._to_memcache(fixture_path("git_config_with_comments"))
        with GitConfigParser(file_obj, read_only=False) as config:
            ev = "ruby -e '\n"
            ev += "		system %(git), %(merge-file), %(--marker-size=%L), %(%A), %(%O), %(%B)\n"
            ev += "		b = File.read(%(%A))\n"
            ev += "		b.sub!(/^<+ .*\\nActiveRecord::Schema\\.define.:version => (\\d+). do\\n=+\\nActiveRecord::Schema\\."  # noqa: E501
            ev += "define.:version => (\\d+). do\\n>+ .*/) do\n"
            ev += "		  %(ActiveRecord::Schema.define(:version => #{[$1, $2].max}) do)\n"
            ev += "		end\n"
            ev += "		File.open(%(%A), %(w)) {|f| f.write(b)}\n"
            ev += "		exit 1 if b.include?(%(<)*%L)'"
            self.assertEqual(config.get('merge "railsschema"', "driver"), ev)
            self.assertEqual(
                config.get("alias", "lg"),
                "log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr)%Creset'"
                " --abbrev-commit --date=relative",
            )
            self.assertEqual(len(config.sections()), 23)

    def test_config_value_with_trailing_new_line(self):
        config_content = b'[section-header]\nkey:"value\n"'
        config_file = io.BytesIO(config_content)
        config_file.name = "multiline_value.config"

        git_config = GitConfigParser(config_file)
        git_config.read()  # This should not throw an exception

    @with_rw_directory
    def test_rewriting_multiline_value_does_not_create_option(self, rw_dir):
        config_path = osp.join(rw_dir, "config")
        with open(config_path, "wb") as config_file:
            config_file.write(b'[core]\n\tzzz = "A\\nhooksPath = ../evil-hooks\\\n"\n')

        with GitConfigParser(config_path, read_only=False) as git_config:
            self.assertEqual(git_config.get_value("core", "zzz"), "A\nhooksPath = ../evil-hooks")
            git_config.set_value("user", "name", "Test User")

        with GitConfigParser(config_path, read_only=True) as git_config:
            self.assertEqual(git_config.get_value("core", "zzz"), "A\nhooksPath = ../evil-hooks")
            self.assertFalse(git_config.has_option("core", "hooksPath"))
        self.assertEqual(
            subprocess.run(["git", "config", "--file", config_path, "--get", "core.hooksPath"]).returncode, 1
        )

    @with_rw_directory
    def test_writer_escapes_special_characters_without_newline(self, rw_dir):
        config_path = osp.join(rw_dir, "config")
        values = {
            "tab": "\tvalue\t",
            "backspace": "a\bb",
            "quote": 'a"b',
            "backslash": "a\\qb",
            "hash": "value#fragment",
            "semicolon": "value;fragment",
            "leading": " value",
            "trailing": "value ",
        }

        with GitConfigParser(config_path, read_only=False) as git_config:
            for key, value in values.items():
                git_config.set_value("section", key, value)

        with GitConfigParser(config_path, read_only=True) as git_config:
            for key, value in values.items():
                self.assertEqual(git_config.get_value("section", key), value)
                self.assertEqual(
                    subprocess.run(
                        ["git", "config", "--file", config_path, "--get", "section.%s" % key],
                        stdout=subprocess.PIPE,
                        check=True,
                    ).stdout,
                    value.encode(defenc) + b"\n",
                )
        with open(config_path, "rb") as config_file:
            self.assertNotIn(b"\x08", config_file.read())

    @with_rw_directory
    def test_writer_preserves_escaped_and_non_ascii_values_safely(self, rw_dir):
        config_path = osp.join(rw_dir, "config")
        with open(config_path, "wb") as config_file:
            config_file.write(
                (
                    '[section]\nnewline = "first\\nsecond"\nquote = "a\\"b"\nbackslash = "a\\\\b"\n'
                    'unicode = "café\\\\path"\n'
                ).encode(defenc)
            )

        with GitConfigParser(config_path, read_only=False) as config:
            config.set_value("unrelated", "key", "value")

        expected = {
            "newline": "first\nsecond",
            "quote": 'a"b',
            "backslash": "a\\b",
            "unicode": "café\\path",
        }
        with GitConfigParser(config_path, read_only=True) as config:
            for key, value in expected.items():
                self.assertEqual(
                    config.get_value("section", key),
                    value,
                    "GitPython should preserve values when rewriting unrelated entries",
                )
                self.assertEqual(
                    subprocess.run(
                        ["git", "config", "--file", config_path, "--get", "section.%s" % key],
                        stdout=subprocess.PIPE,
                        check=True,
                    ).stdout,
                    value.encode(defenc) + b"\n",
                    "git should read rewritten values with the same semantics",
                )

        with open(config_path, "rb") as config_file:
            contents = config_file.read()
        self.assertNotIn(b"\r", contents, "the writer should never emit carriage returns")
        self.assertNotIn(b"\x00", contents, "the writer should never emit NUL bytes")

        for name, value in (("return", b"first\\rsecond"), ("nul", b"first\x00second")):
            unsafe_path = osp.join(rw_dir, "%s-config" % name)
            unsafe_contents = b'[section]\nvalue = "' + value + b'"\n'
            with open(unsafe_path, "wb") as config_file:
                config_file.write(unsafe_contents)
            with self.assertRaisesRegex(
                ValueError,
                "CR or NUL",
                msg="unsafe existing values should abort rewrites",
            ):
                with GitConfigParser(unsafe_path, read_only=False) as config:
                    config.set_value("unrelated", "key", "value")
            with open(unsafe_path, "rb") as config_file:
                self.assertEqual(
                    config_file.read(),
                    unsafe_contents,
                    "rejected rewrites should leave the original file unchanged",
                )

    @with_rw_directory
    def test_set_value_rejects_config_injection(self, rw_dir):
        config_path = osp.join(rw_dir, "config")
        payload = "foo\n[core]\nhooksPath=/tmp/hooks"

        with GitConfigParser(config_path, read_only=False) as git_config:
            with pytest.raises(ValueError, match="CR, LF, or NUL"):
                git_config.set_value("user", "name", payload)

        with GitConfigParser(config_path, read_only=True) as git_config:
            self.assertFalse(git_config.has_section("user"))
            self.assertFalse(git_config.has_section("core"))

    @with_rw_directory
    def test_set_value_rejects_unsafe_section_and_option_names(self, rw_dir):
        config_path = osp.join(rw_dir, "config")
        bad_keys = ("user]\n[core", "user]\r[core", "user]\x00[core")

        with GitConfigParser(config_path, read_only=False) as git_config:
            git_config.add_section("user")
            for bad_key in bad_keys:
                with pytest.raises(ValueError, match="CR, LF, or NUL"):
                    git_config.add_section(bad_key)
                with pytest.raises(ValueError, match="CR, LF, or NUL"):
                    git_config.set(bad_key, "hooksPath", "/tmp/hooks")
                with pytest.raises(ValueError, match="CR, LF, or NUL"):
                    git_config.set("user", bad_key, "/tmp/hooks")
                with pytest.raises(ValueError, match="CR, LF, or NUL"):
                    git_config.set_value(bad_key, "hooksPath", "/tmp/hooks")
                with pytest.raises(ValueError, match="CR, LF, or NUL"):
                    git_config.set_value("user", bad_key, "/tmp/hooks")
                with pytest.raises(ValueError, match="CR, LF, or NUL"):
                    git_config.add_value(bad_key, "hooksPath", "/tmp/hooks")
                with pytest.raises(ValueError, match="CR, LF, or NUL"):
                    git_config.add_value("user", bad_key, "/tmp/hooks")
                with pytest.raises(ValueError, match="CR, LF, or NUL"):
                    git_config.rename_section("user", bad_key)

            git_config.set_value("user", "name", "safe")

        with GitConfigParser(config_path, read_only=True) as git_config:
            self.assertEqual(git_config.get_value("user", "name"), "safe")
            self.assertFalse(git_config.has_section("core"))

    @with_rw_directory
    def test_writer_rejects_invalid_option_names(self, rw_dir):
        config_path = osp.join(rw_dir, "config")
        bad_options = (
            "name=value",
            "name#comment",
            "name;comment",
            "name with space",
            "name\twith-tab",
            "name[section",
            "name]section",
            "name:colon",
            'name"quote',
            "name\\escape",
        )

        with GitConfigParser(config_path, read_only=False) as git_config:
            git_config.add_section("user")
            for bad_option in bad_options:
                with pytest.raises(ValueError, match="option name"):
                    git_config.set("user", bad_option, "unsafe")
                with pytest.raises(ValueError, match="option name"):
                    git_config.set_value("user", bad_option, "unsafe")
                with pytest.raises(ValueError, match="option name"):
                    git_config.add_value("user", bad_option, "unsafe")

            git_config.set_value("user", "safe-option1", "safe")
            git_config.set_value("user", "safe_option2", "safe")
            git_config.set_value("user", "3safe_option", "safe")
            git_config.set_value("user", "safe.option3", "safe")

        with GitConfigParser(config_path, read_only=True) as git_config:
            self.assertEqual(git_config.get_value("user", "safe-option1"), "safe")
            self.assertEqual(git_config.get_value("user", "safe_option2"), "safe")
            self.assertEqual(git_config.get_value("user", "3safe_option"), "safe")
            self.assertEqual(git_config.get_value("user", "safe.option3"), "safe")

    @with_rw_directory
    def test_writer_rejects_unquoted_section_terminators(self, rw_dir):
        config_path = osp.join(rw_dir, "config")
        bad_sections = (
            "user] [other",
            'user"] [other"',
            'submodule "docs"] [other',
            'submodule "docs] [other',
            'submodule "docs] [other\\',
        )
        safe_section = 'submodule "docs]archive"'

        with GitConfigParser(config_path, read_only=False) as git_config:
            git_config.add_section("user")
            for bad_section in bad_sections:
                with pytest.raises(ValueError, match="section name"):
                    git_config.add_section(bad_section)
                with pytest.raises(ValueError, match="section name"):
                    git_config.set(bad_section, "name", "value")
                with pytest.raises(ValueError, match="section name"):
                    git_config.set_value(bad_section, "name", "value")
                with pytest.raises(ValueError, match="section name"):
                    git_config.add_value(bad_section, "name", "value")
                with pytest.raises(ValueError, match="section name"):
                    git_config.rename_section("user", bad_section)

            git_config.set_value("user", "name", "safe")
            git_config.set_value(safe_section, "name", "safe")
            self.assertEqual(git_config.get_value(safe_section, "name"), "safe")

        # A closing bracket inside a quoted subsection name is data, not a section terminator.
        with open(config_path, "rb") as config_file:
            self.assertIn(
                b'[submodule "docs]archive"]\n',
                config_file.read(),
                "a closing bracket within a quoted subsection name should be preserved",
            )

        # Reparse the file to verify that rejected names did not inject an [other] section.
        with GitConfigParser(config_path, read_only=True) as git_config:
            self.assertEqual(
                git_config.get_value("user", "name"),
                "safe",
                "rejected section names corrupted the existing section",
            )
            self.assertFalse(git_config.has_section("other"), "an unsafe section name injected an [other] section")

    @with_rw_directory
    def test_set_and_add_value_reject_unsafe_value_characters(self, rw_dir):
        config_path = osp.join(rw_dir, "config")
        bad_values = ("foo\rbar", "foo\nbar", "foo\x00bar", b"foo\nbar")

        with GitConfigParser(config_path, read_only=False) as git_config:
            git_config.add_section("user")
            for bad_value in bad_values:
                with pytest.raises(ValueError, match="CR, LF, or NUL"):
                    git_config.set("user", "name", bad_value)
                with pytest.raises(ValueError, match="CR, LF, or NUL"):
                    git_config.set_value("user", "name", bad_value)
                with pytest.raises(ValueError, match="CR, LF, or NUL"):
                    git_config.add_value("user", "name", bad_value)

            git_config.set_value("user", "name", "safe")

        with GitConfigParser(config_path, read_only=True) as git_config:
            self.assertEqual(git_config.get_value("user", "name"), "safe")

    def test_base(self):
        path_repo = fixture_path("git_config")
        path_global = fixture_path("git_config_global")
        r_config = GitConfigParser([path_repo, path_global], read_only=True)
        assert r_config.read_only
        num_sections = 0
        num_options = 0

        # Test reader methods.
        assert r_config._is_initialized is False
        for section in r_config.sections():
            num_sections += 1
            for option in r_config.options(section):
                num_options += 1
                val = r_config.get(section, option)
                val_typed = r_config.get_value(section, option)
                assert isinstance(val_typed, (bool, int, float, str))
                assert val
                assert "\n" not in option
                assert "\n" not in val

                # Writing must fail.
                with self.assertRaises(IOError):
                    r_config.set(section, option, None)
                with self.assertRaises(IOError):
                    r_config.remove_option(section, option)
            # END for each option
            with self.assertRaises(IOError):
                r_config.remove_section(section)
        # END for each section
        assert num_sections and num_options
        assert r_config._is_initialized is True

        # Get value which doesn't exist, with default.
        default = "my default value"
        assert r_config.get_value("doesnt", "exist", default) == default

        # It raises if there is no default though.
        with self.assertRaises(cp.NoSectionError):
            r_config.get_value("doesnt", "exist")

    @with_rw_directory
    def test_config_include(self, rw_dir):
        def write_test_value(cw, value):
            cw.set_value(value, "value", value)

        def check_test_value(cr, value):
            assert cr.get_value(value, "value") == value

        # PREPARE CONFIG FILE A
        fpa = osp.join(rw_dir, "a")
        with GitConfigParser(fpa, read_only=False) as cw:
            write_test_value(cw, "a")

            fpb = osp.join(rw_dir, "b")
            fpc = osp.join(rw_dir, "c")
            cw.set_value("include", "relative_path_b", "b")
            cw.set_value("include", "doesntexist", "foobar")
            cw.set_value("include", "relative_cycle_a_a", "a")
            cw.set_value("include", "absolute_cycle_a_a", fpa)
        assert osp.exists(fpa)

        # PREPARE CONFIG FILE B
        with GitConfigParser(fpb, read_only=False) as cw:
            write_test_value(cw, "b")
            cw.set_value("include", "relative_cycle_b_a", "a")
            cw.set_value("include", "absolute_cycle_b_a", fpa)
            cw.set_value("include", "relative_path_c", "c")
            cw.set_value("include", "absolute_path_c", fpc)

        # PREPARE CONFIG FILE C
        with GitConfigParser(fpc, read_only=False) as cw:
            write_test_value(cw, "c")

        with GitConfigParser(fpa, read_only=True) as cr:
            for tv in ("a", "b", "c"):
                check_test_value(cr, tv)
            # END for each test to verify
            assert len(cr.items("include")) == 8, "Expected all include sections to be merged"

        # Test writable config writers - assure write-back doesn't involve includes.
        with GitConfigParser(fpa, read_only=False, merge_includes=True) as cw:
            tv = "x"
            write_test_value(cw, tv)

        with GitConfigParser(fpa, read_only=True) as cr:
            with self.assertRaises(cp.NoSectionError):
                check_test_value(cr, tv)

        # But can make it skip includes altogether, and thus allow write-backs.
        with GitConfigParser(fpa, read_only=False, merge_includes=False) as cw:
            write_test_value(cw, tv)

        with GitConfigParser(fpa, read_only=True) as cr:
            check_test_value(cr, tv)

    @with_rw_directory
    def test_config_relative_path_include(self, rw_dir):
        included_path = osp.join(rw_dir, "included")
        with GitConfigParser(included_path, read_only=False) as cw:
            cw.set_value("included", "value", "included")

        config_path = osp.join(rw_dir, "config")
        with GitConfigParser(config_path, read_only=False) as cw:
            cw.set_value("include", "path", "included")

        if osp.splitdrive(config_path)[0] != osp.splitdrive(os.getcwd())[0]:
            pytest.skip("The temporary directory and checkout are on different drives")

        relative_config_path = osp.relpath(config_path)
        with GitConfigParser(relative_config_path, read_only=True) as cr:
            assert cr.get_value("included", "value") == "included"

    @pytest.mark.skipif(os.name != "nt", reason="Specifically for Windows drive-rooted paths.")
    @with_rw_directory
    def test_config_drive_rooted_path_include(self, rw_dir):
        with cwd(rw_dir):
            included_path = osp.join(rw_dir, "included")
            with GitConfigParser(included_path, read_only=False) as cw:
                cw.set_value("included", "value", "included")

            _drive, rooted_included_path = osp.splitdrive(included_path)
            config_path = osp.join(rw_dir, "config")
            with GitConfigParser(config_path, read_only=False) as cw:
                cw.set_value("include", "path", rooted_included_path)

            with GitConfigParser(config_path, read_only=True) as cr:
                assert cr.get_value("included", "value") == "included"

    @with_rw_directory
    def test_multiple_include_paths_with_same_key(self, rw_dir):
        """Test that multiple 'path' entries under [include] are all respected.

        Regression test for https://github.com/gitpython-developers/GitPython/issues/2099.
        Git config allows multiple ``path`` values under ``[include]``, e.g.::

            [include]
                path = file1
                path = file2

        Previously only one of these was included because _OMD.items() returns
        only the last value for each key.
        """
        # Create two config files to be included.
        fp_inc1 = osp.join(rw_dir, "inc1.cfg")
        fp_inc2 = osp.join(rw_dir, "inc2.cfg")
        fp_main = osp.join(rw_dir, "main.cfg")

        with GitConfigParser(fp_inc1, read_only=False) as cw:
            cw.set_value("user", "name", "from-inc1")

        with GitConfigParser(fp_inc2, read_only=False) as cw:
            cw.set_value("core", "bar", "from-inc2")

        # Write a config with two path entries under a single [include] section.
        # We write it manually because set_value would overwrite the key.
        with open(fp_main, "w") as f:
            f.write("[include]\n")
            f.write(f"\tpath = {fp_inc1}\n")
            f.write(f"\tpath = {fp_inc2}\n")

        with GitConfigParser(fp_main, read_only=True) as cr:
            # Both included files should be loaded.
            assert cr.get_value("user", "name") == "from-inc1"
            assert cr.get_value("core", "bar") == "from-inc2"

    @with_rw_directory
    def test_conditional_includes_from_git_dir(self, rw_dir):
        # Initiate repository path.
        git_dir = osp.join(rw_dir, "target1", "repo1")
        git_dir_pattern = git_dir.replace("\\", "/")
        os.makedirs(git_dir)

        # Initiate mocked repository.
        repo = mock.Mock(git_dir=git_dir)

        # Initiate config files.
        path1 = osp.join(rw_dir, "config1")
        path2 = osp.join(rw_dir, "config2")
        template = '[includeIf "{}:{}"]\n    path={}\n'

        with open(path1, "w") as stream:
            # on Windows, this writes a backslash pattern.
            stream.write(template.format("gitdir", git_dir, path2))

        # Ensure that config is ignored if no repo is set.
        with GitConfigParser(path1) as config:
            assert not config._has_includes()
            assert config._included_paths() == []

        # Git uses forward slashes in gitdir patterns on every platform:
        # backslashes escape the next pattern character rather than separate
        # path components. On Windows, GitPython therefore normalizes git_dir
        # to forward slashes but leaves this backslash pattern unchanged, so
        # the two do not match and no path is included.
        with GitConfigParser(path1, repo=repo, merge_includes=False) as config:
            expected_paths = [] if sys.platform == "win32" else [("path", path2)]
            assert config._included_paths() == expected_paths

        # Ensure that Git's forward-slash syntax matches native Windows paths.
        with open(path1, "w") as stream:
            stream.write(template.format("gitdir", git_dir_pattern, path2))

        with GitConfigParser(path1, repo=repo) as config:
            assert config._has_includes()
            assert config._included_paths() == [("path", path2)]

        # Ensure that config is ignored if case is incorrect.
        with open(path1, "w") as stream:
            stream.write(template.format("gitdir", git_dir_pattern.upper(), path2))

        with GitConfigParser(path1, repo=repo) as config:
            assert not config._has_includes()
            assert config._included_paths() == []

        # Ensure that config is included if case is ignored.
        with open(path1, "w") as stream:
            stream.write(template.format("gitdir/i", git_dir_pattern.upper(), path2))

        with GitConfigParser(path1, repo=repo) as config:
            assert config._has_includes()
            assert config._included_paths() == [("path", path2)]

        # Ensure that config is included with path using glob pattern.
        with open(path1, "w") as stream:
            stream.write(template.format("gitdir", "**/repo1", path2))

        with GitConfigParser(path1, repo=repo) as config:
            assert config._has_includes()
            assert config._included_paths() == [("path", path2)]

        # Ensure that config is ignored if path is not matching git_dir.
        with open(path1, "w") as stream:
            stream.write(template.format("gitdir", "incorrect", path2))

        with GitConfigParser(path1, repo=repo) as config:
            assert not config._has_includes()
            assert config._included_paths() == []

        # Ensure that config is included if path in hierarchy.
        with open(path1, "w") as stream:
            stream.write(template.format("gitdir", "target1/", path2))

        with GitConfigParser(path1, repo=repo) as config:
            assert config._has_includes()
            assert config._included_paths() == [("path", path2)]

    @with_rw_directory
    def test_conditional_includes_do_not_treat_backslashes_as_separators(self, rw_dir):
        git_dir = osp.join(rw_dir, "target", "repo")
        repo = mock.Mock(git_dir=git_dir)
        config_path = osp.join(rw_dir, "config")
        included_path = osp.join(rw_dir, "included")
        pattern = git_dir.replace("\\", "/").replace("/target/repo", R"/target\repo")

        with open(config_path, "w") as stream:
            stream.write(f'[includeIf "gitdir:{pattern}"]\n    path={included_path}\n')

        with GitConfigParser(config_path, repo=repo, merge_includes=False) as config:
            assert config._included_paths() == []

    @with_rw_directory
    def test_conditional_includes_from_branch_name(self, rw_dir):
        # Initiate mocked branch.
        branch = mock.Mock()
        type(branch).name = mock.PropertyMock(return_value="/foo/branch")

        # Initiate mocked repository.
        repo = mock.Mock(active_branch=branch)

        # Initiate config files.
        path1 = osp.join(rw_dir, "config1")
        path2 = osp.join(rw_dir, "config2")
        template = '[includeIf "onbranch:{}"]\n    path={}\n'

        # Ensure that config is included is branch is correct.
        with open(path1, "w") as stream:
            stream.write(template.format("/foo/branch", path2))

        with GitConfigParser(path1, repo=repo) as config:
            assert config._has_includes()
            assert config._included_paths() == [("path", path2)]

        # Ensure that config is included is branch is incorrect.
        with open(path1, "w") as stream:
            stream.write(template.format("incorrect", path2))

        with GitConfigParser(path1, repo=repo) as config:
            assert not config._has_includes()
            assert config._included_paths() == []

        # Ensure that config is included with branch using glob pattern.
        with open(path1, "w") as stream:
            stream.write(template.format("/foo/**", path2))

        with GitConfigParser(path1, repo=repo) as config:
            assert config._has_includes()
            assert config._included_paths() == [("path", path2)]

    @with_rw_directory
    def test_conditional_includes_from_branch_name_error(self, rw_dir):
        # Initiate mocked repository to raise an error if HEAD is detached.
        repo = mock.Mock()
        type(repo).active_branch = mock.PropertyMock(side_effect=TypeError)

        # Initiate config file.
        path1 = osp.join(rw_dir, "config1")

        # Ensure that config is ignored when active branch cannot be found.
        with open(path1, "w") as stream:
            stream.write('[includeIf "onbranch:foo"]\n    path=/path\n')

        with GitConfigParser(path1, repo=repo) as config:
            assert not config._has_includes()
            assert config._included_paths() == []

    @with_rw_directory
    def test_conditional_includes_remote_url(self, rw_dir):
        # Initiate mocked repository.
        repo = mock.Mock()
        repo.remotes = [mock.Mock(url="https://github.com/foo/repo")]

        # Initiate config files.
        path1 = osp.join(rw_dir, "config1")
        path2 = osp.join(rw_dir, "config2")
        template = '[includeIf "hasconfig:remote.*.url:{}"]\n    path={}\n'

        # Ensure that config with hasconfig and full url is correct.
        with open(path1, "w") as stream:
            stream.write(template.format("https://github.com/foo/repo", path2))

        with GitConfigParser(path1, repo=repo) as config:
            assert config._has_includes()
            assert config._included_paths() == [("path", path2)]

        # Ensure that config with hasconfig and incorrect url is incorrect.
        with open(path1, "w") as stream:
            stream.write(template.format("incorrect", path2))

        with GitConfigParser(path1, repo=repo) as config:
            assert not config._has_includes()
            assert config._included_paths() == []

        # Ensure that config with hasconfig and url using glob pattern is correct.
        with open(path1, "w") as stream:
            stream.write(template.format("**/**github.com*/**", path2))

        with GitConfigParser(path1, repo=repo) as config:
            assert config._has_includes()
            assert config._included_paths() == [("path", path2)]

    def test_rename(self):
        file_obj = self._to_memcache(fixture_path("git_config"))
        with GitConfigParser(file_obj, read_only=False, merge_includes=False) as cw:
            with self.assertRaises(ValueError):
                cw.rename_section("doesntexist", "foo")
            with self.assertRaises(ValueError):
                cw.rename_section("core", "include")

            nn = "bee"
            assert cw.rename_section("core", nn) is cw
            assert not cw.has_section("core")
            assert len(cw.items(nn)) == 4

    def test_complex_aliases(self):
        file_obj = self._to_memcache(fixture_path(".gitconfig"))
        with GitConfigParser(file_obj, read_only=False) as w_config:
            self.assertEqual(
                w_config.get("alias", "rbi"),
                "!g() { git rebase -i origin/${1:-master} ; } ; g",
            )
        self.assertEqual(
            file_obj.getvalue(),
            self._to_memcache(fixture_path(".gitconfig")).getvalue(),
        )

    def test_config_with_extra_whitespace(self):
        cr = GitConfigParser(fixture_path("git_config_with_extra_whitespace"), read_only=True)
        self.assertEqual(cr.get("init", "defaultBranch"), "trunk")

    def test_empty_config_value(self):
        cr = GitConfigParser(fixture_path("git_config_with_empty_value"), read_only=True)

        assert cr.get_value("core", "filemode"), "Should read keys with values"

        with self.assertRaises(cp.NoOptionError):
            cr.get_value("color", "ui")

    def test_config_with_quotes(self):
        cr = GitConfigParser(fixture_path("git_config_with_quotes"), read_only=True)

        self.assertEqual(cr.get("user", "name"), "Cody Veal")
        self.assertEqual(cr.get("user", "email"), "cveal05@gmail.com")

    def test_config_with_empty_quotes(self):
        cr = GitConfigParser(fixture_path("git_config_with_empty_quotes"), read_only=True)
        self.assertEqual(cr.get("core", "filemode"), "", "quotes can form a literal empty string as value")

    def test_config_with_quotes_with_literal_whitespace(self):
        cr = GitConfigParser(fixture_path("git_config_with_quotes_whitespace_inside"), read_only=True)
        self.assertEqual(cr.get("core", "commentString"), "# ")

    def test_config_with_quotes_with_whitespace_outside_value(self):
        cr = GitConfigParser(fixture_path("git_config_with_quotes_whitespace_outside"), read_only=True)
        self.assertEqual(cr.get("init", "defaultBranch"), "trunk")

    def test_config_with_quotes_containing_escapes(self):
        """Interpret Git's quoted escapes without changing malformed values."""
        cr = GitConfigParser(fixture_path("git_config_with_quotes_escapes"), read_only=True)

        self.assertEqual(cr.get("custom", "hasnewline"), "first\nsecond")
        self.assertEqual(cr.get("custom", "hasbackslash"), R"foo\bar")
        self.assertEqual(cr.get("custom", "hasquote"), 'ab"cd')
        self.assertEqual(cr.get("custom", "hastrailingbackslash"), "word\\")
        self.assertEqual(cr.get("custom", "hasunrecognized"), R"p\qrs")

        # It is less obvious whether and what to eventually do with this.
        self.assertEqual(cr.get("custom", "hasunescapedquotes"), '"ab"cd"e"')

        # Cases where quote removal is clearly safe should happen even after those.
        self.assertEqual(cr.get("custom", "ordinary"), "hello world")

        # Cases without quotes should still parse correctly even after those, too.
        self.assertEqual(cr.get("custom", "unquoted"), "good evening")

    def test_get_values_works_without_requiring_any_other_calls_first(self):
        file_obj = self._to_memcache(fixture_path("git_config_multiple"))
        cr = GitConfigParser(file_obj, read_only=True)
        self.assertEqual(cr.get_values("section0", "option0"), ["value0"])
        file_obj.seek(0)
        cr = GitConfigParser(file_obj, read_only=True)
        self.assertEqual(cr.get_values("section1", "option1"), ["value1a", "value1b"])
        file_obj.seek(0)
        cr = GitConfigParser(file_obj, read_only=True)
        self.assertEqual(cr.get_values("section1", "other_option1"), ["other_value1"])

    def test_multiple_values(self):
        file_obj = self._to_memcache(fixture_path("git_config_multiple"))
        with GitConfigParser(file_obj, read_only=False) as cw:
            self.assertEqual(cw.get("section0", "option0"), "value0")
            self.assertEqual(cw.get_values("section0", "option0"), ["value0"])
            self.assertEqual(cw.items("section0"), [("option0", "value0")])

            # Where there are multiple values, "get" returns the last.
            self.assertEqual(cw.get("section1", "option1"), "value1b")
            self.assertEqual(cw.get_values("section1", "option1"), ["value1a", "value1b"])
            self.assertEqual(
                cw.items("section1"),
                [("option1", "value1b"), ("other_option1", "other_value1")],
            )
            self.assertEqual(
                cw.items_all("section1"),
                [
                    ("option1", ["value1a", "value1b"]),
                    ("other_option1", ["other_value1"]),
                ],
            )
            with self.assertRaises(KeyError):
                cw.get_values("section1", "missing")

            self.assertEqual(cw.get_values("section1", "missing", 1), [1])
            self.assertEqual(cw.get_values("section1", "missing", "s"), ["s"])

    def test_multiple_values_rename(self):
        file_obj = self._to_memcache(fixture_path("git_config_multiple"))
        with GitConfigParser(file_obj, read_only=False) as cw:
            cw.rename_section("section1", "section2")
            cw.write()
            file_obj.seek(0)
            cr = GitConfigParser(file_obj, read_only=True)
            self.assertEqual(cr.get_value("section2", "option1"), "value1b")
            self.assertEqual(cr.get_values("section2", "option1"), ["value1a", "value1b"])
            self.assertEqual(
                cr.items("section2"),
                [("option1", "value1b"), ("other_option1", "other_value1")],
            )
            self.assertEqual(
                cr.items_all("section2"),
                [
                    ("option1", ["value1a", "value1b"]),
                    ("other_option1", ["other_value1"]),
                ],
            )

    def test_multiple_to_single(self):
        file_obj = self._to_memcache(fixture_path("git_config_multiple"))
        with GitConfigParser(file_obj, read_only=False) as cw:
            cw.set_value("section1", "option1", "value1c")

            cw.write()
            file_obj.seek(0)
            cr = GitConfigParser(file_obj, read_only=True)
            self.assertEqual(cr.get_value("section1", "option1"), "value1c")
            self.assertEqual(cr.get_values("section1", "option1"), ["value1c"])
            self.assertEqual(
                cr.items("section1"),
                [("option1", "value1c"), ("other_option1", "other_value1")],
            )
            self.assertEqual(
                cr.items_all("section1"),
                [("option1", ["value1c"]), ("other_option1", ["other_value1"])],
            )

    def test_single_to_multiple(self):
        file_obj = self._to_memcache(fixture_path("git_config_multiple"))
        with GitConfigParser(file_obj, read_only=False) as cw:
            cw.add_value("section1", "other_option1", "other_value1a")

            cw.write()
            file_obj.seek(0)
            cr = GitConfigParser(file_obj, read_only=True)
            self.assertEqual(cr.get_value("section1", "option1"), "value1b")
            self.assertEqual(cr.get_values("section1", "option1"), ["value1a", "value1b"])
            self.assertEqual(cr.get_value("section1", "other_option1"), "other_value1a")
            self.assertEqual(
                cr.get_values("section1", "other_option1"),
                ["other_value1", "other_value1a"],
            )
            self.assertEqual(
                cr.items("section1"),
                [("option1", "value1b"), ("other_option1", "other_value1a")],
            )
            self.assertEqual(
                cr.items_all("section1"),
                [
                    ("option1", ["value1a", "value1b"]),
                    ("other_option1", ["other_value1", "other_value1a"]),
                ],
            )

    def test_add_to_multiple(self):
        file_obj = self._to_memcache(fixture_path("git_config_multiple"))
        with GitConfigParser(file_obj, read_only=False) as cw:
            cw.add_value("section1", "option1", "value1c")
            cw.write()
            file_obj.seek(0)
            cr = GitConfigParser(file_obj, read_only=True)
            self.assertEqual(cr.get_value("section1", "option1"), "value1c")
            self.assertEqual(cr.get_values("section1", "option1"), ["value1a", "value1b", "value1c"])
            self.assertEqual(
                cr.items("section1"),
                [("option1", "value1c"), ("other_option1", "other_value1")],
            )
            self.assertEqual(
                cr.items_all("section1"),
                [
                    ("option1", ["value1a", "value1b", "value1c"]),
                    ("other_option1", ["other_value1"]),
                ],
            )

    def test_setlast(self):
        # Test directly, not covered by higher-level tests.
        omd = _OMD()
        omd.setlast("key", "value1")
        self.assertEqual(omd["key"], "value1")
        self.assertEqual(omd.getall("key"), ["value1"])
        omd.setlast("key", "value2")
        self.assertEqual(omd["key"], "value2")
        self.assertEqual(omd.getall("key"), ["value2"])
