Repository: gitpython-developers/GitPython
Pull request #934 (2019-10-10 12:37:48)

Fixed#731
Added check for local file `url` starting with `$HOME` / `~` to expand them using `os.path.expanduser`.

stsewd: As a more general solution we could use `expandvars` https://docs.python.org/3/library/os.path.html#os.path.expandvars

Patch:
diff --git a/.deepsource.toml b/.deepsource.toml
index 1dbc3878..6e2f9d92 100644
--- a/.deepsource.toml
+++ b/.deepsource.toml
@@ -12,4 +12,4 @@ exclude_patterns = [
 [[analyzers]]
 name = 'python'
 enabled = true
-runtime_version = '2.x.x'
+runtime_version = '3.x.x'
diff --git a/.gitignore b/.gitignore
index ff1992dc..1fa8458b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,5 @@ nbproject
 /*egg-info
 /.tox
 /.vscode/
+.idea/
+.cache/
diff --git a/.gitmodules b/.gitmodules
index 4a3f37c2..251eeeec 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,3 @@
-[submodule "gitdb"]
-	url = https://github.com/gitpython-developers/gitdb.git
-	path = git/ext/gitdb
+[submodule "gitdb"]
+	url = https://github.com/gitpython-developers/gitdb.git
+	path = git/ext/gitdb
diff --git a/git/cmd.py b/git/cmd.py
index 50b1e321..78319c75 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -330,6 +330,9 @@ class Git(LazyMixin):
             but git stops liking them as it will escape the backslashes.
             Hence we undo the escaping just to be sure.
             """
+            url = os.path.expandvars(url)
+            if url.startswith('~'):
+                url = os.path.expanduser(url)
             url = url.replace("\\\\", "\\").replace("\\", "/")
 
         return url
@@ -375,7 +378,6 @@ class Git(LazyMixin):
                 proc.wait()    # ensure process goes away
             except OSError as ex:
                 log.info("Ignored error after process had died: %r", ex)
-                pass  # ignore error when process already died
             except AttributeError:
                 # try windows
                 # for some reason, providing None for stdout/stderr still prints something. This is why
diff --git a/git/compat.py b/git/compat.py
index 02dc69de..e88ca9b5 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -145,14 +145,12 @@ FS_ERRORS = 'surrogateescape'
 def u(text):
     if PY3:
         return text
-    else:
-        return text.decode('unicode_escape')
+    return text.decode('unicode_escape')
 
 def b(data):
     if PY3:
         return data.encode('latin1')
-    else:
-        return data
+    return data
 
 if PY3:
     _unichr = chr
@@ -282,8 +280,7 @@ def encodefilename(fn):
                 ch_utf8 = ch.encode('utf-8')
                 encoded.append(ch_utf8)
         return bytes().join(encoded)
-    else:
-        return fn.encode(FS_ENCODING, FS_ERRORS)
+    return fn.encode(FS_ENCODING, FS_ERRORS)
 
 def decodefilename(fn):
     return fn.decode(FS_ENCODING, FS_ERRORS)
diff --git a/git/config.py b/git/config.py
index b03d9d42..edd5750b 100644
--- a/git/config.py
+++ b/git/config.py
@@ -339,8 +339,7 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
 
             if PY3:
                 return v.encode(defenc).decode('unicode_escape')
-            else:
-                return v.decode('string_escape')
+            return v.decode('string_escape')
             # end
         # end
 
diff --git a/git/db.py b/git/db.py
index 653fa7da..9b334528 100644
--- a/git/db.py
+++ b/git/db.py
@@ -51,7 +51,7 @@ class GitCmdObjectDB(LooseObjectDB):
         :note: currently we only raise BadObject as git does not communicate
             AmbiguousObjects separately"""
         try:
-            hexsha, typename, size = self._git.get_object_header(partial_hexsha)  # @UnusedVariable
+            hexsha, _typename, _size = self._git.get_object_header(partial_hexsha)
             return hex_to_bin(hexsha)
         except (GitCommandError, ValueError):
             raise BadObject(partial_hexsha)
diff --git a/git/index/base.py b/git/index/base.py
index 04a3934d..b8c9d5e6 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -158,7 +158,7 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable):
 
     def _deserialize(self, stream):
         """Initialize this instance with index values read from the given stream"""
-        self.version, self.entries, self._extension_data, conten_sha = read_cache(stream)  # @UnusedVariable
+        self.version, self.entries, self._extension_data, _conten_sha = read_cache(stream)
         return self
 
     def _entries_sorted(self):
@@ -392,7 +392,7 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable):
                     continue
             # END glob handling
             try:
-                for root, dirs, files in os.walk(abs_path, onerror=raise_exc):  # @UnusedVariable
+                for root, _dirs, files in os.walk(abs_path, onerror=raise_exc):
                     for rela_file in files:
                         # add relative paths only
                         yield osp.join(root.replace(rs, ''), rela_file)
@@ -569,6 +569,9 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable):
         """ Split the items into two lists of path strings and BaseEntries. """
         paths = []
         entries = []
+        # if it is a string put in list
+        if isinstance(items, str):
+            items = [items]
 
         for item in items:
             if isinstance(item, string_types):
@@ -580,7 +583,7 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable):
             else:
                 raise TypeError("Invalid Type: %r" % item)
         # END for each item
-        return (paths, entries)
+        return paths, entries
 
     def _store_path(self, filepath, fprogress):
         """Store file at filepath in the database and return the base index entry
@@ -801,6 +804,10 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable):
         """Returns a list of repo-relative paths from the given items which
         may be absolute or relative paths, entries or blobs"""
         paths = []
+        # if string put in list
+        if isinstance(items, str):
+            items = [items]
+
         for item in items:
             if isinstance(item, (BaseIndexEntry, (Blob, Submodule))):
                 paths.append(self._to_relative_path(item.path))
diff --git a/git/index/fun.py b/git/index/fun.py
index c8912dd2..5906a358 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -173,8 +173,7 @@ def entry_key(*entry):
     :param entry: One instance of type BaseIndexEntry or the path and the stage"""
     if len(entry) == 1:
         return (entry[0].path, entry[0].stage)
-    else:
-        return tuple(entry)
+    return tuple(entry)
     # END handle entry
 
 
@@ -265,7 +264,7 @@ def write_tree_from_cache(entries, odb, sl, si=0):
 
             # enter recursion
             # ci - 1 as we want to count our current item as well
-            sha, tree_entry_list = write_tree_from_cache(entries, odb, slice(ci - 1, xi), rbound + 1)  # @UnusedVariable
+            sha, _tree_entry_list = write_tree_from_cache(entries, odb, slice(ci - 1, xi), rbound + 1)
             tree_items_append((sha, S_IFDIR, base))
 
             # skip ahead
diff --git a/git/objects/commit.py b/git/objects/commit.py
index 916a1081..f7201d90 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -140,7 +140,7 @@ class Commit(base.Object, Iterable, Diffable, Traversable, Serializable):
     def _set_cache_(self, attr):
         if attr in Commit.__slots__:
             # read the data in a chunk, its faster - then provide a file wrapper
-            binsha, typename, self.size, stream = self.repo.odb.stream(self.binsha)  # @UnusedVariable
+            _binsha, _typename, self.size, stream = self.repo.odb.stream(self.binsha)
             self._deserialize(BytesIO(stream.read()))
         else:
             super(Commit, self)._set_cache_(attr)
@@ -174,8 +174,7 @@ class Commit(base.Object, Iterable, Diffable, Traversable, Serializable):
         # as the empty paths version will ignore merge commits for some reason.
         if paths:
             return len(self.repo.git.rev_list(self.hexsha, '--', paths, **kwargs).splitlines())
-        else:
-            return len(self.repo.git.rev_list(self.hexsha, **kwargs).splitlines())
+        return len(self.repo.git.rev_list(self.hexsha, **kwargs).splitlines())
 
     @property
     def name_rev(self):
diff --git a/git/objects/fun.py b/git/objects/fun.py
index 38dce0a5..dc879fd2 100644
--- a/git/objects/fun.py
+++ b/git/objects/fun.py
@@ -156,7 +156,7 @@ def traverse_trees_recursive(odb, tree_shas, path_prefix):
             # END skip already done items
             entries = [None for _ in range(nt)]
             entries[ti] = item
-            sha, mode, name = item                          # its faster to unpack @UnusedVariable
+            _sha, mode, name = item
             is_dir = S_ISDIR(mode)                          # type mode bits
 
             # find this item in all other tree data items
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 384f7c5f..bc76bcce 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -233,8 +233,7 @@ class Submodule(IndexObject, Iterable, Traversable):
     def _module_abspath(cls, parent_repo, path, name):
         if cls._need_gitfile_submodules(parent_repo.git):
             return osp.join(parent_repo.git_dir, 'modules', name)
-        else:
-            return osp.join(parent_repo.working_tree_dir, path)
+        return osp.join(parent_repo.working_tree_dir, path)
         # end
 
     @classmethod
diff --git a/git/objects/tag.py b/git/objects/tag.py
index 4295a03a..017c4988 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -52,8 +52,8 @@ class TagObject(base.Object):
             ostream = self.repo.odb.stream(self.binsha)
             lines = ostream.read().decode(defenc).splitlines()
 
-            obj, hexsha = lines[0].split(" ")       # object <hexsha> @UnusedVariable
-            type_token, type_name = lines[1].split(" ")  # type <type_name> @UnusedVariable
+            _obj, hexsha = lines[0].split(" ")
+            _type_token, type_name = lines[1].split(" ")
             self.object = \
                 get_object_type_by_name(type_name.encode('ascii'))(self.repo, hex_to_bin(hexsha))
 
diff --git a/git/refs/head.py b/git/refs/head.py
index 4b0abb06..cc838590 100644
--- a/git/refs/head.py
+++ b/git/refs/head.py
@@ -219,8 +219,7 @@ class Head(Reference):
         self.repo.git.checkout(self, **kwargs)
         if self.repo.head.is_detached:
             return self.repo.head
-        else:
-            return self.repo.active_branch
+        return self.repo.active_branch
 
     #{ Configuration
     def _config_parser(self, read_only):
diff --git a/git/refs/log.py b/git/refs/log.py
index fc962680..e8c2d7ad 100644
--- a/git/refs/log.py
+++ b/git/refs/log.py
@@ -39,10 +39,9 @@ class RefLogEntry(tuple):
         res = self.format()
         if PY3:
             return res
-        else:
-            # repr must return a string, which it will auto-encode from unicode using the default encoding.
-            # This usually fails, so we encode ourselves
-            return res.encode(defenc)
+        # repr must return a string, which it will auto-encode from unicode using the default encoding.
+        # This usually fails, so we encode ourselves
+        return res.encode(defenc)
 
     def format(self):
         """:return: a string suitable to be placed in a reflog file"""
diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index a8ca6538..766037c1 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -614,7 +614,7 @@ class SymbolicReference(object):
         # END for each directory to walk
 
         # read packed refs
-        for sha, rela_path in cls._iter_packed_refs(repo):  # @UnusedVariable
+        for _sha, rela_path in cls._iter_packed_refs(repo):
             if rela_path.startswith(common_path):
                 rela_paths.add(rela_path)
             # END relative path matches common path
diff --git a/git/remote.py b/git/remote.py
index 73f10286..23337a9c 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -75,8 +75,7 @@ def to_progress_instance(progress):
         return RemoteProgress()
 
     # assume its the old API with an instance of RemoteProgress.
-    else:
-        return progress
+    return progress
 
 
 class PushInfo(object):
@@ -183,7 +182,7 @@ class PushInfo(object):
             split_token = "..."
             if control_character == " ":
                 split_token = ".."
-            old_sha, new_sha = summary.split(' ')[0].split(split_token)  # @UnusedVariable
+            old_sha, _new_sha = summary.split(' ')[0].split(split_token)
             # have to use constructor here as the sha usually is abbreviated
             old_commit = old_sha
         # END message handling
@@ -295,7 +294,7 @@ class FetchInfo(object):
         # parse lines
         control_character, operation, local_remote_ref, remote_local_ref, note = match.groups()
         try:
-            new_hex_sha, fetch_operation, fetch_note = fetch_line.split("\t")  # @UnusedVariable
+            _new_hex_sha, _fetch_operation, fetch_note = fetch_line.split("\t")
             ref_type_name, fetch_note = fetch_note.split(' ', 1)
         except ValueError:  # unpack error
             raise ValueError("Failed to parse FETCH_HEAD line: %r" % fetch_line)
diff --git a/git/repo/base.py b/git/repo/base.py
index 4d701153..106ed2eb 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -478,8 +478,7 @@ class Repo(object):
         :return: ``git.Commit``"""
         if rev is None:
             return self.head.commit
-        else:
-            return self.rev_parse(text_type(rev) + "^0")
+        return self.rev_parse(text_type(rev) + "^0")
 
     def iter_trees(self, *args, **kwargs):
         """:return: Iterator yielding Tree objects
@@ -501,8 +500,7 @@ class Repo(object):
             operations might have unexpected results."""
         if rev is None:
             return self.head.commit.tree
-        else:
-            return self.rev_parse(text_type(rev) + "^{tree}")
+        return self.rev_parse(text_type(rev) + "^{tree}")
 
     def iter_commits(self, rev=None, paths='', **kwargs):
         """A list of Commit objects representing the history of a given ref/commit
@@ -601,8 +599,7 @@ class Repo(object):
             with open(alternates_path, 'rb') as f:
                 alts = f.read().decode(defenc)
             return alts.strip().splitlines()
-        else:
-            return []
+        return []
 
     def _set_alternates(self, alts):
         """Sets the alternates
diff --git a/git/util.py b/git/util.py
index 7c07b0f3..99600e37 100644
--- a/git/util.py
+++ b/git/util.py
@@ -416,7 +416,7 @@ class RemoteProgress(object):
         # END could not get match
 
         op_code = 0
-        remote, op_name, percent, cur_count, max_count, message = match.groups()  # @UnusedVariable
+        _remote, op_name, _percent, cur_count, max_count, message = match.groups()
 
         # get operation id
         if op_name == "Counting objects":
@@ -577,9 +577,8 @@ class Actor(object):
             m = cls.name_only_regex.search(string)
             if m:
                 return Actor(m.group(1), None)
-            else:
-                # assume best and use the whole string as name
-                return Actor(string, None)
+            # assume best and use the whole string as name
+            return Actor(string, None)
             # END special case name
         # END handle name/email matching
 


Test patch:
diff --git a/git/test/performance/test_streams.py b/git/test/performance/test_streams.py
index 2e3772a0..cc6f0335 100644
--- a/git/test/performance/test_streams.py
+++ b/git/test/performance/test_streams.py
@@ -121,7 +121,7 @@ class TestObjDBPerformance(TestBigRepoR):
 
             # read all
             st = time()
-            hexsha, typename, size, data = rwrepo.git.get_object_data(gitsha)  # @UnusedVariable
+            _hexsha, _typename, size, data = rwrepo.git.get_object_data(gitsha)
             gelapsed_readall = time() - st
             print("Read %i KiB of %s data at once using git-cat-file in %f s ( %f Read KiB / s)"
                   % (size_kib, desc, gelapsed_readall, size_kib / gelapsed_readall), file=sys.stderr)
@@ -132,7 +132,7 @@ class TestObjDBPerformance(TestBigRepoR):
 
             # read chunks
             st = time()
-            hexsha, typename, size, stream = rwrepo.git.stream_object_data(gitsha)  # @UnusedVariable
+            _hexsha, _typename, size, stream = rwrepo.git.stream_object_data(gitsha)
             while True:
                 data = stream.read(cs)
                 if len(data) < cs:
diff --git a/git/test/test_commit.py b/git/test/test_commit.py
index cd6c5d5f..96a03b20 100644
--- a/git/test/test_commit.py
+++ b/git/test/test_commit.py
@@ -125,7 +125,7 @@ class TestCommit(TestBase):
         check_entries(stats.total)
         assert "files" in stats.total
 
-        for filepath, d in stats.files.items():  # @UnusedVariable
+        for _filepath, d in stats.files.items():
             check_entries(d)
         # END for each stated file
 
diff --git a/git/test/test_docs.py b/git/test/test_docs.py
index c14f5ff3..a1fea454 100644
--- a/git/test/test_docs.py
+++ b/git/test/test_docs.py
@@ -349,7 +349,7 @@ class Tutorials(TestBase):
         # The index contains all blobs in a flat list
         assert len(list(index.iter_blobs())) == len([o for o in repo.head.commit.tree.traverse() if o.type == 'blob'])
         # Access blob objects
-        for (path, stage), entry in index.entries.items():  # @UnusedVariable
+        for (path, _stage), entry in index.entries.items():
             pass
         new_file_path = os.path.join(repo.working_tree_dir, 'new-file-name')
         open(new_file_path, 'w').close()
diff --git a/git/test/test_index.py b/git/test/test_index.py
index 11009239..9b8c957e 100644
--- a/git/test/test_index.py
+++ b/git/test/test_index.py
@@ -75,7 +75,7 @@ class TestIndex(TestBase):
 
     def _assert_fprogress(self, entries):
         self.assertEqual(len(entries), len(self._fprogress_map))
-        for path, call_count in self._fprogress_map.items():  # @UnusedVariable
+        for _path, call_count in self._fprogress_map.items():
             self.assertEqual(call_count, 2)
         # END for each item in progress map
         self._reset_progress()
@@ -201,7 +201,7 @@ class TestIndex(TestBase):
 
         # test BlobFilter
         prefix = 'lib/git'
-        for stage, blob in base_index.iter_blobs(BlobFilter([prefix])):  # @UnusedVariable
+        for _stage, blob in base_index.iter_blobs(BlobFilter([prefix])):
             assert blob.path.startswith(prefix)
 
         # writing a tree should fail with an unmerged index
@@ -772,7 +772,16 @@ class TestIndex(TestBase):
             orig_tree = commit.tree
             self.assertEqual(index.write_tree(), orig_tree)
         # END for each commit
-
+    
+    @with_rw_repo('HEAD', bare=False)
+    def test_index_single_addremove(self, rw_repo):
+        fp = osp.join(rw_repo.working_dir, 'testfile.txt')
+        with open(fp, 'w') as fs:
+            fs.write(u'content of testfile')
+        self._assert_entries(rw_repo.index.add(fp))
+        deleted_files = rw_repo.index.remove(fp)
+        assert deleted_files
+        
     def test_index_new(self):
         B = self.rorepo.tree("6d9b1f4f9fa8c9f030e3207e7deacc5d5f8bba4e")
         H = self.rorepo.tree("25dca42bac17d511b7e2ebdd9d1d679e7626db5f")
diff --git a/git/test/test_remote.py b/git/test/test_remote.py
index 77e3ffbf..95898f12 100644
--- a/git/test/test_remote.py
+++ b/git/test/test_remote.py
@@ -93,7 +93,7 @@ class TestRemoteProgress(RemoteProgress):
         assert self._stages_per_op
 
         # must have seen all stages
-        for op, stages in self._stages_per_op.items():  # @UnusedVariable
+        for _op, stages in self._stages_per_op.items():
             assert stages & self.STAGE_MASK == self.STAGE_MASK
         # END for each op/stage
 
diff --git a/git/test/test_repo.py b/git/test/test_repo.py
index 94f9fdf8..de1e951a 100644
--- a/git/test/test_repo.py
+++ b/git/test/test_repo.py
@@ -737,7 +737,6 @@ class TestRepo(TestBase):
                 except (BadName, BadObject):
                     print("failed on %s" % path_section)
                     # is fine, in case we have something like 112, which belongs to remotes/rname/merge-requests/112
-                    pass
                 # END exception handling
             # END for each token
             if ref_no == 3 - 1: