Repository: pypa/pip

Issue:
pip freeze errors when packages are pip install -e'd against git repos with no remote
* Pip version: 9.0.1
* Python version: 2.7
* Operating system: Linux (SUSE Tumbleweed)

### Description:

If I pip install -e against a git repo with no remotes then pip freeze will error when reporting the version for that package because it claims to not know the repo location.

### What I've run:

```
clark@morbo:~/tmp$ virtualenv testenv
New python executable in /home/clark/tmp/testenv/bin/python2
Also creating executable in /home/clark/tmp/testenv/bin/python
Installing setuptools, pip, wheel...done.
clark@morbo:~/tmp$ testenv/bin/pip --version
pip 9.0.1 from /home/clark/tmp/testenv/lib/python2.7/site-packages (python 2.7)
clark@morbo:~/tmp$ testenv/bin/pip install -e git/git-review
Obtaining file:///home/clark/tmp/git/git-review
Collecting requests>=1.1 (from git-review==1.25.1.dev52)
  Using cached requests-2.18.4-py2.py3-none-any.whl
Collecting idna<2.7,>=2.5 (from requests>=1.1->git-review==1.25.1.dev52)
  Using cached idna-2.6-py2.py3-none-any.whl
Collecting urllib3<1.23,>=1.21.1 (from requests>=1.1->git-review==1.25.1.dev52)
  Using cached urllib3-1.22-py2.py3-none-any.whl
Collecting certifi>=2017.4.17 (from requests>=1.1->git-review==1.25.1.dev52)
  Using cached certifi-2017.7.27.1-py2.py3-none-any.whl
Collecting chardet<3.1.0,>=3.0.2 (from requests>=1.1->git-review==1.25.1.dev52)
  Using cached chardet-3.0.4-py2.py3-none-any.whl
Installing collected packages: idna, urllib3, certifi, chardet, requests, git-review
  Running setup.py develop for git-review
Successfully installed certifi-2017.7.27.1 chardet-3.0.4 git-review idna-2.6 requests-2.18.4 urllib3-1.22
clark@morbo:~/tmp$ testenv/bin/pip freeze
Complete output from command git config --get-regexp remote\..*\.url:

----------------------------------------
Error when trying to get requirement for VCS system Command "git config --get-regexp remote\..*\.url" failed with error code 1 in /home/clark/tmp/git/git-review, falling back to uneditable format
Could not determine repository location of /home/clark/tmp/git/git-review
certifi==2017.7.27.1
chardet==3.0.4
## !! Could not determine repository location
git-review==1.25.1.dev52
idna==2.6
requests==2.18.4
urllib3==1.22
```

### What I expect

It is perfectly valid to have a git repo with no remotes. That just means the current repo is canonical. Instead of erroring pip should use the file location as the location of the repo. Something like:

```
git+file:///path/to/package/repository
```


Discussion:
Thanks for filing this @cboylan!

@cjerdonek I think you'd know a bit more about this than I do -- did this get fixed at some point?

Nvm. Ignore that comment. 🤷‍♂️ 
I just stumbled on this. I concur that a Git repository without a remote is a viable installation candidate.

Resolving patch:
diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py
--- a/src/pip/_internal/operations/freeze.py
+++ b/src/pip/_internal/operations/freeze.py
@@ -172,7 +172,7 @@ def get_requirement_info(dist):
 
     location = os.path.normcase(os.path.abspath(dist.location))
 
-    from pip._internal.vcs import vcs
+    from pip._internal.vcs import vcs, RemoteNotFoundError
     vc_type = vcs.get_backend_type(location)
 
     if not vc_type:
@@ -188,6 +188,15 @@ def get_requirement_info(dist):
 
     try:
         req = vc_type().get_src_requirement(location, dist.project_name)
