Repository: gitpython-developers/GitPython
Pull request #877 (2019-05-21 15:03:29)

Fix for issue #876
Normalize path after joining submodule path and the relative path to the git dir, to eliminate path length errors on Windows


Patch:
diff --git a/.travis.yml b/.travis.yml
index aed714af..7939e161 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,5 @@
 language: python
 python:
-  - "2.7"
   - "3.4"
   - "3.5"
   - "3.6"
diff --git a/AUTHORS b/AUTHORS
index d2483dd4..5f42f856 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -29,5 +29,8 @@ Contributors are:
 -Tim Swast <swast _at_ google.com>
 -William Luc Ritchie
 -David Host <hostdm _at_ outlook.com>
+-A. Jesse Jiryu Davis <jesse _at_ emptysquare.net>
+-Steven Whitman <ninloot _at_ gmail.com>
+-Stefan Stancu <stefan.stancu _at_ gmail.com>
 
 Portions derived from other open source works and are clearly marked.
diff --git a/README.md b/README.md
index f5735bc2..3734019e 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ If it is not in your `PATH`, you can help GitPython find it by setting
 the `GIT_PYTHON_GIT_EXECUTABLE=<path/to/git>` environment variable.
 
 * Git (1.7.x or newer)
-* Python 2.7 to 3.7.
+* Python 3 to 3.7.
 
 The list of dependencies are listed in `./requirements.txt` and `./test-requirements.txt`.
 The installer takes care of installing them for you.
