diff options
author | Lars Wirzenius <liw@liw.fi> | 2016-02-13 19:36:58 +0200 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2016-02-14 11:11:05 +0200 |
commit | 2131ab187c0a482904a2d3a8a60942ce23f10348 (patch) | |
tree | 457ab08b45dbf4781ef78a7caf4edf20d779fda8 | |
parent | e40d9119c43e475951b1ce63cd9d67b44516cd89 (diff) | |
download | bumper-2131ab187c0a482904a2d3a8a60942ce23f10348.tar.gz |
Add preliminary initial scenario
-rwxr-xr-x | bumper | 63 | ||||
-rw-r--r-- | bumper.yarn | 129 | ||||
-rwxr-xr-x | check | 15 | ||||
-rw-r--r-- | yarnstep.py | 27 |
4 files changed, 234 insertions, 0 deletions
@@ -0,0 +1,63 @@ +#!/usr/bin/env python2 +# Copyright 2016 Lars Wirzenius +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# =*= License: GPL-3+ =*= + + +import cliapp + + +class Bumper(cliapp.Application): + + def process_args(self, args): + print 'args:', repr(args) + version = args[0] + filename = args[1] + self.write_version_py(filename, version) + self.commit(filename) + self.make_release_tag(version) + + def commit(self, filename): + cliapp.runcmd(['git', 'commit', '-m', 'Version bump', filename]) + + def make_release_tag(self, version): + name = self.get_project_name() + tag_name = '{}-{}'.format(name, version) + msg = 'Release version {}'.format(version) + cliapp.runcmd(['git', 'tag', '-am', msg, tag_name]) + + def get_project_name(self): + output = cliapp.runcmd(['python', 'setup.py', '--name']) + return output.strip() + + def write_version_py(self, filename, version): + version_info = self.parse_version_info(version) + with open(filename, 'w') as f: + f.write('__version__ = "{}"\n'.format(version)) + f.write('__version_info__ = {!r}\n'.format(version_info)) + + def parse_version_info(self, version): + parts = version.split('.') + result = [] + for part in parts: + try: + result.append(int(part)) + except ValueError: + result.append(part) + return tuple(result) + + +Bumper().run() diff --git a/bumper.yarn b/bumper.yarn new file mode 100644 index 0000000..b29286a --- /dev/null +++ b/bumper.yarn @@ -0,0 +1,129 @@ +--- +title: Bump version numbers when releasing +author: Lars Wirzenius +version: git version +... + + +# Introduction + +Bumper is a small utility for updating version numbers when making a +release of a software project. It updates the version number in the +various locations in which it is stored in the source tree, creates a +git tag for indicating the release, then updates the source tree again +with a version number that indicates an unreleased version. + +# Assumptions about the source tree + +Bumper makes several assumptions about the source tree, to keep things +simpler. Here's a list: + +* The source tree is stored in git. No other version control systems + are supported, sorry, but that's just because git is the only thing + the author uses now. + +* The project is in Python. Again, this is just because that's what + the author uses. + +* The code gets its version numbers from two variables in a Python + module that contains nothing else. The variables are `__version__` + and `__version_info__`. Bumper will overwrite the file with new + values when it runs, and commit its version to git. + +* It's OK for Bumper to make several commits and a tag in the git + repository. + + +# Using Bumper + +In the examples below, we'll use Bumper on a fairly typical Python +project called `foo`, and we'll make a release 3.2 of it. + + SCENARIO release a project + +The Foo project consists of a main program, which uses a little Python +package where all the real code is, and where the version number is +also stored. + + GIVEN Python project foo, version controlled by git + AND a file foolib/version.py in foo containing + ... "__version__ = '0.0'\n__version_info__ = (0, 0)\n" + +We run Bumper, and it does several things. + + WHEN user runs "bumper 3.2 foolib/version.py" in the foo directory + THEN git repository foo has tag foo-3.2 + AND in foo, tag foo-3.2, foolib/version.py contains + ... "__version__ = "3.2"\n__version_info__ = (3, 2)\n" + AND file foolib/version.py in foo contains + ... "__version__ = "3.2"\n__version_info__ = (3, 2)\n" + + +# Appendix: Scenario step implementations + +This chapter provides executable implementations of the various +scenario steps, making this manual an automated [yarn][] test suite +for Bumper. + +[yarn]: http://liw.fi/cmdtest/ + + IMPLEMENTS GIVEN Python project (\S+), version controlled by git + import os, cliapp, yarnstep + project = yarnstep.get_next_match() + dirname = yarnstep.datadir(project) + yarnstep.write_file(os.path.join(dirname, 'setup.py'), ''' + from distutils.core import setup + setup(name='{project}') + '''.format(project=project)) + cliapp.runcmd(['git', 'init', dirname]) + + IMPLEMENTS GIVEN a file (\S+) in (\S+) containing "(.*)" + import os, cliapp, yarnstep + filename = yarnstep.get_next_match() + dirname = yarnstep.get_next_match_as_datadir_path() + data = yarnstep.get_next_match() + yarnstep.write_file( + os.path.join(dirname, filename), + yarnstep.unescape_backslashes(data)) + cliapp.runcmd(['git', 'add', filename], cwd=dirname) + cliapp.runcmd( + ['git', 'commit', '-m', 'Add {}'.format(filename)], + cwd=dirname) + + IMPLEMENTS WHEN user runs "bumper (\S+) (\S+)" in the (\S+) directory + import cliapp, yarnstep + version = yarnstep.get_next_match() + filename = yarnstep.get_next_match() + dirname = yarnstep.get_next_match_as_datadir_path() + bin = yarnstep.srcdir('bumper') + cliapp.runcmd([bin, version, filename], cwd=dirname) + + IMPLEMENTS THEN file (\S+) in (\S+) contains "(.*)" + import os, yarnstep + filename = yarnstep.get_next_match() + dirname = yarnstep.get_next_match_as_datadir_path() + wanted_data_escaped = yarnstep.get_next_match() + wanted_data = yarnstep.unescape_backslashes(wanted_data_escaped) + actual_data = yarnstep.cat(os.path.join(dirname, filename)) + assert wanted_data == actual_data + + IMPLEMENTS THEN in (\S+), tag (\S+), (\S+) contains "(.*)" + import cliapp, yarnstep + dirname = yarnstep.get_next_match_as_datadir_path() + tag = yarnstep.get_next_match() + filename = yarnstep.get_next_match() + wanted_data_escaped = yarnstep.get_next_match() + wanted_data = yarnstep.unescape_backslashes(wanted_data_escaped) + actual_data = cliapp.runcmd( + ['git', 'cat-file', 'blob', '{}:{}'.format(tag, filename)], + cwd=dirname) + print 'wanted:', repr(wanted_data) + print 'actual:', repr(actual_data) + assert wanted_data == actual_data + + IMPLEMENTS THEN git repository (\S+) has tag (\S+) + import cliapp, yarnstep + dirname = yarnstep.get_next_match_as_datadir_path() + tagname = yarnstep.get_next_match() + output = cliapp.runcmd(['git', 'show', tagname], cwd=dirname) + assert output.startswith('tag ' + tagname) @@ -0,0 +1,15 @@ +#!/bin/sh + +set -eu + +# We need to set PYTHONPATH so that yarnstep.py is found in the +# IMPLEMENTS when yarn runs them. +if env | grep '^PYTHONPATH=' > /dev/null +then + PYTHONPATH="$PYTHONPATH:." +else + PYTHONPATH="." +fi + +yarn --env "PYTHONPATH=$PYTHONPATH" \ + --shell python2 --shell-arg '' *.yarn "$@" diff --git a/yarnstep.py b/yarnstep.py index 1935cc6..30ccae1 100644 --- a/yarnstep.py +++ b/yarnstep.py @@ -58,3 +58,30 @@ def iter_over_files(root): def cat(filename): with open(filename) as f: return f.read() + + +def write_file(filename, data): + dirname = os.path.dirname(filename) + if not os.path.exists(dirname): + os.makedirs(dirname) + with open(filename, 'w') as f: + f.write(data) + + +def unescape_backslashes(s): + result = '' + while s: + if s.startswith('\\') and len(s) > 1: + result += unescape_char(s[1]) + s = s[2:] + else: + result += s[0] + s = s[1:] + return result + + +def unescape_char(c): + table = { + 'n': '\n', + } + return table.get(c, c) |