+    except RemoteNotFoundError:
+        req = dist.as_requirement()
+        comments = [
+            '# Editable {} install with no remote ({})'.format(
+                vc_type.__name__, req,
+            )
+        ]
+        return (location, True, comments)
+
     except BadCommand:
         logger.warning(
             'cannot determine version of editable source in %s '
diff --git a/src/pip/_internal/vcs/__init__.py b/src/pip/_internal/vcs/__init__.py
--- a/src/pip/_internal/vcs/__init__.py
+++ b/src/pip/_internal/vcs/__init__.py
@@ -29,6 +29,10 @@
 logger = logging.getLogger(__name__)
 
 
+class RemoteNotFoundError(Exception):
+    pass
+
+
 class RevOptions(object):
 
     """
@@ -452,6 +456,9 @@ def get_src_requirement(self, location, project_name):
     def get_remote_url(self, location):
         """
         Return the url used at location
+
+        Raises RemoteNotFoundError if the repository does not have a remote
+        url configured.
         """
         raise NotImplementedError
 
diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py
--- a/src/pip/_internal/vcs/git.py
+++ b/src/pip/_internal/vcs/git.py
@@ -14,7 +14,7 @@
     display_path, make_vcs_requirement_url, redact_password_from_url,
 )
 from pip._internal.utils.temp_dir import TempDirectory
-from pip._internal.vcs import VersionControl, vcs
+from pip._internal.vcs import RemoteNotFoundError, VersionControl, vcs
 
 urlsplit = urllib_parse.urlsplit
 urlunsplit = urllib_parse.urlunsplit
@@ -250,13 +250,24 @@ def update(self, dest, url, rev_options):
         self.update_submodules(dest)
 
     def get_remote_url(self, location):
-        """Return URL of the first remote encountered."""
-        remotes = self.run_command(
+        """
+        Return URL of the first remote encountered.
+
+        Raises RemoteNotFoundError if the repository does not have a remote
+        url configured.
+        """
+        # We need to pass 1 for extra_ok_returncodes since the command
+        # exits with return code 1 if there are no matching lines.
+        stdout = self.run_command(
             ['config', '--get-regexp', r'remote\..*\.url'],
-            show_stdout=False, cwd=location,
+            extra_ok_returncodes=(1, ), show_stdout=False, cwd=location,
         )
-        remotes = remotes.splitlines()
-        found_remote = remotes[0]
+        remotes = stdout.splitlines()
+        try:
+            found_remote = remotes[0]
+        except IndexError:
+            raise RemoteNotFoundError
+
         for remote in remotes:
             if remote.startswith('remote.origin.url '):
                 found_remote = remote


Test patch:
diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py
--- a/tests/functional/test_freeze.py
+++ b/tests/functional/test_freeze.py
@@ -137,6 +137,26 @@ def test_freeze_editable_not_vcs(script, tmpdir):
     _check_output(result.stdout, expected)
 
 
+@pytest.mark.git
+def test_freeze_editable_git_with_no_remote(script, tmpdir):
+    """
+    Test an editable Git install with no remote url.
+    """
+    pkg_path = _create_test_package(script)
+    script.pip('install', '-e', pkg_path)
+    result = script.pip('freeze')
+
+    assert result.stderr == ''
+
+    # We need to apply os.path.normcase() to the path since that is what
+    # the freeze code does.
+    expected = textwrap.dedent("""\
+    ...# Editable Git install with no remote (version-pkg==0.1)
+    -e {}
+    ...""".format(os.path.normcase(pkg_path)))
+    _check_output(result.stdout, expected)
+
+
 @pytest.mark.svn
 def test_freeze_svn(script, tmpdir):
     """Test freezing a svn checkout"""
diff --git a/tests/functional/test_install_vcs_git.py b/tests/functional/test_install_vcs_git.py
--- a/tests/functional/test_install_vcs_git.py
+++ b/tests/functional/test_install_vcs_git.py
@@ -367,20 +367,6 @@ def test_git_with_ambiguous_revs(script):
     result.assert_installed('version-pkg', with_files=['.git'])
 
 
-def test_git_works_with_editable_non_origin_repo(script):
-    # set up, create a git repo and install it as editable from a local
-    # directory path
-    version_pkg_path = _create_test_package(script)
-    script.pip('install', '-e', version_pkg_path.abspath)
-
-    # 'freeze'ing this should not fall over, but should result in stderr output
-    # warning
-    result = script.pip('freeze', expect_stderr=True)
-    assert "Error when trying to get requirement" in result.stderr
-    assert "Could not determine repository location" in result.stdout
-    assert "version-pkg==0.1" in result.stdout
-
-
 def test_editable__no_revision(script):
     """
     Test a basic install in editable mode specifying no revision.
diff --git a/tests/functional/test_vcs_git.py b/tests/functional/test_vcs_git.py
--- a/tests/functional/test_vcs_git.py
+++ b/tests/functional/test_vcs_git.py
@@ -4,7 +4,9 @@
 
 import os
 
-from pip._internal.vcs.git import Git
+import pytest
+
+from pip._internal.vcs.git import Git, RemoteNotFoundError
 from tests.lib import _create_test_package, _git_commit, _test_path_to_file_url
 
 
@@ -93,6 +95,20 @@ def test_get_remote_url(script, tmpdir):
     assert remote_url == source_url
 
 
+def test_get_remote_url__no_remote(script, tmpdir):
+    """
+    Test a repo with no remote.
+    """
+    repo_dir = tmpdir / 'temp-repo'
+    repo_dir.mkdir()
+    repo_dir = str(repo_dir)
+
+    script.run('git', 'init', cwd=repo_dir)
+
+    with pytest.raises(RemoteNotFoundError):
+        Git().get_remote_url(repo_dir)
+
+
 def test_get_current_branch(script):
     repo_dir = str(script.scratch_path)