diff options
Diffstat (limited to 'tasks.py')
-rw-r--r-- | tasks.py | 188 |
1 files changed, 188 insertions, 0 deletions
diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..e271c99 --- /dev/null +++ b/tasks.py @@ -0,0 +1,188 @@ +# -*- coding: utf-8 -*- +import os +import re +import io +from contextlib import contextmanager +from datetime import datetime + +from invoke import task, run + +VERSION_FILE = 'pytest_catchlog.py' +CHANGE_LOG_FILE = 'CHANGES.rst' + + +def _path_abs_join(*nodes): + return os.path.abspath(os.path.join(os.path.dirname(__file__), *nodes)) + + +def _path_open(*nodes, **kwargs): + return io.open(_path_abs_join(*nodes), **kwargs) + + +def _shell_quote(s): + """Quote given string to be suitable as input for bash as argument.""" + if not s: + return "''" + if re.search(r'[^\w@%+=:,./-]', s) is None: + return s + return "'" + s.replace("'", "'\"'\"'") + "'" + + +def _git_do(*commands, **kwargs): + """Execute arbitrary git commands.""" + kwargs.setdefault('hide', 'out') + results = [run('git ' + command, **kwargs).stdout.strip('\n') + for command in commands] + return results if len(commands) > 1 else results[0] + + +def _git_checkout(branch_name): + """Switches to the given branch name.""" + return _git_do('checkout ' + _shell_quote(branch_name)) + + +@contextmanager +def _git_work_on(branch_name): + """Work on given branch. Preserves current git branch.""" + original_branch = _git_do('rev-parse --abbrev-ref HEAD') + try: + if original_branch != branch_name: + _git_checkout(branch_name) + yield + finally: + if original_branch and original_branch != branch_name: + _git_checkout(original_branch) + + +def _version_find_existing(): + """Returns set of existing versions in this repository. + + This information is backed by previously used version tags + stored in the git repository. + """ + git_tags = [y.strip() for y in _git_do('tag -l').split('\n')] + + _version_re = re.compile(r'^v?(\d+)(?:\.(\d+)(?:\.(\d+))?)?$') + return {tuple(int(n) if n else 0 for n in m.groups()) + for m in (_version_re.match(t) for t in git_tags if t) if m} + + +def _version_find_latest(): + """Returns the most recent used version number. + + This information is backed by previously used version tags + stored in the git repository. + """ + return max(_version_find_existing()) + + +def _version_guess_next(position='minor'): + """Guess next version. + + A guess for the next version is determined by incrementing given + position or minor level position in latest existing version. + """ + try: + latest_version = list(_version_find_latest()) + except ValueError: + latest_version = [0, 0, 0] + + position_index = {'major': 0, 'minor': 1, 'patch': 2}[position] + latest_version[position_index] += 1 + latest_version[position_index + 1:] = [0] * (2 - position_index) + return tuple(latest_version) + + +def _version_format(version): + """Return version in dotted string format.""" + return '.'.join(str(x) for x in version) + + +def _patch_file(file_path, line_callback): + """Patch given file with result from line callback. + + Each line will be passed to the line callback. + The return value of the given callback will determine + the new content for the file. + + :param str file_path: + The file to patch. + :param callable line_callback: + The patch function to run over each line. + :return: + Whenever the file has changed or not. + :rtype: + bool + """ + new_file_content, file_changed = [], False + with _path_open(file_path) as in_stream: + for l in (x.strip('\n') for x in in_stream): + alt_lines = line_callback(l) or [l] + if alt_lines != [l]: + file_changed = True + new_file_content += (x + u'\n' for x in alt_lines) + + new_file_name = file_path + '.new' + with _path_open(new_file_name, mode='w') as out_stream: + out_stream.writelines(new_file_content) + out_stream.flush() + os.fsync(out_stream.fileno()) + os.rename(new_file_name, file_path) + + return file_changed + + +def _patch_version(new_version): + """Patch given version into version file.""" + _patch_version_re = re.compile(r"""^(\s*__version__\s*=\s*(?:"|'))""" + r"""(?:[^'"]*)(?:("|')\s*)$""") + + def __line_callback(line): + match = _patch_version_re.match(line) + if match: + line_head, line_tail = match.groups() + return [line_head + new_version + line_tail] + return _patch_file(VERSION_FILE, __line_callback) + + +def _patch_change_log(new_version): + """Patch given version into change log file.""" + def __line_callback(line): + if line == u'`Unreleased`_': + return [u'`{}`_'.format(new_version)] + elif line == u'Yet to be released.': + return [datetime.utcnow().strftime(u'Released on %F.')] + elif line == u'.. %UNRELEASED_SECTION%': + return [u'.. %UNRELEASED_SECTION%', + u'', + u'`Unreleased`_', + u'-------------', + u'', + u'Yet to be released.', + u''] + return _patch_file(CHANGE_LOG_FILE, __line_callback) + + +@task() +def mkrelease(position='minor'): + """Merge development state into Master Branch and tags a new Release.""" + next_version = _version_format(_version_guess_next(position)) + with _git_work_on('develop'): + patched_files = [] + if _patch_version(next_version): + patched_files.append(VERSION_FILE) + + if _patch_change_log(next_version): + patched_files.append(CHANGE_LOG_FILE) + + if patched_files: + patched_files = ' '.join(_shell_quote(x) for x in patched_files) + _git_do('diff --color=always -- ' + patched_files, + ("commit -m 'Bump Version to {0}' -- {1}" + .format(next_version, patched_files)), + hide=None) + + with _git_work_on('master'): + message = _shell_quote('Release {0}'.format(next_version)) + _git_do('merge --no-ff --no-edit -m {0} develop'.format(message), + "tag -a -m {0} {1}".format(message, next_version)) |