Unverified Commit e5c2459f authored by Waylan Limberg's avatar Waylan Limberg Committed by GitHub
Browse files

Warn users deploying with older version. (#1467)

Fixes #640.
parent ee376654
......@@ -66,6 +66,7 @@ authors should review how [search and themes] interact.
### Other Changes and Additions to Development Version
* Add MkDocs version check to gh-deploy script (#640).
* Improve Markdown extension error messages. (#782).
* Drop official support for Python 3.3 and set `tornado>=5.0` (#1427).
* Add support for GitLab edit links (#1435).
......
......@@ -89,6 +89,7 @@ remote_branch_help = ("The remote branch to commit to for Github Pages. This "
remote_name_help = ("The remote name to commit to for Github Pages. This "
"overrides the value specified in config")
force_help = "Force the push to the repository."
ignore_version_help = "Ignore check that build is not being deployed with an older version of MkDocs."
pgk_dir = os.path.dirname(os.path.abspath(__file__))
......@@ -172,8 +173,9 @@ def build_command(clean, config_file, strict, theme, theme_dir, site_dir):
@click.option('-b', '--remote-branch', help=remote_branch_help)
@click.option('-r', '--remote-name', help=remote_name_help)
@click.option('--force', is_flag=True, help=force_help)
@click.option('--ignore-version', is_flag=True, help=ignore_version_help)
@common_options
def gh_deploy_command(config_file, clean, message, remote_branch, remote_name, force):
def gh_deploy_command(config_file, clean, message, remote_branch, remote_name, force, ignore_version):
"""Deploy your documentation to GitHub Pages"""
try:
cfg = config.load_config(
......@@ -182,7 +184,7 @@ def gh_deploy_command(config_file, clean, message, remote_branch, remote_name, f
remote_name=remote_name
)
build.build(cfg, dirty=not clean)
gh_deploy.gh_deploy(cfg, message=message, force=force)
gh_deploy.gh_deploy(cfg, message=message, force=force, ignore_version=ignore_version)
except exceptions.ConfigurationError as e: # pragma: no cover
# Avoid ugly, unhelpful traceback
raise SystemExit('\n' + str(e))
......
......@@ -2,6 +2,8 @@ from __future__ import unicode_literals
import logging
import subprocess
import os
import re
from pkg_resources import parse_version
import mkdocs
from mkdocs.utils import ghp_import
......@@ -49,20 +51,49 @@ def _get_remote_url(remote_name):
return host, path
def gh_deploy(config, message=None, force=False):
def _check_version(branch):
proc = subprocess.Popen(['git', 'show', '-s', '--format=%s', 'refs/heads/{}'.format(branch)],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, _ = proc.communicate()
msg = stdout.decode('utf-8').strip()
m = re.search(r'\d+(\.\d+)+', msg, re.X | re.I)
previousv = parse_version(m.group()) if m else None
currentv = parse_version(mkdocs.__version__)
if not previousv:
log.warn('Version check skipped: No version specificed in previous deployment.')
elif currentv > previousv:
log.info(
'Previous deployment was done with MkDocs version {}; '
'you are deploying with a newer version ({})'.format(previousv, currentv)
)
elif currentv < previousv:
log.error(
'Deployment terminated: Previous deployment was made with MkDocs version {}; '
'you are attempting to deploy with an older version ({}). Use --ignore-version '
'to deploy anyway.'.format(previousv, currentv)
)
raise SystemExit(1)
def gh_deploy(config, message=None, force=False, ignore_version=False):
if not _is_cwd_git_repo():
log.error('Cannot deploy - this directory does not appear to be a git '
'repository')
remote_branch = config['remote_branch']
remote_name = config['remote_name']
if not ignore_version:
_check_version(remote_branch)
if message is None:
message = default_message
sha = _get_current_sha(os.path.dirname(config.config_file_path))
message = message.format(version=mkdocs.__version__, sha=sha)
remote_branch = config['remote_branch']
remote_name = config['remote_name']
log.info("Copying '%s' to '%s' branch and pushing to GitHub.",
config['site_dir'], config['remote_branch'])
......
......@@ -2,9 +2,14 @@ from __future__ import unicode_literals
import textwrap
import markdown
import os
import logging
import collections
import unittest
from mkdocs import toc
from mkdocs import config
from mkdocs import utils
def dedent(text):
......@@ -37,3 +42,97 @@ def load_config(**cfg):
errors_warnings = conf.validate()
assert(errors_warnings == ([], [])), errors_warnings
return conf
# Backport unittest.TestCase.assertLogs for Python 2.7
# see https://github.com/python/cpython/blob/3.6/Lib/unittest/case.py
if not utils.PY3:
_LoggingWatcher = collections.namedtuple("_LoggingWatcher",
["records", "output"])
class _CapturingHandler(logging.Handler):
"""
A logging handler capturing all (raw and formatted) logging output.
"""
def __init__(self):
logging.Handler.__init__(self)
self.watcher = _LoggingWatcher([], [])
def flush(self):
pass
def emit(self, record):
self.watcher.records.append(record)
msg = self.format(record)
self.watcher.output.append(msg)
class _AssertLogsContext(object):
"""A context manager used to implement TestCase.assertLogs()."""
LOGGING_FORMAT = "%(levelname)s:%(name)s:%(message)s"
def __init__(self, test_case, logger_name, level):
self.test_case = test_case
self.logger_name = logger_name
if level:
self.level = logging._levelNames.get(level, level)
else:
self.level = logging.INFO
self.msg = None
def __enter__(self):
if isinstance(self.logger_name, logging.Logger):
logger = self.logger = self.logger_name
else:
logger = self.logger = logging.getLogger(self.logger_name)
formatter = logging.Formatter(self.LOGGING_FORMAT)
handler = _CapturingHandler()
handler.setFormatter(formatter)
self.watcher = handler.watcher
self.old_handlers = logger.handlers[:]
self.old_level = logger.level
self.old_propagate = logger.propagate
logger.handlers = [handler]
logger.setLevel(self.level)
logger.propagate = False
return handler.watcher
def __exit__(self, exc_type, exc_value, tb):
self.logger.handlers = self.old_handlers
self.logger.propagate = self.old_propagate
self.logger.setLevel(self.old_level)
if exc_type is not None:
# let unexpected exceptions pass through
return False
if len(self.watcher.records) == 0:
self._raiseFailure(
"no logs of level {} or higher triggered on {}"
.format(logging.getLevelName(self.level), self.logger.name))
def _raiseFailure(self, standardMsg):
msg = self.test_case._formatMessage(self.msg, standardMsg)
raise self.test_case.failureException(msg)
class LogTestCase(unittest.TestCase):
def assertLogs(self, logger=None, level=None):
"""Fail unless a log message of level *level* or higher is emitted
on *logger_name* or its children. If omitted, *level* defaults to
INFO and *logger* defaults to the root logger.
This method must be used as a context manager, and will yield
a recording object with two attributes: `output` and `records`.
At the end of the context manager, the `output` attribute will
be a list of the matching formatted log messages and the
`records` attribute will be a list of the corresponding LogRecord
objects.
Example::
with self.assertLogs('foo', level='INFO') as cm:
logging.getLogger('foo').info('first message')
logging.getLogger('foo.bar').error('second message')
self.assertEqual(cm.output, ['INFO:foo:first message',
'ERROR:foo.bar:second message'])
"""
return _AssertLogsContext(self, logger, level)
else:
LogTestCase = unittest.TestCase
......@@ -346,6 +346,8 @@ class CLITests(unittest.TestCase):
self.assertEqual(g_kwargs['message'], None)
self.assertTrue('force' in g_kwargs)
self.assertEqual(g_kwargs['force'], False)
self.assertTrue('ignore_version' in g_kwargs)
self.assertEqual(g_kwargs['ignore_version'], False)
self.assertEqual(mock_build.call_count, 1)
b_args, b_kwargs = mock_build.call_args
self.assertTrue('dirty' in b_kwargs)
......@@ -471,3 +473,19 @@ class CLITests(unittest.TestCase):
self.assertEqual(g_kwargs['force'], True)
self.assertEqual(mock_build.call_count, 1)
self.assertEqual(mock_load_config.call_count, 1)
@mock.patch('mkdocs.config.load_config', autospec=True)
@mock.patch('mkdocs.commands.build.build', autospec=True)
@mock.patch('mkdocs.commands.gh_deploy.gh_deploy', autospec=True)
def test_gh_deploy_ognore_version(self, mock_gh_deploy, mock_build, mock_load_config):
result = self.runner.invoke(
cli.cli, ['gh-deploy', '--ignore-version'], catch_exceptions=False)
self.assertEqual(result.exit_code, 0)
self.assertEqual(mock_gh_deploy.call_count, 1)
g_args, g_kwargs = mock_gh_deploy.call_args
self.assertTrue('ignore_version' in g_kwargs)
self.assertEqual(g_kwargs['ignore_version'], True)
self.assertEqual(mock_build.call_count, 1)
self.assertEqual(mock_load_config.call_count, 1)
......@@ -3,8 +3,9 @@ from __future__ import unicode_literals
import unittest
import mock
from mkdocs.tests.base import load_config
from mkdocs.tests.base import load_config, LogTestCase
from mkdocs.commands import gh_deploy
from mkdocs import __version__
class TestGitHubDeploy(unittest.TestCase):
......@@ -99,6 +100,32 @@ class TestGitHubDeploy(unittest.TestCase):
)
gh_deploy.gh_deploy(config)
@mock.patch('mkdocs.commands.gh_deploy._is_cwd_git_repo', return_value=True)
@mock.patch('mkdocs.commands.gh_deploy._get_current_sha', return_value='shashas')
@mock.patch('mkdocs.commands.gh_deploy._get_remote_url', return_value=(None, None))
@mock.patch('mkdocs.commands.gh_deploy._check_version')
@mock.patch('mkdocs.commands.gh_deploy.ghp_import.ghp_import', return_value=(True, ''))
def test_deploy_ignore_version_default(self, mock_import, check_version, get_remote, get_sha, is_repo):
config = load_config(
remote_branch='test',
)
gh_deploy.gh_deploy(config)
check_version.assert_called_once()
@mock.patch('mkdocs.commands.gh_deploy._is_cwd_git_repo', return_value=True)
@mock.patch('mkdocs.commands.gh_deploy._get_current_sha', return_value='shashas')
@mock.patch('mkdocs.commands.gh_deploy._get_remote_url', return_value=(None, None))
@mock.patch('mkdocs.commands.gh_deploy._check_version')
@mock.patch('mkdocs.commands.gh_deploy.ghp_import.ghp_import', return_value=(True, ''))
def test_deploy_ignore_version(self, mock_import, check_version, get_remote, get_sha, is_repo):
config = load_config(
remote_branch='test',
)
gh_deploy.gh_deploy(config, ignore_version=True)
check_version.assert_not_called()
@mock.patch('mkdocs.utils.ghp_import.ghp_import')
@mock.patch('mkdocs.commands.gh_deploy.log')
def test_deploy_error(self, mock_log, mock_import):
......@@ -112,3 +139,43 @@ class TestGitHubDeploy(unittest.TestCase):
self.assertRaises(SystemExit, gh_deploy.gh_deploy, config)
mock_log.error.assert_called_once_with('Failed to deploy to GitHub with error: \n%s',
error_string)
class TestGitHubDeployLogs(LogTestCase):
@mock.patch('subprocess.Popen')
def test_mkdocs_newer(self, mock_popeno):
mock_popeno().communicate.return_value = (b'Deployed 12345678 with MkDocs version: 0.1.2\n', b'')
with self.assertLogs('mkdocs', level='INFO') as cm:
gh_deploy._check_version('gh-pages')
self.assertEqual(
cm.output, ['INFO:mkdocs.commands.gh_deploy:Previous deployment was done with MkDocs '
'version 0.1.2; you are deploying with a newer version ({})'.format(__version__)]
)
@mock.patch('subprocess.Popen')
def test_mkdocs_older(self, mock_popeno):
mock_popeno().communicate.return_value = (b'Deployed 12345678 with MkDocs version: 10.1.2\n', b'')
with self.assertLogs('mkdocs', level='ERROR') as cm:
self.assertRaises(SystemExit, gh_deploy._check_version, 'gh-pages')
self.assertEqual(
cm.output, ['ERROR:mkdocs.commands.gh_deploy:Deployment terminated: Previous deployment was made with '
'MkDocs version 10.1.2; you are attempting to deploy with an older version ({}). Use '
'--ignore-version to deploy anyway.'.format(__version__)]
)
@mock.patch('subprocess.Popen')
def test_version_unknown(self, mock_popeno):
mock_popeno().communicate.return_value = (b'No version specified\n', b'')
with self.assertLogs('mkdocs', level='WARNING') as cm:
gh_deploy._check_version('gh-pages')
self.assertEqual(
cm.output,
['WARNING:mkdocs.commands.gh_deploy:Version check skipped: No version specificed in previous deployment.']
)
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment