summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2016-02-13 19:36:58 +0200
committerLars Wirzenius <liw@liw.fi>2016-02-14 11:11:05 +0200
commit2131ab187c0a482904a2d3a8a60942ce23f10348 (patch)
tree457ab08b45dbf4781ef78a7caf4edf20d779fda8
parente40d9119c43e475951b1ce63cd9d67b44516cd89 (diff)
downloadbumper-2131ab187c0a482904a2d3a8a60942ce23f10348.tar.gz
Add preliminary initial scenario
-rwxr-xr-xbumper63
-rw-r--r--bumper.yarn129
-rwxr-xr-xcheck15
-rw-r--r--yarnstep.py27
4 files changed, 234 insertions, 0 deletions
diff --git a/bumper b/bumper
new file mode 100755
index 0000000..fe37f14
--- /dev/null
+++ b/bumper
@@ -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)
diff --git a/check b/check
new file mode 100755
index 0000000..ad52a7a
--- /dev/null
+++ b/check
@@ -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)