@@ -2,7 +2,12 @@
Loading
2 2
import logging
3 3
import os
4 4
import subprocess
5 -
from shutil import which
5 +
from shutil import (
6 +
    which,
7 +
    move,
8 +
)
9 +
import sys
10 +
import tempfile
6 11
7 12
from cookiecutter.exceptions import (
8 13
    RepositoryCloneFailed,
@@ -10,7 +15,12 @@
Loading
10 15
    UnknownRepoType,
11 16
    VCSNotInstalled,
12 17
)
13 -
from cookiecutter.utils import make_sure_path_exists, prompt_and_delete
18 +
from cookiecutter.utils import (
19 +
    make_sure_path_exists,
20 +
    prompt_ok_to_delete,
21 +
    prompt_ok_to_reuse,
22 +
    rmtree,
23 +
)
14 24
15 25
logger = logging.getLogger(__name__)
16 26
@@ -85,36 +95,81 @@
Loading
85 95
        repo_dir = os.path.normpath(os.path.join(clone_to_dir, repo_name))
86 96
    logger.debug('repo_dir is {0}'.format(repo_dir))
87 97
88 -
    if os.path.isdir(repo_dir):
89 -
        clone = prompt_and_delete(repo_dir, no_input=no_input)
98 +
    if _need_to_clone(repo_dir, no_input):
99 +
        _delete_old_and_clone_new(
100 +
            repo_dir, repo_url, repo_type, clone_to_dir, checkout,
101 +
        )
102 +
    return repo_dir
103 +
104 +
105 +
def _need_to_clone(repo_dir, no_input):
106 +
    ok_to_delete = prompt_ok_to_delete(repo_dir, no_input=no_input)
107 +
108 +
    if ok_to_delete:
109 +
        ok_to_reuse = False
90 110
    else:
91 -
        clone = True
111 +
        ok_to_reuse = prompt_ok_to_reuse(repo_dir, no_input=no_input)
112 +
113 +
    if not ok_to_delete and not ok_to_reuse:
114 +
        sys.exit()
115 +
116 +
    need_to_clone = ok_to_delete and not ok_to_reuse
117 +
    return need_to_clone
118 +
119 +
120 +
def _delete_old_and_clone_new(repo_dir, repo_url, repo_type, clone_to_dir, checkout):
121 +
    with tempfile.TemporaryDirectory() as tmp_dir:
122 +
        backup_performed = os.path.exists(repo_dir)
123 +
        if backup_performed:
124 +
            backup_dir = os.path.join(tmp_dir, os.path.basename(repo_dir))
125 +
            _backup_and_delete_repo(repo_dir, backup_dir)
92 126
93 -
    if clone:
94 127
        try:
95 -
            subprocess.check_output(
96 -
                [repo_type, 'clone', repo_url],
97 -
                cwd=clone_to_dir,
98 -
                stderr=subprocess.STDOUT,
99 -
            )
100 -
            if checkout is not None:
101 -
                subprocess.check_output(
102 -
                    [repo_type, 'checkout', checkout],
103 -
                    cwd=repo_dir,
104 -
                    stderr=subprocess.STDOUT,
105 -
                )
128 +
            _clone_repo(repo_dir, repo_url, repo_type, clone_to_dir, checkout)
106 129
        except subprocess.CalledProcessError as clone_error:
107 -
            output = clone_error.output.decode('utf-8')
108 -
            if 'not found' in output.lower():
109 -
                raise RepositoryNotFound(
110 -
                    'The repository {} could not be found, '
111 -
                    'have you made a typo?'.format(repo_url)
112 -
                )
113 -
            if any(error in output for error in BRANCH_ERRORS):
114 -
                raise RepositoryCloneFailed(
115 -
                    'The {} branch of repository {} could not found, '
116 -
                    'have you made a typo?'.format(checkout, repo_url)
117 -
                )
118 -
            raise
119 -
120 -
    return repo_dir
130 +
            if backup_performed:
131 +
                _restore_old_repo(repo_dir, backup_dir)
132 +
            _handle_clone_error(clone_error, repo_url, checkout)
133 +
134 +
135 +
def _clone_repo(repo_dir, repo_url, repo_type, clone_to_dir, checkout):
136 +
    subprocess.check_output(
137 +
        [repo_type, 'clone', repo_url], cwd=clone_to_dir, stderr=subprocess.STDOUT,
138 +
    )
139 +
    if checkout is not None:
140 +
        subprocess.check_output(
141 +
            [repo_type, 'checkout', checkout], cwd=repo_dir, stderr=subprocess.STDOUT,
142 +
        )
143 +
144 +
145 +
def _handle_clone_error(clone_error, repo_url, checkout):
146 +
    output = clone_error.output.decode('utf-8')
147 +
    if 'not found' in output.lower():
148 +
        raise RepositoryNotFound(
149 +
            'The repository {} could not be found, '
150 +
            'have you made a typo?'.format(repo_url)
151 +
        )