@@ -192,8 +192,10 @@ New BSD License.  See the LICENSE file.
 [![Code Climate](https://codeclimate.com/github/gitpython-developers/GitPython/badges/gpa.svg)](https://codeclimate.com/github/gitpython-developers/GitPython)
 [![Documentation Status](https://readthedocs.org/projects/gitpython/badge/?version=stable)](https://readthedocs.org/projects/gitpython/?badge=stable)
 [![Stories in Ready](https://badge.waffle.io/gitpython-developers/GitPython.png?label=ready&title=Ready)](https://waffle.io/gitpython-developers/GitPython)
+[![Packaging status](https://repology.org/badge/tiny-repos/python:gitpython.svg)](https://repology.org/metapackage/python:gitpython/versions)
 [![Throughput Graph](https://graphs.waffle.io/gitpython-developers/GitPython/throughput.svg)](https://waffle.io/gitpython-developers/GitPython/metrics/throughput)
 
+
 Now that there seems to be a massive user base, this should be motivation enough to let git-python
 return to a proper state, which means
 
diff --git a/doc/source/changes.rst b/doc/source/changes.rst
index 32e58c7f..92c28b69 100644
--- a/doc/source/changes.rst
+++ b/doc/source/changes.rst
@@ -2,6 +2,17 @@
 Changelog
 =========
 
+2.1.12 - Bugfixes and Features
+==============================
+
+* Multi-value support and interface improvements for Git configuration. Thanks to A. Jesse Jiryu Davis.
+
+see the following for (most) details:
+https://github.com/gitpython-developers/gitpython/milestone/27?closed=1
+
+or run have a look at the difference between tags v2.1.11 and v2.1.12:
+https://github.com/gitpython-developers/GitPython/compare/2.1.11...2.1.12
+
 2.1.11 - Bugfixes
 =================
 
diff --git a/doc/source/intro.rst b/doc/source/intro.rst
index d68e5eb2..e2cd196b 100644
--- a/doc/source/intro.rst
+++ b/doc/source/intro.rst
@@ -13,7 +13,7 @@ The object database implementation is optimized for handling large quantities of
 Requirements
 ============
 
-* `Python`_ 2.7 or newer
+* `Python`_ 3.0 or newer
 * `Git`_ 1.7.0 or newer
     It should also work with older versions, but it may be that some operations
     involving remotes will not work as expected.
diff --git a/doc/source/reference.rst b/doc/source/reference.rst
index 53fa8636..68a7f0ba 100644
--- a/doc/source/reference.rst
+++ b/doc/source/reference.rst
@@ -3,6 +3,13 @@
 API Reference
 =============
 
+Version
+-------
+
+.. py:data:: git.__version__
+
+   Current GitPython version.
+
 Objects.Base
 ------------
 
diff --git a/git/compat.py b/git/compat.py
index b63768f3..02dc69de 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -30,7 +30,10 @@ PY3 = sys.version_info[0] >= 3
 is_win = (os.name == 'nt')
 is_posix = (os.name == 'posix')
 is_darwin = (os.name == 'darwin')
-defenc = sys.getdefaultencoding()
+if hasattr(sys, 'getfilesystemencoding'):
+    defenc = sys.getfilesystemencoding()
+if defenc is None:
+    defenc = sys.getdefaultencoding()
 
 if PY3:
     import io
diff --git a/git/config.py b/git/config.py
index 68d65ae9..b03d9d42 100644
--- a/git/config.py
+++ b/git/config.py
@@ -146,6 +146,51 @@ class SectionConstraint(object):
         self._config.__exit__(exception_type, exception_value, traceback)
 
 
+class _OMD(OrderedDict):
+    """Ordered multi-dict."""
+
+    def __setitem__(self, key, value):
+        super(_OMD, self).__setitem__(key, [value])
+
+    def add(self, key, value):
+        if key not in self:
+            super(_OMD, self).__setitem__(key, [value])
+            return
+
+        super(_OMD, self).__getitem__(key).append(value)
+
+    def setall(self, key, values):
+        super(_OMD, self).__setitem__(key, values)
+
+    def __getitem__(self, key):
+        return super(_OMD, self).__getitem__(key)[-1]
+
+    def getlast(self, key):
+        return super(_OMD, self).__getitem__(key)[-1]
+
+    def setlast(self, key, value):
+        if key not in self:
+            super(_OMD, self).__setitem__(key, [value])
+            return
+
+        prior = super(_OMD, self).__getitem__(key)
+        prior[-1] = value
+
+    def get(self, key, default=None):
+        return super(_OMD, self).get(key, [default])[-1]
+
+    def getall(self, key):
+        return super(_OMD, self).__getitem__(key)
+
+    def items(self):
+        """List of (key, last value for key)."""
+        return [(k, self[k]) for k in self]
+
+    def items_all(self):
+        """List of (key, list of values for key)."""
+        return [(k, self.getall(k)) for k in self]
+
+
 class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, object)):
 
     """Implements specifics required to read git style configuration files.
@@ -200,7 +245,7 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
             contents into ours. This makes it impossible to write back an individual configuration file.
             Thus, if you want to modify a single configuration file, turn this off to leave the original
             dataset unaltered when reading it."""
-        cp.RawConfigParser.__init__(self, dict_type=OrderedDict)
+        cp.RawConfigParser.__init__(self, dict_type=_OMD)
 
         # Used in python 3, needs to stay in sync with sections for underlying implementation to work
         if not hasattr(self, '_proxies'):
@@ -348,7 +393,8 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
                         is_multi_line = True
                         optval = string_decode(optval[1:])
                     # end handle multi-line
-                    cursect[optname] = optval
+                    # preserves multiple values for duplicate optnames
+                    cursect.add(optname, optval)
                 else:
                     # check if it's an option with no value - it's just ignored by git
                     if not self.OPTVALUEONLY.match(line):
@@ -362,7 +408,8 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
                     is_multi_line = False
                     line = line[:-1]
                 # end handle quotations
-                cursect[optname] += string_decode(line)
+                optval = cursect.getlast(optname)
+                cursect.setlast(optname, optval + string_decode(line))
             # END parse section or option
         # END while reading
 
@@ -442,9 +489,12 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
         git compatible format"""
         def write_section(name, section_dict):
             fp.write(("[%s]\n" % name).encode(defenc))
-            for (key, value) in section_dict.items():
-                if key != "__name__":
-                    fp.write(("\t%s = %s\n" % (key, self._value_to_string(value).replace('\n', '\n\t'))).encode(defenc))
+            for (key, values) in section_dict.items_all():
+                if key == "__name__":
+                    continue
+
+                for v in values:
+                    fp.write(("\t%s = %s\n" % (key, self._value_to_string(v).replace('\n', '\n\t'))).encode(defenc))
                 # END if key is not __name__
         # END section writing
 
@@ -457,6 +507,22 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
         """:return: list((option, value), ...) pairs of all items in the given section"""
         return [(k, v) for k, v in super(GitConfigParser, self).items(section_name) if k != '__name__']
 
+    def items_all(self, section_name):
+        """:return: list((option, [values...]), ...) pairs of all items in the given section"""
+        rv = _OMD(self._defaults)
+
+        for k, vs in self._sections[section_name].items_all():
+            if k == '__name__':
+                continue
+
+            if k in rv and rv.getall(k) == vs:
+                continue
+
+            for v in vs:
+                rv.add(k, v)
+
+        return rv.items_all()
+
     @needs_values
     def write(self):
         """Write changes to our file, if there are changes at all
@@ -508,7 +574,11 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
         return self._read_only
 
     def get_value(self, section, option, default=None):
-        """
+        """Get an option's value.
+
+        If multiple values are specified for this option in the section, the
+        last one specified is returned.
+
         :param default:
             If not None, the given default value will be returned in case
             the option did not exist
@@ -523,6 +593,31 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
                 return default
             raise
 
+        return self._string_to_value(valuestr)
+
+    def get_values(self, section, option, default=None):
+        """Get an option's values.
+
+        If multiple values are specified for this option in the section, all are
+        returned.
+
+        :param default:
+            If not None, a list containing the given default value will be
+            returned in case the option did not exist
+        :return: a list of properly typed values, either int, float or string
+
+        :raise TypeError: in case the value could not be understood
+            Otherwise the exceptions known to the ConfigParser will be raised."""
+        try:
+            lst = self._sections[section].getall(option)
+        except Exception:
+            if default is not None:
+                return [default]
+            raise
+
+        return [self._string_to_value(valuestr) for valuestr in lst]
+
+    def _string_to_value(self, valuestr):
         types = (int, float)
         for numtype in types:
             try:
@@ -545,7 +640,9 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
             return True
 
         if not isinstance(valuestr, string_types):
-            raise TypeError("Invalid value type: only int, long, float and str are allowed", valuestr)
+            raise TypeError(
+                "Invalid value type: only int, long, float and str are allowed",
+                valuestr)
 
         return valuestr
 
@@ -572,6 +669,25 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
         self.set(section, option, self._value_to_string(value))
         return self
 
+    @needs_values
+    @set_dirty_and_flush_changes
+    def add_value(self, section, option, value):
+        """Adds a value for the given option in section.
+        It will create the section if required, and will not throw as opposed to the default
+        ConfigParser 'set' method. The value becomes the new value of the option as returned
+        by 'get_value', and appends to the list of values returned by 'get_values`'.
+
+        :param section: Name of the section in which the option resides or should reside
+        :param option: Name of the option
+
+        :param value: Value to add to option. It must be a string or convertible
+            to a string
+        :return: this instance"""
+        if not self.has_section(section):
+            self.add_section(section)
+        self._sections[section].add(option, self._value_to_string(value))
+        return self
+
     def rename_section(self, section, new_name):
         """rename the given section to new_name
         :raise ValueError: if section doesn't exit
@@ -584,8 +700,9 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
             raise ValueError("Destination section '%s' already exists" % new_name)
 
         super(GitConfigParser, self).add_section(new_name)
-        for k, v in self.items(section):
-            self.set(new_name, k, self._value_to_string(v))
+        new_section = self._sections[new_name]
+        for k, vs in self.items_all(section):
+            new_section.setall(k, vs)
         # end for each value to copy
 
         # This call writes back the changes, which is why we don't have the respective decorator
diff --git a/git/ext/gitdb b/git/ext/gitdb
index 3e718330..c0fd43b5 160000
--- a/git/ext/gitdb
+++ b/git/ext/gitdb
@@ -1,1 +1,1 @@
-Subproject commit 3e71833044718f60be36ac47494668469a2a235a
+Subproject commit c0fd43b5ff8c356fcf9cdebbbbd1803a502b4651
diff --git a/git/objects/tag.py b/git/objects/tag.py
index 19cb04bf..4295a03a 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -59,8 +59,9 @@ class TagObject(base.Object):
 
             self.tag = lines[2][4:]  # tag <tag name>
 
-            tagger_info = lines[3]  # tagger <actor> <date>
-            self.tagger, self.tagged_date, self.tagger_tz_offset = parse_actor_and_date(tagger_info)
+            if len(lines) > 3:
+                tagger_info = lines[3]  # tagger <actor> <date>
+                self.tagger, self.tagged_date, self.tagger_tz_offset = parse_actor_and_date(tagger_info)
 
             # line 4 empty - it could mark the beginning of the next header
             # in case there really is no message, it would not exist. Otherwise
diff --git a/git/remote.py b/git/remote.py
index 0965c1f6..4f32540f 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -544,10 +544,9 @@ class Remote(LazyMixin, Iterable):
                 except GitCommandError as ex:
                     if any(msg in str(ex) for msg in ['correct access rights', 'cannot run ssh']):
                         # If ssh is not setup to access this repository, see issue 694
-                        result = Git().execute(
-                            ['git', 'config', '--get', 'remote.%s.url' % self.name]
-                        )
-                        yield result
+                        remote_details = self.repo.git.config('--get-all', 'remote.%s.url' % self.name)
+                        for line in remote_details.split('\n'):
+                            yield line
                     else:
                         raise ex
             else:
diff --git a/git/repo/base.py b/git/repo/base.py
index 58f11e51..911494ad 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -4,6 +4,7 @@
 # This module is part of GitPython and is released under
 # the BSD License: http://www.opensource.org/licenses/bsd-license.php
 
+from builtins import str
 from collections import namedtuple
 import logging
 import os
@@ -559,11 +560,11 @@ class Repo(object):
         return res
 
     def is_ancestor(self, ancestor_rev, rev):
-        """Check if a commit  is an ancestor of another
+        """Check if a commit is an ancestor of another
 
         :param ancestor_rev: Rev which should be an ancestor
         :param rev: Rev to test against ancestor_rev
-        :return: ``True``, ancestor_rev is an accestor to rev.
+        :return: ``True``, ancestor_rev is an ancestor to rev.
         """
         try:
             self.git.merge_base(ancestor_rev, rev, is_ancestor=True)
@@ -931,7 +932,7 @@ class Repo(object):
         return cls(path, odbt=odbt)
 
     @classmethod
-    def _clone(cls, git, url, path, odb_default_type, progress, **kwargs):
+    def _clone(cls, git, url, path, odb_default_type, progress, multi_options=None, **kwargs):
         if progress is not None:
             progress = to_progress_instance(progress)
 
@@ -953,7 +954,10 @@ class Repo(object):
         sep_dir = kwargs.get('separate_git_dir')
         if sep_dir:
             kwargs['separate_git_dir'] = Git.polish_url(sep_dir)
-        proc = git.clone(Git.polish_url(url), clone_path, with_extended_output=True, as_process=True,
+        multi = None
+        if multi_options:
+            multi = ' '.join(multi_options).split(' ')
+        proc = git.clone(multi, Git.polish_url(url), clone_path, with_extended_output=True, as_process=True,
                          v=True, universal_newlines=True, **add_progress(kwargs, git, progress))
         if progress:
             handle_process_output(proc, None, progress.new_message_handler(), finalize_process, decode_streams=False)
@@ -983,33 +987,38 @@ class Repo(object):
         # END handle remote repo
         return repo
 
-    def clone(self, path, progress=None, **kwargs):
+    def clone(self, path, progress=None, multi_options=None, **kwargs):
         """Create a clone from this repository.
 
         :param path: is the full path of the new repo (traditionally ends with ./<name>.git).
         :param progress: See 'git.remote.Remote.push'.
+        :param multi_options: A list of Clone options that can be provided multiple times.  One
+            option per list item which is passed exactly as specified to clone.
+            For example ['--config core.filemode=false', '--config core.ignorecase',
+                         '--recurse-submodule=repo1_path', '--recurse-submodule=repo2_path']
         :param kwargs:
             * odbt = ObjectDatabase Type, allowing to determine the object database
               implementation used by the returned Repo instance
             * All remaining keyword arguments are given to the git-clone command
 
         :return: ``git.Repo`` (the newly cloned repo)"""
-        return self._clone(self.git, self.common_dir, path, type(self.odb), progress, **kwargs)
+        return self._clone(self.git, self.common_dir, path, type(self.odb), progress, multi_options, **kwargs)
 
     @classmethod
-    def clone_from(cls, url, to_path, progress=None, env=None, **kwargs):
+    def clone_from(cls, url, to_path, progress=None, env=None, multi_options=None, **kwargs):
         """Create a clone from the given URL
 
         :param url: valid git url, see http://www.kernel.org/pub/software/scm/git/docs/git-clone.html#URLS
         :param to_path: Path to which the repository should be cloned to
         :param progress: See 'git.remote.Remote.push'.
         :param env: Optional dictionary containing the desired environment variables.
+        :param mutli_options: See ``clone`` method
         :param kwargs: see the ``clone`` method
         :return: Repo instance pointing to the cloned directory"""
         git = Git(os.getcwd())
         if env is not None:
             git.update_environment(**env)
-        return cls._clone(git, url, to_path, GitCmdObjectDB, progress, **kwargs)
+        return cls._clone(git, url, to_path, GitCmdObjectDB, progress, multi_options, **kwargs)
 
     def archive(self, ostream, treeish=None, prefix=None, **kwargs):
         """Archive the tree at the given revision.
diff --git a/git/repo/fun.py b/git/repo/fun.py
index 6aefd9d6..5a47fff3 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -86,7 +86,7 @@ def find_submodule_git_dir(d):
                 ## Cygwin creates submodules prefixed with `/cygdrive/...` suffixes.
                 path = decygpath(path)
             if not osp.isabs(path):
-                path = osp.join(osp.dirname(d), path)
+                path = osp.normpath(osp.join(osp.dirname(d), path))
             return find_submodule_git_dir(path)
     # end handle exception
     return None
diff --git a/git/util.py b/git/util.py
index a3b1fbfb..3ba58857 100644
--- a/git/util.py
+++ b/git/util.py
@@ -534,8 +534,8 @@ class Actor(object):
     can be committers and authors or anything with a name and an email as
     mentioned in the git log entries."""
     # PRECOMPILED REGEX
-    name_only_regex = re.compile(r'<(.+)>')
-    name_email_regex = re.compile(r'(.*) <(.+?)>')
+    name_only_regex = re.compile(r'<(.*)>')
+    name_email_regex = re.compile(r'(.*) <(.*?)>')
 
     # ENVIRONMENT VARIABLES
     # read when creating new commits
diff --git a/requirements.txt b/requirements.txt
index 63d5ddfe..c0cca9f4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,1 +1,3 @@
 gitdb2 (>=2.0.0)
+gitdb>=0.6.4
+ddt>=1.1.1
diff --git a/setup.py b/setup.py
index 9959fdbd..65656d55 100755
--- a/setup.py
+++ b/setup.py
@@ -79,8 +79,8 @@ setup(
     package_data={'git.test': ['fixtures/*']},
     package_dir={'git': 'git'},
     license="BSD License",
-    python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
-    requires=requirements,
+    python_requires='>=3.0, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
+    install_requires=requirements,
     tests_require=requirements + test_requirements,
     zip_safe=False,
     long_description="""GitPython is a python library used to interact with Git repositories""",
@@ -102,8 +102,6 @@ setup(
         "Operating System :: Microsoft :: Windows",
         "Operating System :: MacOS :: MacOS X",
         "Programming Language :: Python",
-        "Programming Language :: Python :: 2",
-        "Programming Language :: Python :: 2.7",
         "Programming Language :: Python :: 3",
         "Programming Language :: Python :: 3.4",
         "Programming Language :: Python :: 3.5",


Test patch:
diff --git a/git/test/fixtures/git_config_multiple b/git/test/fixtures/git_config_multiple
new file mode 100644
index 00000000..03a97568
--- /dev/null
+++ b/git/test/fixtures/git_config_multiple
@@ -0,0 +1,7 @@
+[section0]
+    option0 = value0
+
+[section1]
+    option1 = value1a
+    option1 = value1b
+    other_option1 = other_value1
diff --git a/git/test/test_config.py b/git/test/test_config.py
index 4d6c8236..93f94748 100644
--- a/git/test/test_config.py
+++ b/git/test/test_config.py
@@ -11,7 +11,7 @@ from git import (
     GitConfigParser
 )
 from git.compat import string_types
-from git.config import cp
+from git.config import _OMD, cp
 from git.test.lib import (
     TestCase,
     fixture_path,
@@ -265,3 +265,110 @@ class TestBase(TestCase):
 
         with self.assertRaises(cp.NoOptionError):
             cr.get_value('color', 'ui')
+
+    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'])
diff --git a/git/test/test_repo.py b/git/test/test_repo.py
index 7fc49f3b..0577bd58 100644
--- a/git/test/test_repo.py
+++ b/git/test/test_repo.py
@@ -229,6 +229,22 @@ class TestRepo(TestBase):
 
         Repo.clone_from(original_repo.git_dir, pathlib.Path(rw_dir) / "clone_pathlib")
 
+    @with_rw_directory
+    def test_clone_from_pathlib_withConfig(self, rw_dir):
+        if pathlib is None:  # pythons bellow 3.4 don't have pathlib
+            raise SkipTest("pathlib was introduced in 3.4")
+
+        original_repo = Repo.init(osp.join(rw_dir, "repo"))
+
+        cloned = Repo.clone_from(original_repo.git_dir, pathlib.Path(rw_dir) / "clone_pathlib_withConfig",
+                                 multi_options=["--recurse-submodules=repo",
+                                                "--config core.filemode=false",
+                                                "--config submodule.repo.update=checkout"])
+
+        assert_equal(cloned.config_reader().get_value('submodule', 'active'), 'repo')
+        assert_equal(cloned.config_reader().get_value('core', 'filemode'), False)
+        assert_equal(cloned.config_reader().get_value('submodule "repo"', 'update'), 'checkout')
+
     @with_rw_repo('HEAD')
     def test_max_chunk_size(self, repo):
         class TestOutputStream(object):
diff --git a/git/test/test_util.py b/git/test/test_util.py
index 9c993205..b5f9d222 100644
--- a/git/test/test_util.py
+++ b/git/test/test_util.py
@@ -212,6 +212,12 @@ class TestUtils(TestBase):
             self.assertIsInstance(Actor.author(cr), Actor)
         # END assure config reader is handled
 
+    def test_actor_from_string(self):
+        self.assertEqual(Actor._from_string("name"), Actor("name", None))
+        self.assertEqual(Actor._from_string("name <>"), Actor("name", ""))
+        self.assertEqual(Actor._from_string("name last another <some-very-long-email@example.com>"),
+                         Actor("name last another", "some-very-long-email@example.com"))
+
     @ddt.data(('name', ''), ('name', 'prefix_'))
     def test_iterable_list(self, case):
         name, prefix = case