Repository: cookiecutter/cookiecutter

Issue:
.cookiecutterrc and cookiecutters_dir not working as I expected
Hi,
Here's the setup:
- I have a ~/.cookiecutterrc as talked about here: http://cookiecutter.readthedocs.org/en/latest/advanced_usage.html
- I also have a cookiecutters_dir at ~/.cookiecutters/ with the subdirectory cookiecutter-pypackage/.

When I try to run "cookiecutter cookiecutter-pypackage/" in ~/Projects/, I get the following error

```
Traceback (most recent call last):
  File "/opt/anaconda/bin/cookiecutter", line 9, in <module>
    load_entry_point('cookiecutter==0.9.0', 'console_scripts', 'cookiecutter')()
  File "/opt/anaconda/lib/python2.7/site-packages/click-3.3-py2.7.egg/click/core.py", line 610, in __call__
    return self.main(*args, **kwargs)
  File "/opt/anaconda/lib/python2.7/site-packages/click-3.3-py2.7.egg/click/core.py", line 590, in main
    rv = self.invoke(ctx)
  File "/opt/anaconda/lib/python2.7/site-packages/click-3.3-py2.7.egg/click/core.py", line 782, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/opt/anaconda/lib/python2.7/site-packages/click-3.3-py2.7.egg/click/core.py", line 416, in invoke
    return callback(*args, **kwargs)
  File "/Users/alexp/Projects/cookiecutter/cookiecutter/cli.py", line 70, in main
    cookiecutter(template, checkout, no_input)
  File "/Users/alexp/Projects/cookiecutter/cookiecutter/main.py", line 95, in cookiecutter
    extra_context=extra_context,
  File "/Users/alexp/Projects/cookiecutter/cookiecutter/generate.py", line 43, in generate_context
    file_handle = open(context_file)
IOError: [Errno 2] No such file or directory: u'cookiecutter-pypackage/cookiecutter.json'
```

This error shows up if I either do pip install or with the git repo locally.  Naturally, it makes a bit of sense.  There is no directory ~/Projects/cookiecutter-pypackage/.  

However, and perhaps I'm making a poor assumption about usage, it seems to me if I clone or otherwise create a cookiecutter and it's sitting in cookiecutters_dir, it'd be nice to just refer to it as I did above.  For my usage, if I create a cookiecutter, I don't particularly want it sitting around a Projects directory, especially if I have multiple project directories for personal and organizational purposes.

In order to do this, I added three lines to main.py in my fork (see below) and it seems to work.  I did it as an `elif` to try to preserve the possibility of a lack of a cookiecutters_dir.  I have not written a test for this and admittedly I don't really know how.  I will likely just use my fork with this modificationgoing forward but I wanted to let the developer crew know about this.  

Cheers.

```
# cookiecutter.main
...
# TODO: find a better way to tell if it's a repo URL
if 'git@' in template or 'https://' in template:
    repo_dir = clone(
        repo_url=template,
        checkout=checkout,
        clone_to_dir=config_dict['cookiecutters_dir'],
        no_input=no_input
    )
#### Added these three lines
elif 'cookiecutters_dir' in config_dict:
    cookiecutters_dir = os.path.expanduser(config_dict['cookiecutters_dir'])
    repo_dir = os.path.join(cookiecutters_dir,template)
else:
    # If it's a local repo, no need to clone or copy to your
    # cookiecutters_dir
    repo_dir = template
```



Discussion:
Hi @apapanico, thanks for reporting this issue.
I think we can definitely improve the user experience here.

