Repository: gitpython-developers/GitPython
Pull request #956 (2019-10-26 16:58:06)

Check if submodule exists before referencing
If you don't set the recursive option when you git clone
The following error:

```
Exception in thread Thread-1:
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/git/objects/submodule/base.py", line 1037, in module
    repo = git.Repo(module_checkout_abspath)
  File "/usr/local/lib/python3.7/site-packages/git/repo/base.py", line 184, in __init__
    raise InvalidGitRepositoryError(epath)
git.exc.InvalidGitRepositoryError: /Users/tanaga/Inci_e3b_modules/Agents_e3b

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/git/cmd.py", line 92, in pump_stream
    handler(line)
  File "/usr/local/lib/python3.7/site-packages/git/diff.py", line 534, in handle_diff_line
    '', change_type, score)
  File "/usr/local/lib/python3.7/site-packages/git/diff.py", line 286, in __init__
    repo = submodule.module()
  File "/usr/local/lib/python3.7/site-packages/git/util.py", line 72, in wrapper
    return func(self, *args, **kwargs)
  File "/usr/local/lib/python3.7/site-packages/git/objects/submodule/base.py", line 1042, in module
    raise InvalidGitRepositoryError("No valid repository at %s" % module_checkout_abspath)
git.exc.InvalidGitRepositoryError: No valid repository at /Users/tanaga/Inci_e3b_modules/Agents_e3b

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/Cellar/python/3.7.2_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/threading.py", line 917, in _bootstrap_inner
    self.run()
  File "/usr/local/Cellar/python/3.7.2_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/threading.py", line 865, in run
    self._target(*self._args, **self._kwargs)
  File "/usr/local/lib/python3.7/site-packages/git/cmd.py", line 95, in pump_stream
    raise CommandError(['<%s-pump>' % name] + cmdline, ex)
git.exc.CommandError: Cmd('<stdout-pump>') failed due to: InvalidGitRepositoryError('No valid repository at /Users/tanaga/Inci_e3b_modules/Agents_e3b')
  cmdline: <stdout-pump> git diff-tree 1503302cd7ddc45641c8b03255e4c6cb6cebbcbe f900136f66bcbc675b24fd44a26c1a06520c7ed1 -r --abbrev=40 --full-index -M --raw --no-color
```

This is useful for ci



Patch:
diff --git a/git/cmd.py b/git/cmd.py
index 484a0181..1cb3df34 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -401,7 +401,7 @@ class Git(LazyMixin):
             :raise GitCommandError: if the return status is not 0"""
             if stderr is None:
                 stderr = b''
-            stderr = force_bytes(stderr)
+            stderr = force_bytes(data=stderr, encoding='utf-8')
 
             status = self.proc.wait()
 
diff --git a/git/config.py b/git/config.py
index edd5750b..762069c7 100644
--- a/git/config.py
+++ b/git/config.py
@@ -20,7 +20,8 @@ from git.compat import (
     defenc,
     force_text,
     with_metaclass,
-    PY3
+    PY3,
+    is_win,
 )
 from git.util import LockFile
 
@@ -40,6 +41,10 @@ __all__ = ('GitConfigParser', 'SectionConstraint')
 log = logging.getLogger('git.config')
 log.addHandler(logging.NullHandler())
 
+# invariants
+# represents the configuration level of a configuration file
+CONFIG_LEVELS = ("system", "user", "global", "repository")
+
 
 class MetaParserBuilder(abc.ABCMeta):
 
@@ -191,6 +196,26 @@ class _OMD(OrderedDict):
         return [(k, self.getall(k)) for k in self]
 
 
+def get_config_path(config_level):
+
+    # we do not support an absolute path of the gitconfig on windows ,
+    # use the global config instead
+    if is_win and config_level == "system":
+        config_level = "global"
+
+    if config_level == "system":
+        return "/etc/gitconfig"
+    elif config_level == "user":
+        config_home = os.environ.get("XDG_CONFIG_HOME") or osp.join(os.environ.get("HOME", '~'), ".config")
+        return osp.normpath(osp.expanduser(osp.join(config_home, "git", "config")))
+    elif config_level == "global":
+        return osp.normpath(osp.expanduser("~/.gitconfig"))
+    elif config_level == "repository":
+        raise ValueError("No repo to get repository configuration from. Use Repo._get_config_path")
+
+    raise ValueError("Invalid configuration level: %r" % config_level)
+
+
 class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, object)):
 
     """Implements specifics required to read git style configuration files.
@@ -229,7 +254,7 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
     # list of RawConfigParser methods able to change the instance
     _mutating_methods_ = ("add_section", "remove_section", "remove_option", "set")
 
-    def __init__(self, file_or_files, read_only=True, merge_includes=True):
+    def __init__(self, file_or_files=None, read_only=True, merge_includes=True, config_level=None):
         """Initialize a configuration reader to read the given file_or_files and to
         possibly allow changes to it by setting read_only False
 
@@ -251,7 +276,17 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
         if not hasattr(self, '_proxies'):
             self._proxies = self._dict()
 
-        self._file_or_files = file_or_files
+        if file_or_files is not None:
+            self._file_or_files = file_or_files
+        else:
+            if config_level is None:
+                if read_only:
+                    self._file_or_files = [get_config_path(f) for f in CONFIG_LEVELS if f != 'repository']
+                else:
+                    raise ValueError("No configuration level or configuration files specified")
+            else:
+                self._file_or_files = [get_config_path(config_level)]
+
         self._read_only = read_only
         self._dirty = False
         self._is_initialized = False
diff --git a/git/diff.py b/git/diff.py
index a0076f0c..fcf40f2b 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -283,7 +283,8 @@ class Diff(object):
         if repo and a_rawpath:
             for submodule in repo.submodules:
                 if submodule.path == a_rawpath.decode("utf-8"):
-                    repo = submodule.module()
+                    if submodule.module_exists():
+                        repo = submodule.module()
                     break
 
         if a_blob_id is None or a_blob_id == self.NULL_HEX_SHA:


Test patch:
diff --git a/git/test/test_repo.py b/git/test/test_repo.py
index de1e951a..c59203ed 100644
--- a/git/test/test_repo.py
+++ b/git/test/test_repo.py
@@ -245,6 +245,20 @@ class TestRepo(TestBase):
         assert_equal(cloned.config_reader().get_value('core', 'filemode'), False)
         assert_equal(cloned.config_reader().get_value('submodule "repo"', 'update'), 'checkout')
 
+    def test_clone_from_with_path_contains_unicode(self):
+        with tempfile.TemporaryDirectory() as tmpdir:
+            unicode_dir_name = '\u0394'
+            path_with_unicode = os.path.join(tmpdir, unicode_dir_name)
+            os.makedirs(path_with_unicode)
+
+            try:
+                Repo.clone_from(
+                    url=self._small_repo_url(),
+                    to_path=path_with_unicode,
+                )
+            except UnicodeEncodeError:
+                self.fail('Raised UnicodeEncodeError')
+
     @with_rw_repo('HEAD')
     def test_max_chunk_size(self, repo):
         class TestOutputStream(object):