Skip to content

Commit

Permalink
Merge bbacd5c into b155476
Browse files Browse the repository at this point in the history
  • Loading branch information
ivanovmg committed Aug 21, 2020
2 parents b155476 + bbacd5c commit c6a4be1
Show file tree
Hide file tree
Showing 7 changed files with 280 additions and 180 deletions.
24 changes: 9 additions & 15 deletions cookiecutter/utils.py
Expand Up @@ -5,7 +5,6 @@
import os
import shutil
import stat
import sys

from cookiecutter.prompt import read_user_yes_no

Expand Down Expand Up @@ -69,16 +68,16 @@ def make_executable(script_path):
os.chmod(script_path, status.st_mode | stat.S_IEXEC)


def prompt_and_delete(path, no_input=False):
def prompt_ok_to_delete(path, no_input=False):
"""
Ask user if it's okay to delete the previously-downloaded file/directory.
If yes, delete it. If no, checks to see if the old version should be
reused. If yes, it's reused; otherwise, Cookiecutter exits.
:param path: Previously downloaded zipfile.
:param no_input: Suppress prompt to delete repo and just delete it.
:return: True if the content was deleted
:param no_input: Suppress prompt.
:return: True if the content will be deleted
"""
# Suppress prompt if called via API
if no_input:
Expand All @@ -89,19 +88,14 @@ def prompt_and_delete(path, no_input=False):
).format(path)

ok_to_delete = read_user_yes_no(question, 'yes')
return ok_to_delete

if ok_to_delete:
if os.path.isdir(path):
rmtree(path)
else:
os.remove(path)
return True

def prompt_ok_to_reuse(path, no_input=False):
if no_input:
ok_to_reuse = False
else:
ok_to_reuse = read_user_yes_no(
"Do you want to re-use the existing version?", 'yes'
)

if ok_to_reuse:
return False

sys.exit()
return ok_to_reuse
117 changes: 86 additions & 31 deletions cookiecutter/vcs.py
Expand Up @@ -2,15 +2,25 @@
import logging
import os
import subprocess
from shutil import which
from shutil import (
which,
move,
)
import sys
import tempfile

from cookiecutter.exceptions import (
RepositoryCloneFailed,
RepositoryNotFound,
UnknownRepoType,
VCSNotInstalled,
)
from cookiecutter.utils import make_sure_path_exists, prompt_and_delete
from cookiecutter.utils import (
make_sure_path_exists,
prompt_ok_to_delete,
prompt_ok_to_reuse,
rmtree,
)

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -85,36 +95,81 @@ def clone(repo_url, checkout=None, clone_to_dir='.', no_input=False):
repo_dir = os.path.normpath(os.path.join(clone_to_dir, repo_name))
logger.debug('repo_dir is {0}'.format(repo_dir))

if os.path.isdir(repo_dir):
clone = prompt_and_delete(repo_dir, no_input=no_input)
if _need_to_clone(repo_dir, no_input):
_delete_old_and_clone_new(
repo_dir, repo_url, repo_type, clone_to_dir, checkout,
)
return repo_dir


def _need_to_clone(repo_dir, no_input):
ok_to_delete = prompt_ok_to_delete(repo_dir, no_input=no_input)

if ok_to_delete:
ok_to_reuse = False
else:
clone = True
ok_to_reuse = prompt_ok_to_reuse(repo_dir, no_input=no_input)

if not ok_to_delete and not ok_to_reuse:
sys.exit()

need_to_clone = ok_to_delete and not ok_to_reuse
return need_to_clone


def _delete_old_and_clone_new(repo_dir, repo_url, repo_type, clone_to_dir, checkout):
with tempfile.TemporaryDirectory() as tmp_dir:
backup_performed = os.path.exists(repo_dir)
if backup_performed:
backup_dir = os.path.join(tmp_dir, os.path.basename(repo_dir))
_backup_and_delete_repo(repo_dir, backup_dir)

if clone:
try:
subprocess.check_output(
[repo_type, 'clone', repo_url],
cwd=clone_to_dir,
stderr=subprocess.STDOUT,
)
if checkout is not None:
subprocess.check_output(
[repo_type, 'checkout', checkout],
cwd=repo_dir,
stderr=subprocess.STDOUT,
)
_clone_repo(repo_dir, repo_url, repo_type, clone_to_dir, checkout)
except subprocess.CalledProcessError as clone_error:
output = clone_error.output.decode('utf-8')
if 'not found' in output.lower():
raise RepositoryNotFound(
'The repository {} could not be found, '
'have you made a typo?'.format(repo_url)
)
if any(error in output for error in BRANCH_ERRORS):
raise RepositoryCloneFailed(
'The {} branch of repository {} could not found, '
'have you made a typo?'.format(checkout, repo_url)
)
raise

return repo_dir
if backup_performed:
_restore_old_repo(repo_dir, backup_dir)
_handle_clone_error(clone_error, repo_url, checkout)


def _clone_repo(repo_dir, repo_url, repo_type, clone_to_dir, checkout):
subprocess.check_output(
[repo_type, 'clone', repo_url], cwd=clone_to_dir, stderr=subprocess.STDOUT,
)
if checkout is not None:
subprocess.check_output(
[repo_type, 'checkout', checkout], cwd=repo_dir, stderr=subprocess.STDOUT,
)


def _handle_clone_error(clone_error, repo_url, checkout):
output = clone_error.output.decode('utf-8')
if 'not found' in output.lower():
raise RepositoryNotFound(
'The repository {} could not be found, '
'have you made a typo?'.format(repo_url)
)
if any(error in output for error in BRANCH_ERRORS):
raise RepositoryCloneFailed(
'The {} branch of repository {} could not found, '
'have you made a typo?'.format(checkout, repo_url)
)
raise


def _backup_and_delete_repo(path, backup_path):
logger.info('Backing up repo {} to {}'.format(path, backup_path))
move(path, backup_path)
logger.info('Moving repo {} to {}'.format(path, backup_path))


def _restore_old_repo(path, backup_path):
try:
rmtree(path)
logger.info('Cleaning {}'.format(path))
except FileNotFoundError:
pass

logger.info('Restoring backup repo {}'.format(backup_path))
move(backup_path, path)
logger.info('Restored {} successfully'.format(path))
4 changes: 2 additions & 2 deletions cookiecutter/zipfile.py
Expand Up @@ -7,7 +7,7 @@

from cookiecutter.exceptions import InvalidZipRepository
from cookiecutter.prompt import read_repo_password
from cookiecutter.utils import make_sure_path_exists, prompt_and_delete
from cookiecutter.utils import make_sure_path_exists, prompt_ok_to_delete


def unzip(zip_uri, is_url, clone_to_dir='.', no_input=False, password=None):
Expand All @@ -34,7 +34,7 @@ def unzip(zip_uri, is_url, clone_to_dir='.', no_input=False, password=None):
zip_path = os.path.join(clone_to_dir, identifier)

if os.path.exists(zip_path):
download = prompt_and_delete(zip_path, no_input=no_input)
download = prompt_ok_to_delete(zip_path, no_input=no_input)
else:
download = True

Expand Down
2 changes: 1 addition & 1 deletion tests/test_cookiecutter_local_no_input.py
Expand Up @@ -128,7 +128,7 @@ def test_cookiecutter_template_cleanup(mocker):
mocker.patch('tempfile.mkdtemp', return_value='fake-tmp', autospec=True)

mocker.patch(
'cookiecutter.utils.prompt_and_delete', return_value=True, autospec=True
'cookiecutter.utils.prompt_ok_to_delete', return_value=True, autospec=True
)

main.cookiecutter('tests/files/fake-repo-tmpl.zip', no_input=True)
Expand Down
128 changes: 21 additions & 107 deletions tests/test_utils.py
Expand Up @@ -87,130 +87,44 @@ def test_work_in(tmp_path):
assert cwd == Path.cwd()


def test_prompt_should_ask_and_rm_repo_dir(mocker, tmp_path):
"""In `prompt_and_delete()`, if the user agrees to delete/reclone the \
repo, the repo should be deleted."""
def test_prompt_ok_to_delete_reads_user_yes(mocker):
"""In `prompt_ok_to_delete()` the user answers yes."""
mock_read_user = mocker.patch(
'cookiecutter.utils.read_user_yes_no', return_value=True
)
repo_dir = Path(tmp_path, 'repo')
repo_dir.mkdir()
ok_to_delete = utils.prompt_ok_to_delete('path/to/repo')

deleted = utils.prompt_and_delete(str(repo_dir))
assert mock_read_user.called_once
assert ok_to_delete

assert mock_read_user.called
assert not repo_dir.exists()
assert deleted


def test_prompt_should_ask_and_exit_on_user_no_answer(mocker, tmp_path):
"""In `prompt_and_delete()`, if the user decline to delete/reclone the \
repo, cookiecutter should exit."""
mock_read_user = mocker.patch(
'cookiecutter.utils.read_user_yes_no', return_value=False,
)
mock_sys_exit = mocker.patch('sys.exit', return_value=True)
repo_dir = Path(tmp_path, 'repo')
repo_dir.mkdir()

deleted = utils.prompt_and_delete(str(repo_dir))

assert mock_read_user.called
assert repo_dir.exists()
assert not deleted
assert mock_sys_exit.called


def test_prompt_should_ask_and_rm_repo_file(mocker, tmp_path):
"""In `prompt_and_delete()`, if the user agrees to delete/reclone a \
repo file, the repo should be deleted."""
def test_prompt_ok_to_delete_reads_user_no(mocker):
"""In `prompt_ok_to_delete()` the user answers no."""
mock_read_user = mocker.patch(
'cookiecutter.utils.read_user_yes_no', return_value=True, autospec=True
'cookiecutter.utils.read_user_yes_no', return_value=False
)
ok_to_delete = utils.prompt_ok_to_delete('path/to/repo')

repo_file = tmp_path.joinpath('repo.zip')
repo_file.write_text('this is zipfile content')

deleted = utils.prompt_and_delete(str(repo_file))
assert mock_read_user.called_once
assert not ok_to_delete

assert mock_read_user.called
assert not repo_file.exists()
assert deleted


def test_prompt_should_ask_and_keep_repo_on_no_reuse(mocker, tmp_path):
"""In `prompt_and_delete()`, if the user wants to keep their old \
cloned template repo, it should not be deleted."""
def test_prompt_ok_to_delete_no_user_input(mocker):
"""In `prompt_ok_to_delete()` no user input means yes."""
mock_read_user = mocker.patch(
'cookiecutter.utils.read_user_yes_no', return_value=False, autospec=True
'cookiecutter.utils.read_user_yes_no', return_value=False
)
repo_dir = Path(tmp_path, 'repo')
repo_dir.mkdir()

with pytest.raises(SystemExit):
utils.prompt_and_delete(str(repo_dir))

assert mock_read_user.called
assert repo_dir.exists()


def test_prompt_should_ask_and_keep_repo_on_reuse(mocker, tmp_path):
"""In `prompt_and_delete()`, if the user wants to keep their old \
cloned template repo, it should not be deleted."""

def answer(question, default):
if 'okay to delete' in question:
return False
else:
return True

mock_read_user = mocker.patch(
'cookiecutter.utils.read_user_yes_no', side_effect=answer, autospec=True
)
repo_dir = Path(tmp_path, 'repo')
repo_dir.mkdir()

deleted = utils.prompt_and_delete(str(repo_dir))

assert mock_read_user.called
assert repo_dir.exists()
assert not deleted


def test_prompt_should_not_ask_if_no_input_and_rm_repo_dir(mocker, tmp_path):
"""Prompt should not ask if no input and rm dir.
In `prompt_and_delete()`, if `no_input` is True, the call to
`prompt.read_user_yes_no()` should be suppressed.
"""
mock_read_user = mocker.patch(
'cookiecutter.prompt.read_user_yes_no', return_value=True, autospec=True
)
repo_dir = Path(tmp_path, 'repo')
repo_dir.mkdir()

deleted = utils.prompt_and_delete(str(repo_dir), no_input=True)
ok_to_delete = utils.prompt_ok_to_delete('path/to/repo', no_input=True)

assert not mock_read_user.called
assert not repo_dir.exists()
assert deleted
assert ok_to_delete


def test_prompt_should_not_ask_if_no_input_and_rm_repo_file(mocker, tmp_path):
"""Prompt should not ask if no input and rm file.
In `prompt_and_delete()`, if `no_input` is True, the call to
`prompt.read_user_yes_no()` should be suppressed.
"""
def test_prompt_ok_to_reuse(mocker):
mock_read_user = mocker.patch(
'cookiecutter.prompt.read_user_yes_no', return_value=True, autospec=True
'cookiecutter.utils.read_user_yes_no', return_value=True,
)
ok_to_reuse = utils.prompt_ok_to_reuse('any_path')

repo_file = tmp_path.joinpath('repo.zip')
repo_file.write_text('this is zipfile content')

deleted = utils.prompt_and_delete(str(repo_file), no_input=True)

assert not mock_read_user.called
assert not repo_file.exists()
assert deleted
assert mock_read_user.called_once
assert ok_to_reuse

0 comments on commit c6a4be1

Please sign in to comment.