152 +
    if any(error in output for error in BRANCH_ERRORS):
153 +
        raise RepositoryCloneFailed(
154 +
            'The {} branch of repository {} could not found, '
155 +
            'have you made a typo?'.format(checkout, repo_url)
156 +
        )
157 +
    raise
158 +
159 +
160 +
def _backup_and_delete_repo(path, backup_path):
161 +
    logger.info('Backing up repo {} to {}'.format(path, backup_path))
162 +
    move(path, backup_path)
163 +
    logger.info('Moving repo {} to {}'.format(path, backup_path))
164 +
165 +
166 +
def _restore_old_repo(path, backup_path):
167 +
    try:
168 +
        rmtree(path)
169 +
        logger.info('Cleaning {}'.format(path))
170 +
    except FileNotFoundError:
171 +
        pass
172 +
173 +
    logger.info('Restoring backup repo {}'.format(backup_path))
174 +
    move(backup_path, path)
175 +
    logger.info('Restored {} successfully'.format(path))

@@ -7,7 +7,7 @@
Loading
7 7
8 8
from cookiecutter.exceptions import InvalidZipRepository
9 9
from cookiecutter.prompt import read_repo_password
10 -
from cookiecutter.utils import make_sure_path_exists, prompt_and_delete
10 +
from cookiecutter.utils import make_sure_path_exists, prompt_ok_to_delete
11 11
12 12
13 13
def unzip(zip_uri, is_url, clone_to_dir='.', no_input=False, password=None):
@@ -34,7 +34,7 @@
Loading
34 34
        zip_path = os.path.join(clone_to_dir, identifier)
35 35
36 36
        if os.path.exists(zip_path):
37 -
            download = prompt_and_delete(zip_path, no_input=no_input)
37 +
            download = prompt_ok_to_delete(zip_path, no_input=no_input)
38 38
        else:
39 39
            download = True
40 40

@@ -5,7 +5,6 @@
Loading
5 5
import os
6 6
import shutil
7 7
import stat
8 -
import sys
9 8
10 9
from cookiecutter.prompt import read_user_yes_no
11 10
@@ -69,16 +68,13 @@
Loading
69 68
    os.chmod(script_path, status.st_mode | stat.S_IEXEC)
70 69
71 70
72 -
def prompt_and_delete(path, no_input=False):
71 +
def prompt_ok_to_delete(path, no_input=False):
73 72
    """
74 73
    Ask user if it's okay to delete the previously-downloaded file/directory.
75 74
76 -
    If yes, delete it. If no, checks to see if the old version should be
77 -
    reused. If yes, it's reused; otherwise, Cookiecutter exits.
78 -
79 75
    :param path: Previously downloaded zipfile.
80 -
    :param no_input: Suppress prompt to delete repo and just delete it.
81 -
    :return: True if the content was deleted
76 +
    :param no_input: Suppress prompt.
77 +
    :return: True if the content will be deleted.
82 78
    """
83 79
    # Suppress prompt if called via API
84 80
    if no_input:
@@ -89,19 +85,21 @@
Loading
89 85
        ).format(path)
90 86
91 87
        ok_to_delete = read_user_yes_no(question, 'yes')
88 +
    return ok_to_delete
89 +
92 90
93 -
    if ok_to_delete:
94 -
        if os.path.isdir(path):
95 -
            rmtree(path)
96 -
        else:
97 -
            os.remove(path)
98 -
        return True
91 +
def prompt_ok_to_reuse(path, no_input=False):
92 +
    """
93 +
    Ask user if it's okay to reuse the previously-downloaded file/directory.
94 +
95 +
    :param path: Previously downloaded zipfile.
96 +
    :param no_input: Suppress prompt.
97 +
    :return: True if the content will be re-used.
98 +
    """
99 +
    if no_input:
100 +
        ok_to_reuse = False
99 101
    else:
100 102
        ok_to_reuse = read_user_yes_no(
101 103
            "Do you want to re-use the existing version?", 'yes'
102 104
        )
103 -
104 -
        if ok_to_reuse:
105 -
            return False
106 -
107 -
        sys.exit()
105 +
    return ok_to_reuse
Files Coverage
cookiecutter 99.30%
__main__.py 100.00%
Project Totals (19 files) 99.30%
2982.3
TRAVIS_PYTHON_VERSION=3.7
TRAVIS_OS_NAME=linux
TOXENV=py37
2982.4
TRAVIS_PYTHON_VERSION=3.8
TRAVIS_OS_NAME=linux
TOXENV=py38
2982.2
TRAVIS_PYTHON_VERSION=3.6
TRAVIS_OS_NAME=linux
TOXENV=py36
2982.7
TRAVIS_OS_NAME=windows
TOXENV=py38
2982.5
TRAVIS_OS_NAME=windows
TOXENV=py36
2982.6
TRAVIS_OS_NAME=windows
TOXENV=py37
1
# comment spam as user can always click the failed coverage check
2
comment: false
Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading