Revision 55775645

b/.gitignore
1
*.db
2
*.pyc
3
*~
4
*.*.swp
5
bin/
6
share/
7
build/
8
include/
9
*.pt.py
10
*.installed.cfg
11
*.sqlite
12
.Python
13
.idea
14
.DS_Store
15
selenium-server-standalone-2.0b2.jar
16
.project
17
.pydevproject
18
.settings/
19
settings.d/*-local.conf
20
*.egg-info
21
dist
22
_build
23

  
24
# version module created automatically from setup.py
25
/devflow/version.py
b/devflow/autopkg.py
1
# Copyright 2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

  
34
import git
35
import os
36
import sys
37
from sh import mktemp, cd, rm, git_dch, python
38
from optparse import OptionParser
39

  
40
try:
41
    from colors import red, green
42
except ImportError:
43
    red = lambda x: x
44
    green = lambda x: x
45

  
46
print_red = lambda x: sys.stdout.write(red(x) + "\n")
47
print_green = lambda x: sys.stdout.write(green(x) + "\n")
48

  
49
AVAILABLE_MODES = ["release", "snapshot"]
50

  
51
# TODO: Make the PACKAGES tuple configurable,
52
# read it from autopkg.conf in the git toplevel directory.
53
PACKAGES = ("devflow", )
54

  
55

  
56
def main():
57
    from devflow.version import __version__
58

  
59
    parser = OptionParser(usage="usage: %prog [options] mode",
60
                          version="%prog - devflow %s" % __version__)
61
    parser.add_option("-k", "--keep-repo",
62
                      action="store_true",
63
                      dest="keep_repo",
64
                      default=False,
65
                      help="Do not delete the cloned repository")
66
    parser.add_option("-b", "--build-dir",
67
                      dest="build_dir",
68
                      default=None,
69
                      help="Directory to store created pacakges")
70
    parser.add_option("-r", "--repo-dir",
71
                      dest="repo_dir",
72
                      default=None,
73
                      help="Directory to clone repository")
74
    parser.add_option("-d", "--dirty",
75
                      dest="force_dirty",
76
                      default=False,
77
                      action="store_true",
78
                      help="Do not check if working directory is dirty")
79

  
80
    (options, args) = parser.parse_args()
81

  
82
    mode = args[0]
83
    if mode not in AVAILABLE_MODES:
84
        raise ValueError(red("Invalid argument! Mode must be one: %s"
85
                         % ", ".join(AVAILABLE_MODES)))
86

  
87
    # Do not prompt for merge message. Required for some Git versions
88
    os.environ["GITFLOW_BUILD_MODE"] = mode
89

  
90
    try:
91
        original_repo = git.Repo(".")
92
    except git.git.InvalidGitRepositoryError:
93
        raise RuntimeError(red("Current directory is not git repository."))
94

  
95
    if original_repo.is_dirty() and not options.force_dirty:
96
        toplevel = original_repo.working_dir
97
        raise RuntimeError(red("Repository %s is dirty." % toplevel))
98

  
99
    repo_dir = options.repo_dir
100
    if not repo_dir:
101
        repo_dir = mktemp("-d", "/tmp/devflow-build-repo-XXX").stdout.strip()
102
        print_green("Created temporary directory '%s' for the cloned repo."
103
                    % repo_dir)
104

  
105
    repo = original_repo.clone(repo_dir)
106
    print_green("Cloned current repository to '%s'." % repo_dir)
107

  
108
    reflog_hexsha = repo.head.log()[-1].newhexsha
109
    print "Latest Reflog entry is %s" % reflog_hexsha
110

  
111
    branch = repo.head.reference.name
112
    if branch == "master":
113
        debian_branch = "debian"
114
    else:
115
        debian_branch = "debian-" + branch
116

  
117
    try:
118
        repo.references[debian_branch]
119
    except IndexError:
120
        # Branch does not exist
121
        # FIXME: remove hard-coded strings..
122
        if branch == "debian":
123
            repo.git.branch("--track", debian_branch, "origin/debian")
124
        else:
125
            repo.git.branch("--track", debian_branch, "origin/debian-develop")
126

  
127
    repo.git.checkout(debian_branch)
128
    print_green("Changed to branch '%s'" % debian_branch)
129

  
130
    repo.git.merge(branch)
131
    print_green("Merged branch '%s' into '%s'" % (branch, debian_branch))
132

  
133
    cd(repo_dir)
134
    version = python(repo_dir + "/devflow/version.py", "debian").strip()
135
    print_green("The new debian version will be: '%s'" % version)
136

  
137
    dch = git_dch("--debian-branch=%s" % debian_branch,
138
            "--git-author",
139
            "--ignore-regex=\".*\"",
140
            "--multimaint-merge",
141
            "--since=HEAD",
142
            "--new-version=%s" % version)
143
    print_green("Successfully ran '%s'" % " ".join(dch.cmd))
144

  
145
    os.system("vim debian/changelog")
146
    repo.git.add("debian/changelog")
147

  
148
    if mode == "release":
149
        repo.git.commit("-s", "-a", "-m", "Bump new upstream version")
150
        if branch == "master":
151
            repo.git.tag("debian/" + version)
152

  
153
    for package in PACKAGES:
154
        # python setup.py should run in its directory
155
        cd(package)
156
        package_dir = repo_dir + "/" + package
157
        res = python(package_dir + "/setup.py", "sdist", _out=sys.stdout)
158
        cd("../")
159
        print res.stdout
160

  
161
    # Add version.py files to repo
162
    os.system("grep \"__version_vcs\" -r . -l -I | xargs git add -f")
163

  
164
    build_dir = options.build_dir
165
    if not options.build_dir:
166
        build_dir = mktemp("-d", "/tmp/devflow-build-XXX").stdout.strip()
167
        print_green("Created directory '%s' to store the .deb files." %
168
                     build_dir)
169

  
170
    os.system("git-buildpackage --git-export-dir=%s --git-upstream-branch=%s"
171
              " --git-debian-branch=%s --git-export=INDEX --git-ignore-new -sa"
172
              % (build_dir, branch, debian_branch))
173

  
174
    if not options.keep_repo:
175
        print_green("Removing cloned repo '%s'." % repo_dir)
176
        rm("-r", repo_dir)
177
    else:
178
        print_green("Repository dir '%s'" % repo_dir)
179

  
180
    print_green("Completed. Version '%s', build area: '%s'"
181
                % (version, build_dir))
182

  
183

  
184
if __name__ == "__main__":
185
    sys.exit(main())
b/devflow/devflow.version_unittest.py
1
#!/usr/bin/env python
2
#
3
# Copyright 2012 GRNET S.A. All rights reserved.
4
#
5
# Redistribution and use in source and binary forms, with or
6
# without modification, are permitted provided that the following
7
# conditions are met:
8
#
9
#   1. Redistributions of source code must retain the above
10
#      copyright notice, this list of conditions and the following
11
#      disclaimer.
12
#
13
#   2. Redistributions in binary form must reproduce the above
14
#      copyright notice, this list of conditions and the following
15
#      disclaimer in the documentation and/or other materials
16
#      provided with the distribution.
17
#
18
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
# POSSIBILITY OF SUCH DAMAGE.
30
#
31
# The views and conclusions contained in the software and
32
# documentation are those of the authors and should not be
33
# interpreted as representing official policies, either expressed
34
# or implied, of GRNET S.A.
35
#
36
#
37

  
38
"""Unit Tests for devflow.versioning
39

  
40
Provides unit tests for module devflow.versioning,
41
for automatic generation of version strings.
42

  
43
"""
44

  
45
import os
46
import unittest
47
from pkg_resources import parse_version
48
from versioning import debian_version_from_python_version
49

  
50

  
51
class DebianVersionObject(object):
52
    """Object representing a Debian Version."""
53
    def __init__(self, pyver):
54
        self.version = debian_version_from_python_version(pyver)
55

  
56
    def __str__(self):
57
        return self.version
58

  
59

  
60
def debian_compare_versions(a, op, b):
61
    i = os.system("dpkg --compare-versions %s %s %s" % (a, op, b))
62
    return i == 0
63

  
64
# Set ordering between DebianVersionObject objects, by adding
65
# debian_compare_versions
66
for op in ["lt", "le", "eq", "ne", "gt", "ge"]:
67
    def gen(op):
68
        def operator_func(self, other):
69
            return debian_compare_versions(self.version, op, other.version)
70
        return operator_func
71
    setattr(DebianVersionObject, "__%s__" % op, gen(op))
72

  
73

  
74
def _random_commit():
75
    import random
76
    import string
77
    return "".join(random.choice(string.hexdigits) for n in xrange(8)).lower()
78

  
79

  
80
# Add a random commit number at the end of snapshot versions
81
def version_with_commit(parse_func, v):
82
    if "_" in v:
83
        return parse_func(v + "_" + _random_commit())
84
    else:
85
        return parse_func(v)
86

  
87
V = lambda v: version_with_commit(parse_version, v)
88
D = lambda v: version_with_commit(DebianVersionObject, v)
89

  
90

  
91
class TestVersionFunctions(unittest.TestCase):
92
    def setUp(self):
93
        self.version_orderings = (
94
            ("0.14next", ">", "0.14"),
95
            ("0.14next", ">", "0.14rc7"),
96
            ("0.14next", "<", "0.14.1"),
97
            ("0.14rc6", "<", "0.14"),
98
            ("0.14.2rc6", ">", "0.14.1"),
99
            ("0.14next_150", "<", "0.14next"),
100
            ("0.14.1next_150", "<", "0.14.1next"),
101
            ("0.14.1_149", "<", "0.14.1"),
102
            ("0.14.1_149", "<", "0.14.1_150"),
103
            ("0.13next_102", "<", "0.13next"),
104
            ("0.13next", "<", "0.14rc5_120"),
105
            ("0.14rc3_120", "<", "0.14rc3"),
106
            # The following test fails, but versioning.python_version
107
            # will never try to produce such a version:
108
            # ("0.14rc3", "<", "0.14_1"),
109
            ("0.14_120", "<", "0.14"),
110
            ("0.14", "<", "0.14next_20"),
111
            ("0.14next_20", "<", "0.14next"),
112
        )
113

  
114
    def test_python_versions(self):
115
        for a, op, b in self.version_orderings:
116
            res = compare(V, a, op, b)
117
            self.assertTrue(res, "Python version: %s %s %s"
118
                                 " is not True" % (a, op, b))
119

  
120
    def test_debian_versions(self):
121
        for a, op, b in self.version_orderings:
122
            res = compare(D, a, op, b)
123
            self.assertTrue(res, "Debian version %s %s %s"
124
                                 " is not True" % (a, op, b))
125

  
126

  
127
def compare(function, a, op, b):
128
    import operator
129
    str_to_op = {"<": operator.lt,
130
            "<=": operator.le,
131
            "==": operator.eq,
132
            ">": operator.gt,
133
            ">=": operator.ge}
134
    try:
135
        return str_to_op[op](function(a), function(b))
136
    except KeyError:
137
        raise ValueError("Unknown operator '%s'" % op)
138

  
139
if __name__ == '__main__':
140
    unittest.main()
b/devflow/versioning.py
1
#!/usr/bin/env python
2
#
3
# Copyright (C) 2010, 2011, 2012 GRNET S.A. All rights reserved.
4
#
5
# Redistribution and use in source and binary forms, with or
6
# without modification, are permitted provided that the following
7
# conditions are met:
8
#
9
#   1. Redistributions of source code must retain the above
10
#      copyright notice, this list of conditions and the following
11
#      disclaimer.
12
#
13
#   2. Redistributions in binary form must reproduce the above
14
#      copyright notice, this list of conditions and the following
15
#      disclaimer in the documentation and/or other materials
16
#      provided with the distribution.
17
#
18
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
# POSSIBILITY OF SUCH DAMAGE.
30
#
31
# The views and conclusions contained in the software and
32
# documentation are those of the authors and should not be
33
# interpreted as representing official policies, either expressed
34
# or implied, of GRNET S.A.
35

  
36

  
37
import os
38
import re
39
import sys
40
import pprint
41
import subprocess
42
import git
43

  
44
from distutils import log
45
from collections import namedtuple
46

  
47

  
48
# Branch types:
49
# builds_snapshot: Whether the branch can produce snapshot builds
50
# builds_release: Whether the branch can produce release builds
51
# versioned: Whether the name of the branch defines a specific version
52
# allowed_version_re: A regular expression describing allowed values for
53
#                     base_version in this branch
54
branch_type = namedtuple("branch_type", ["builds_snapshot", "builds_release",
55
                                         "versioned", "allowed_version_re"])
56
VERSION_RE = "[0-9]+\.[0-9]+"
57
BRANCH_TYPES = {
58
    "feature": branch_type(True, False, False, "^%snext$" % VERSION_RE),
59
    "develop": branch_type(True, False, False, "^%snext$" % VERSION_RE),
60
    "release": branch_type(True, True, True,
61
                           "^(?P<bverstr>%s)rc[1-9][0-9]*$" % VERSION_RE),
62
    "master": branch_type(False, True, False,
63
                          "^%s$" % VERSION_RE),
64
    "hotfix": branch_type(True, True, True,
65
                          "^(?P<bverstr>^%s\.[1-9][0-9]*)$" % VERSION_RE)}
66
BASE_VERSION_FILE = "version"
67

  
68

  
69
def get_commit_id(commit, current_branch):
70
    """Return the commit ID
71

  
72
    If the commit is a 'merge' commit, and one of the parents is a
73
    debian branch we return a compination of the parents commits.
74

  
75
    """
76
    def short_id(commit):
77
        return commit.hexsha[0:7]
78

  
79
    parents = commit.parents
80
    cur_br_name = current_branch.name
81
    if len(parents) == 1:
82
        return short_id(commit)
83
    elif len(parents) == 2:
84
        if cur_br_name.startswith("debian-") or cur_br_name == "debian":
85
            pr1, pr2 = parents
86
            return short_id(pr1) + "~" + short_id(pr2)
87
        else:
88
            return short_id(commit)
89
    else:
90
        raise RuntimeError("Commit %s has more than 2 parents!" % commit)
91

  
92

  
93
def vcs_info():
94
    """
95
    Return current git HEAD commit information.
96

  
97
    Returns a tuple containing
98
        - branch name
99
        - commit id
100
        - commit count
101
        - git describe output
102
        - path of git toplevel directory
103

  
104
    """
105
    try:
106
        repo = git.Repo(".")
107
        branch = repo.head.reference
108
        revid = get_commit_id(branch.commit, branch)
109
        revno = len(list(repo.iter_commits()))
110
        desc = repo.git.describe("--tags")
111
        toplevel = repo.working_dir
112
    except git.InvalidGitRepositoryError:
113
        log.error("Could not retrieve git information. " +
114
                  "Current directory not a git repository?")
115
        return None
116

  
117
    info = namedtuple("vcs_info", ["branch", "revid", "revno",
118
                                   "desc", "toplevel"])
119

  
120
    return info(branch=branch.name, revid=revid, revno=revno, desc=desc,
121
                toplevel=toplevel)
122

  
123

  
124
def base_version(vcs_info):
125
    """Determine the base version from a file in the repository"""
126

  
127
    f = open(os.path.join(vcs_info.toplevel, BASE_VERSION_FILE))
128
    lines = [l.strip() for l in f.readlines()]
129
    l = [l for l in lines if not l.startswith("#")]
130
    if len(l) != 1:
131
        raise ValueError("File '%s' should contain a single non-comment line.")
132
    return l[0]
133

  
134

  
135
def build_mode():
136
    """Determine the build mode from the value of $GITFLOW_BUILD_MODE"""
137
    try:
138
        mode = os.environ["GITFLOW_BUILD_MODE"]
139
        assert mode == "release" or mode == "snapshot"
140
    except (KeyError, AssertionError):
141
        raise ValueError("GITFLOW_BUILD_MODE environment variable must be "
142
                         "'release' or 'snapshot'")
143
    return mode
144

  
145

  
146
def python_version(base_version, vcs_info, mode):
147
    """Generate a Python distribution version following devtools conventions.
148

  
149
    This helper generates a Python distribution version from a repository
150
    commit, following devtools conventions. The input data are:
151
        * base_version: a base version number, presumably stored in text file
152
          inside the repository, e.g., /version
153
        * vcs_info: vcs information: current branch name and revision no
154
        * mode: "snapshot", or "release"
155

  
156
    This helper assumes a git branching model following:
157
    http://nvie.com/posts/a-successful-git-branching-model/
158

  
159
    with 'master', 'develop', 'release-X', 'hotfix-X' and 'feature-X' branches.
160

  
161
    General rules:
162
    a) any repository commit can get as a Python version
163
    b) a version is generated either in 'release' or in 'snapshot' mode
164
    c) the choice of mode depends on the branch, see following table.
165

  
166
    A python version is of the form A_NNN,
167
    where A: X.Y.Z{,next,rcW} and NNN: a revision number for the commit,
168
    as returned by vcs_info().
169

  
170
    For every combination of branch and mode, releases are numbered as follows:
171

  
172
    BRANCH:  /  MODE: snapshot        release
173
    --------          ------------------------------
174
    feature           0.14next_150    N/A
175
    develop           0.14next_151    N/A
176
    release           0.14rc2_249     0.14rc2
177
    master            N/A             0.14
178
    hotfix            0.14.1rc6_121   0.14.1rc6
179
                      N/A             0.14.1
180

  
181
    The suffix 'next' in a version name is used to denote the upcoming version,
182
    the one being under development in the develop and release branches.
183
    Version '0.14next' is the version following 0.14, and only lives on the
184
    develop and feature branches.
185

  
186
    The suffix 'rc' is used to denote release candidates. 'rc' versions live
187
    only in release and hotfix branches.
188

  
189
    Suffixes 'next' and 'rc' have been chosen to ensure proper ordering
190
    according to setuptools rules:
191

  
192
        http://www.python.org/dev/peps/pep-0386/#setuptools
193

  
194
    Every branch uses a value for A so that all releases are ordered based
195
    on the branch they came from, so:
196

  
197
    So
198
        0.13next < 0.14rcW < 0.14 < 0.14next < 0.14.1
199

  
200
    and
201

  
202
    >>> V("0.14next") > V("0.14")
203
    True
204
    >>> V("0.14next") > V("0.14rc7")
205
    True
206
    >>> V("0.14next") > V("0.14.1")
207
    False
208
    >>> V("0.14rc6") > V("0.14")
209
    False
210
    >>> V("0.14.2rc6") > V("0.14.1")
211
    True
212

  
213
    The value for _NNN is chosen based of the revision number of the specific
214
    commit. It is used to ensure ascending ordering of consecutive releases
215
    from the same branch. Every version of the form A_NNN comes *before*
216
    than A: All snapshots are ordered so they come before the corresponding
217
    release.
218

  
219
    So
220
        0.14next_* < 0.14
221
        0.14.1_* < 0.14.1
222
        etc
223

  
224
    and
225

  
226
    >>> V("0.14next_150") < V("0.14next")
227
    True
228
    >>> V("0.14.1next_150") < V("0.14.1next")
229
    True
230
    >>> V("0.14.1_149") < V("0.14.1")
231
    True
232
    >>> V("0.14.1_149") < V("0.14.1_150")
233
    True
234

  
235
    Combining both of the above, we get
236
       0.13next_* < 0.13next < 0.14rcW_* < 0.14rcW < 0.14_* < 0.14
237
       < 0.14next_* < 0.14next < 0.14.1_* < 0.14.1
238

  
239
    and
240

  
241
    >>> V("0.13next_102") < V("0.13next")
242
    True
243
    >>> V("0.13next") < V("0.14rc5_120")
244
    True
245
    >>> V("0.14rc3_120") < V("0.14rc3")
246
    True
247
    >>> V("0.14rc3") < V("0.14_1")
248
    True
249
    >>> V("0.14_120") < V("0.14")
250
    True
251
    >>> V("0.14") < V("0.14next_20")
252
    True
253
    >>> V("0.14next_20") < V("0.14next")
254
    True
255

  
256
    Note: one of the tests above fails because of constraints in the way
257
    setuptools parses version numbers. It does not affect us because the
258
    specific version format that triggers the problem is not contained in the
259
    table showing allowed branch / mode combinations, above.
260

  
261

  
262
    """
263

  
264
    branch = vcs_info.branch
265

  
266
    # If it's a debian branch, ignore starting "debian-"
267
    brnorm = branch
268
    if brnorm == "debian":
269
        brnorm = "debian-master"
270
    if brnorm.startswith("debian-"):
271
        brnorm = brnorm.replace("debian-", "", 1)
272

  
273
    # Sanity checks
274
    if "-" in brnorm:
275
        btypestr = brnorm.split("-")[0]
276
    else:
277
        btypestr = brnorm
278

  
279
    try:
280
        btype = BRANCH_TYPES[btypestr]
281
    except KeyError:
282
        allowed_branches = ", ".join(x for x in BRANCH_TYPES.keys())
283
        raise ValueError("Malformed branch name '%s', cannot classify as one "
284
                         "of %s" % (btypestr, allowed_branches))
285

  
286
    if btype.versioned:
287
        try:
288
            bverstr = brnorm.split("-")[1]
289
        except IndexError:
290
            # No version
291
            raise ValueError("Branch name '%s' should contain version" %
292
                             branch)
293

  
294
        # Check that version is well-formed
295
        if not re.match(VERSION_RE, bverstr):
296
            raise ValueError("Malformed version '%s' in branch name '%s'" %
297
                             (bverstr, branch))
298

  
299
    m = re.match(btype.allowed_version_re, base_version)
300
    if not m or (btype.versioned and m.groupdict()["bverstr"] != bverstr):
301
        raise ValueError("Base version '%s' unsuitable for branch name '%s'" %
302
                         (base_version, branch))
303

  
304
    if mode not in ["snapshot", "release"]:
305
        raise ValueError("Specified mode '%s' should be one of 'snapshot' or "
306
                         "'release'" % mode)
307
    snap = (mode == "snapshot")
308

  
309
    if ((snap and not btype.builds_snapshot) or
310
        (not snap and not btype.builds_release)):
311
        raise ValueError("Invalid mode '%s' in branch type '%s'" %
312
                         (mode, btypestr))
313

  
314
    if snap:
315
        v = "%s_%d_%s" % (base_version, vcs_info.revno, vcs_info.revid)
316
    else:
317
        v = base_version
318
    return v
319

  
320

  
321
def debian_version_from_python_version(pyver):
322
    """Generate a debian package version from a Python version.
323

  
324
    This helper generates a Debian package version from a Python version,
325
    following devtools conventions.
326

  
327
    Debian sorts version strings differently compared to setuptools:
328
    http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version
329

  
330
    Initial tests:
331

  
332
    >>> debian_version("3") < debian_version("6")
333
    True
334
    >>> debian_version("3") < debian_version("2")
335
    False
336
    >>> debian_version("1") == debian_version("1")
337
    True
338
    >>> debian_version("1") != debian_version("1")
339
    False
340
    >>> debian_version("1") >= debian_version("1")
341
    True
342
    >>> debian_version("1") <= debian_version("1")
343
    True
344

  
345
    This helper defines a 1-1 mapping between Python and Debian versions,
346
    with the same ordering.
347

  
348
    Debian versions are ordered in the same way as Python versions:
349

  
350
    >>> D("0.14next") > D("0.14")
351
    True
352
    >>> D("0.14next") > D("0.14rc7")
353
    True
354
    >>> D("0.14next") > D("0.14.1")
355
    False
356
    >>> D("0.14rc6") > D("0.14")
357
    False
358
    >>> D("0.14.2rc6") > D("0.14.1")
359
    True
360

  
361
    and
362

  
363
    >>> D("0.14next_150") < D("0.14next")
364
    True
365
    >>> D("0.14.1next_150") < D("0.14.1next")
366
    True
367
    >>> D("0.14.1_149") < D("0.14.1")
368
    True
369
    >>> D("0.14.1_149") < D("0.14.1_150")
370
    True
371

  
372
    and
373

  
374
    >>> D("0.13next_102") < D("0.13next")
375
    True
376
    >>> D("0.13next") < D("0.14rc5_120")
377
    True
378
    >>> D("0.14rc3_120") < D("0.14rc3")
379
    True
380
    >>> D("0.14rc3") < D("0.14_1")
381
    True
382
    >>> D("0.14_120") < D("0.14")
383
    True
384
    >>> D("0.14") < D("0.14next_20")
385
    True
386
    >>> D("0.14next_20") < D("0.14next")
387
    True
388

  
389
    """
390
    return pyver.replace("_", "~").replace("rc", "~rc") + "-1"
391

  
392

  
393
def debian_version(base_version, vcs_info, mode):
394
    p = python_version(base_version, vcs_info, mode)
395
    return debian_version_from_python_version(p)
396

  
397

  
398
def user_info():
399
    import getpass
400
    import socket
401
    return "%s@%s" % (getpass.getuser(), socket.getfqdn())
402

  
403

  
404
def update_version(module, name="version", root="."):
405
    """
406
    Generate or replace version.py as a submodule of `module`.
407

  
408
    This is a helper to generate/replace a version.py file containing version
409
    information as a submodule of passed `module`.
410

  
411
    """
412

  
413
    v = vcs_info()
414
    if not v:
415
        # Return early if not in development environment
416
        return
417
    b = base_version(v)
418
    mode = build_mode()
419
    paths = [root] + module.split(".") + ["%s.py" % name]
420
    module_filename = os.path.join(*paths)
421
    version = python_version(b, v, mode)
422
    content = """
423
__version__ = "%(version)s"
424
__version_info__ = %(version_info)s
425
__version_vcs_info__ = %(vcs_info)s
426
__version_user_info__ = "%(user_info)s"
427
    """ % dict(version=version, version_info=version.split("."),
428
               vcs_info=pprint.PrettyPrinter().pformat(dict(v._asdict())),
429
               user_info=user_info())
430

  
431
    module_file = file(module_filename, "w+")
432
    module_file.write(content)
433
    module_file.close()
434

  
435

  
436
if __name__ == "__main__":
437
    v = vcs_info()
438
    b = base_version(v)
439
    mode = build_mode()
440

  
441
    try:
442
        arg = sys.argv[1]
443
        assert arg == "python" or arg == "debian"
444
    except IndexError:
445
        raise ValueError("A single argument, 'python' or 'debian is required")
446

  
447
    if arg == "python":
448
        print python_version(b, v, mode)
449
    elif arg == "debian":
450
        print debian_version(b, v, mode)

Also available in: Unified diff