We do check if there is an existing checkout inside the call to [`clone`](https://github.com/audreyr/cookiecutter/blob/master/cookiecutter/vcs.py#L104-L105), but we only support deleting and re-cloning out the repo.

I think we should be detecting existing templates (for both repo URL and repo name variants) and then presenting a choice to either:
- delete and re-clone the repo
- pull the repo
- use the existing checkout

Thoughts?

Interesting.  I guess I was more thinking of having a cookiecutter that wasn't a repo somewhere so I could just build one for my needs, stash it in ~/.cookiecutters and call it by name.

And in line with how the repos are stored in ~/.cookiecutters, after you get a cutter from a repo on github you could just call it again by name.

As a recent user of cookiecutter, I approve with the suggested change. I too, thought that one could stash templates in `~/.cookiecutters` and then call them by name, without specifying the directory.  

In any case, it might still be useful to explicitly document it, IMO.

Just ran into this myself and left me scratching my head a bit.

Re-visiting this one, and my current plan is:
- [x] Gracefully handle the case where the code assumes we're referencing a local filesystem repo, but it doesn't exist (#652)
- [ ] After trying repository URLs and local repos, check if we have a match for an existing directory in `~/.cookiecutters` and use that
- [ ] When we're trying to clone a remote repo to `~/.cookiecutters`, but there's already something there, allow users to use the existing repo directory if they decide not to delete and re-clone.


Resolving patch:
diff --git a/cookiecutter/repository.py b/cookiecutter/repository.py
--- a/cookiecutter/repository.py
+++ b/cookiecutter/repository.py
@@ -77,21 +77,27 @@ def determine_repo_dir(template, abbreviations, clone_to_dir, checkout,
     template = expand_abbreviations(template, abbreviations)
 
     if is_repo_url(template):
-        repo_dir = clone(
+        cloned_repo = clone(
             repo_url=template,
             checkout=checkout,
             clone_to_dir=clone_to_dir,
             no_input=no_input,
         )
+        repository_candidates = [cloned_repo]
     else:
-        # If it's a local repo, no need to clone or copy to your
-        # cookiecutters_dir
-        repo_dir = template
+        repository_candidates = [
+            template,
+            os.path.join(clone_to_dir, template)
+        ]
 
-    if repository_has_cookiecutter_json(repo_dir):
-        return repo_dir
+    for repo_candidate in repository_candidates:
+        if repository_has_cookiecutter_json(repo_candidate):
+            return repo_candidate
 
     raise RepositoryNotFound(
-        'The repository {} could not be located or does not contain '
-        'a "cookiecutter.json" file.'.format(repo_dir)
+        'A valid repository for "{}" could not be found in the following '
+        'locations:\n{}'.format(
+            template,
+            '\n'.join(repository_candidates)
+        )
     )


Test patch:
diff --git a/tests/repository/test_determine_repo_dir_clones_repo.py b/tests/repository/test_determine_repo_dir_clones_repo.py
--- a/tests/repository/test_determine_repo_dir_clones_repo.py
+++ b/tests/repository/test_determine_repo_dir_clones_repo.py
@@ -46,7 +46,8 @@ def test_repository_url_should_clone(
     assert 'tests/fake-repo-tmpl' == project_dir
 
 
-def test_repository_url_with_no_context_file(mocker, template_url):
+def test_repository_url_with_no_context_file(
+        mocker, template_url, user_config_data):
     mocker.patch(
         'cookiecutter.repository.clone',
         return_value='tests/fake-repo-bad',
@@ -63,6 +64,9 @@ def test_repository_url_with_no_context_file(mocker, template_url):
         )
 
     assert str(err.value) == (
-        'The repository tests/fake-repo-bad could not be located or does not '
-        'contain a "cookiecutter.json" file.'
+        'A valid repository for "{}" could not be found in the following '
+        'locations:\n{}'.format(
+            template_url,
+            'tests/fake-repo-bad',
+        )
     )
diff --git a/tests/repository/test_determine_repo_dir_finds_existing_cookiecutter.py b/tests/repository/test_determine_repo_dir_finds_existing_cookiecutter.py
new file mode 100644
--- /dev/null
+++ b/tests/repository/test_determine_repo_dir_finds_existing_cookiecutter.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+import io
+import os
+import pytest
+
+from cookiecutter import repository
+
+
+@pytest.fixture
+def template():
+    return 'cookiecutter-pytest-plugin'
+
+
+@pytest.fixture
+def cloned_cookiecutter_path(user_config_data, template):
+    cookiecutters_dir = user_config_data['cookiecutters_dir']
+
+    cloned_template_path = os.path.join(cookiecutters_dir, template)
+    os.mkdir(cloned_template_path)
+
+    io.open(os.path.join(cloned_template_path, 'cookiecutter.json'), 'w')
+
+    return cloned_template_path
+
+
+def test_should_find_existing_cookiecutter(
+        template, user_config_data, cloned_cookiecutter_path):
+    project_dir = repository.determine_repo_dir(
+        template,
+        abbreviations={},
+        clone_to_dir=user_config_data['cookiecutters_dir'],
+        checkout=None,
+        no_input=True,
+    )
+
+    assert cloned_cookiecutter_path == project_dir
diff --git a/tests/repository/test_determine_repository_should_use_local_repo.py b/tests/repository/test_determine_repository_should_use_local_repo.py
--- a/tests/repository/test_determine_repository_should_use_local_repo.py
+++ b/tests/repository/test_determine_repository_should_use_local_repo.py
@@ -1,15 +1,16 @@
 # -*- coding: utf-8 -*-
+import os
 from cookiecutter import repository, exceptions
 
 import pytest
 
 
-def test_finds_local_repo():
+def test_finds_local_repo(tmpdir):
     """A valid local repository should be returned."""
     project_dir = repository.determine_repo_dir(
         'tests/fake-repo',
         abbreviations={},
-        clone_to_dir=None,
+        clone_to_dir=str(tmpdir),
         checkout=None,
         no_input=True
     )
@@ -17,39 +18,53 @@ def test_finds_local_repo():
     assert 'tests/fake-repo' == project_dir
 
 
-def test_local_repo_with_no_context_raises():
+def test_local_repo_with_no_context_raises(tmpdir):
     """A local repository without a cookiecutter.json should raise a
     `RepositoryNotFound` exception.
     """
+    template_path = os.path.join('tests', 'fake-repo-bad')
     with pytest.raises(exceptions.RepositoryNotFound) as err:
         repository.determine_repo_dir(
-            'tests/fake-repo-bad',
+            template_path,
             abbreviations={},
-            clone_to_dir=None,
+            clone_to_dir=str(tmpdir),
             checkout=None,
             no_input=True
         )
 
     assert str(err.value) == (
-        'The repository tests/fake-repo-bad could not be located or does not '
-        'contain a "cookiecutter.json" file.'
+        'A valid repository for "{}" could not be found in the following '
+        'locations:\n{}'.format(
+            template_path,
+            '\n'.join([
+                template_path,
+                str(tmpdir / 'tests/fake-repo-bad')
+            ]),
+        )
     )
 
 
-def test_local_repo_typo():
+def test_local_repo_typo(tmpdir):
     """An unknown local repository should raise a `RepositoryNotFound`
     exception.
     """
+    template_path = os.path.join('tests', 'unknown-repo')
     with pytest.raises(exceptions.RepositoryNotFound) as err:
         repository.determine_repo_dir(
-            'tests/unknown-repo',
+            template_path,
             abbreviations={},
-            clone_to_dir=None,
+            clone_to_dir=str(tmpdir),
             checkout=None,
             no_input=True
         )
 
     assert str(err.value) == (
-        'The repository tests/unknown-repo could not be located or does not '
-        'contain a "cookiecutter.json" file.'
+        'A valid repository for "{}" could not be found in the following '
+        'locations:\n{}'.format(
+            template_path,
+            '\n'.join([
+                template_path,
+                str(tmpdir / 'tests/unknown-repo')
+            ]),
+        )
     )