Statistics
| Branch: | Tag: | Revision:

root / devflow / versioning.py @ 6b88d711

History | View | Annotate | Download (13.5 kB)

1
#!/usr/bin/env python
2
#
3
# Copyright (C) 2012, 2013 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
"""Helper functions for automatic version computation.
37

38
This module contains helper functions for extracting information
39
from a Git repository, and computing the python and debian version
40
of the repository code.
41

42
"""
43

    
44
import os
45
import re
46
import sys
47

    
48
from distutils import log  # pylint: disable=E0611
49

    
50
from devflow import BRANCH_TYPES, BASE_VERSION_FILE, VERSION_RE
51
from devflow import utils
52

    
53

    
54
def get_base_version(vcs_info):
55
    """Determine the base version from a file in the repository"""
56

    
57
    f = open(os.path.join(vcs_info.toplevel, BASE_VERSION_FILE))
58
    lines = [l.strip() for l in f.readlines()]
59
    lines = [l for l in lines if not l.startswith("#")]
60
    if len(lines) != 1:
61
        raise ValueError("File '%s' should contain a single non-comment line.")
62
    f.close()
63
    return lines[0]
64

    
65

    
66
def python_version(base_version, vcs_info, mode):
67
    """Generate a Python distribution version following devtools conventions.
68

69
    This helper generates a Python distribution version from a repository
70
    commit, following devtools conventions. The input data are:
71
        * base_version: a base version number, presumably stored in text file
72
          inside the repository, e.g., /version
73
        * vcs_info: vcs information: current branch name and revision no
74
        * mode: "snapshot", or "release"
75

76
    This helper assumes a git branching model following:
77
    http://nvie.com/posts/a-successful-git-branching-model/
78

79
    with 'master', 'develop', 'release-X', 'hotfix-X' and 'feature-X' branches.
80

81
    General rules:
82
    a) any repository commit can get as a Python version
83
    b) a version is generated either in 'release' or in 'snapshot' mode
84
    c) the choice of mode depends on the branch, see following table.
85

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

90
    For every combination of branch and mode, releases are numbered as follows:
91

92
    BRANCH:  /  MODE: snapshot        release
93
    --------          ------------------------------
94
    feature           0.14next_150    N/A
95
    develop           0.14next_151    N/A
96
    release           0.14rc2_249     0.14rc2
97
    master            N/A             0.14
98
    hotfix            0.14.1rc6_121   0.14.1rc6
99
                      N/A             0.14.1
100

101
    The suffix 'next' in a version name is used to denote the upcoming version,
102
    the one being under development in the develop and release branches.
103
    Version '0.14next' is the version following 0.14, and only lives on the
104
    develop and feature branches.
105

106
    The suffix 'rc' is used to denote release candidates. 'rc' versions live
107
    only in release and hotfix branches.
108

109
    Suffixes 'next' and 'rc' have been chosen to ensure proper ordering
110
    according to setuptools rules:
111

112
        http://www.python.org/dev/peps/pep-0386/#setuptools
113

114
    Every branch uses a value for A so that all releases are ordered based
115
    on the branch they came from, so:
116

117
    So
118
        0.13next < 0.14rcW < 0.14 < 0.14next < 0.14.1
119

120
    and
121

122
    >>> V("0.14next") > V("0.14")
123
    True
124
    >>> V("0.14next") > V("0.14rc7")
125
    True
126
    >>> V("0.14next") > V("0.14.1")
127
    False
128
    >>> V("0.14rc6") > V("0.14")
129
    False
130
    >>> V("0.14.2rc6") > V("0.14.1")
131
    True
132

133
    The value for _NNN is chosen based of the revision number of the specific
134
    commit. It is used to ensure ascending ordering of consecutive releases
135
    from the same branch. Every version of the form A_NNN comes *before*
136
    than A: All snapshots are ordered so they come before the corresponding
137
    release.
138

139
    So
140
        0.14next_* < 0.14
141
        0.14.1_* < 0.14.1
142
        etc
143

144
    and
145

146
    >>> V("0.14next_150") < V("0.14next")
147
    True
148
    >>> V("0.14.1next_150") < V("0.14.1next")
149
    True
150
    >>> V("0.14.1_149") < V("0.14.1")
151
    True
152
    >>> V("0.14.1_149") < V("0.14.1_150")
153
    True
154

155
    Combining both of the above, we get
156
       0.13next_* < 0.13next < 0.14rcW_* < 0.14rcW < 0.14_* < 0.14
157
       < 0.14next_* < 0.14next < 0.14.1_* < 0.14.1
158

159
    and
160

161
    >>> V("0.13next_102") < V("0.13next")
162
    True
163
    >>> V("0.13next") < V("0.14rc5_120")
164
    True
165
    >>> V("0.14rc3_120") < V("0.14rc3")
166
    True
167
    >>> V("0.14rc3") < V("0.14_1")
168
    True
169
    >>> V("0.14_120") < V("0.14")
170
    True
171
    >>> V("0.14") < V("0.14next_20")
172
    True
173
    >>> V("0.14next_20") < V("0.14next")
174
    True
175

176
    Note: one of the tests above fails because of constraints in the way
177
    setuptools parses version numbers. It does not affect us because the
178
    specific version format that triggers the problem is not contained in the
179
    table showing allowed branch / mode combinations, above.
180

181

182
    """
183

    
184
    branch = vcs_info.branch
185

    
186
    brnorm = utils.normalize_branch_name(branch)
187
    btypestr = utils.get_branch_type(branch)
188

    
189
    try:
190
        btype = BRANCH_TYPES[btypestr]
191
    except KeyError:
192
        allowed_branches = ", ".join(x for x in BRANCH_TYPES.keys())
193
        raise ValueError("Malformed branch name '%s', cannot classify as one "
194
                         "of %s" % (btypestr, allowed_branches))
195

    
196
    if btype.versioned:
197
        try:
198
            bverstr = brnorm.split("-")[1]
199
        except IndexError:
200
            # No version
201
            raise ValueError("Branch name '%s' should contain version" %
202
                             branch)
203

    
204
        # Check that version is well-formed
205
        if not re.match(VERSION_RE, bverstr):
206
            raise ValueError("Malformed version '%s' in branch name '%s'" %
207
                             (bverstr, branch))
208

    
209
    m = re.match(btype.allowed_version_re, base_version)
210
    if not m or (btype.versioned and m.groupdict()["bverstr"] != bverstr):
211
        raise ValueError("Base version '%s' unsuitable for branch name '%s'" %
212
                         (base_version, branch))
213

    
214
    if mode not in ["snapshot", "release"]:
215
        raise ValueError("Specified mode '%s' should be one of 'snapshot' or "
216
                         "'release'" % mode)
217
    snap = (mode == "snapshot")
218

    
219
    if ((snap and not btype.builds_snapshot) or
220
        (not snap and not btype.builds_release)):  # nopep8
221
        raise ValueError("Invalid mode '%s' in branch type '%s'" %
222
                         (mode, btypestr))
223

    
224
    if snap:
225
        v = "%s_%d_%s" % (base_version, vcs_info.revno, vcs_info.revid)
226
    else:
227
        v = base_version
228
    return v
229

    
230

    
231
def debian_version_from_python_version(pyver):
232
    """Generate a debian package version from a Python version.
233

234
    This helper generates a Debian package version from a Python version,
235
    following devtools conventions.
236

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

240
    Initial tests:
241

242
    >>> debian_version("3") < debian_version("6")
243
    True
244
    >>> debian_version("3") < debian_version("2")
245
    False
246
    >>> debian_version("1") == debian_version("1")
247
    True
248
    >>> debian_version("1") != debian_version("1")
249
    False
250
    >>> debian_version("1") >= debian_version("1")
251
    True
252
    >>> debian_version("1") <= debian_version("1")
253
    True
254

255
    This helper defines a 1-1 mapping between Python and Debian versions,
256
    with the same ordering.
257

258
    Debian versions are ordered in the same way as Python versions:
259

260
    >>> D("0.14next") > D("0.14")
261
    True
262
    >>> D("0.14next") > D("0.14rc7")
263
    True
264
    >>> D("0.14next") > D("0.14.1")
265
    False
266
    >>> D("0.14rc6") > D("0.14")
267
    False
268
    >>> D("0.14.2rc6") > D("0.14.1")
269
    True
270

271
    and
272

273
    >>> D("0.14next_150") < D("0.14next")
274
    True
275
    >>> D("0.14.1next_150") < D("0.14.1next")
276
    True
277
    >>> D("0.14.1_149") < D("0.14.1")
278
    True
279
    >>> D("0.14.1_149") < D("0.14.1_150")
280
    True
281

282
    and
283

284
    >>> D("0.13next_102") < D("0.13next")
285
    True
286
    >>> D("0.13next") < D("0.14rc5_120")
287
    True
288
    >>> D("0.14rc3_120") < D("0.14rc3")
289
    True
290
    >>> D("0.14rc3") < D("0.14_1")
291
    True
292
    >>> D("0.14_120") < D("0.14")
293
    True
294
    >>> D("0.14") < D("0.14next_20")
295
    True
296
    >>> D("0.14next_20") < D("0.14next")
297
    True
298

299
    """
300
    version = pyver.replace("_", "~").replace("rc", "~rc")
301
    codename = utils.get_distribution_codename()
302
    minor = str(get_revision(version, codename))
303
    return version + "-" + minor + "~" + codename
304

    
305

    
306
def get_revision(version, codename):
307
    """Find revision for a debian version"""
308
    version_tag = utils.version_to_tag(version)
309
    repo = utils.get_repository()
310
    minor = 1
311
    while True:
312
        tag = "debian/" + version_tag + "-" + str(minor) + codename
313
        if tag in repo.tags:
314
            minor += 1
315
        else:
316
            return minor
317

    
318

    
319
def get_python_version():
320
    v = utils.get_vcs_info()
321
    b = get_base_version(v)
322
    mode = utils.get_build_mode()
323
    return python_version(b, v, mode)
324

    
325

    
326
def debian_version(base_version, vcs_info, mode):
327
    p = python_version(base_version, vcs_info, mode)
328
    return debian_version_from_python_version(p)
329

    
330

    
331
def get_debian_version():
332
    v = utils.get_vcs_info()
333
    b = get_base_version(v)
334
    mode = utils.get_build_mode()
335
    return debian_version(b, v, mode)
336

    
337

    
338
def update_version():
339
    """Generate or replace version files
340

341
    Helper function for generating/replacing version files containing version
342
    information.
343

344
    """
345

    
346
    v = utils.get_vcs_info()
347
    toplevel = v.toplevel
348

    
349
    config = utils.get_config()
350
    if not v:
351
        # Return early if not in development environment
352
        raise RuntimeError("Can not compute version outside of a git"
353
                           " repository.")
354
    b = get_base_version(v)
355
    mode = utils.get_build_mode()
356
    version = python_version(b, v, mode)
357
    vcs_info = """{
358
    'branch': '%s',
359
    'revid': '%s',
360
    'revno': %s}""" % (v.branch, v.revid, v.revno)
361
    content =\
362
"""__version__ = "%(version)s"
363
__version_info__ = %(version_info)s
364
__version_vcs_info__ = %(vcs_info)s
365
__version_user_email__ = "%(user_email)s"
366
__version_user_name__ = "%(user_name)s"
367
""" % dict(version=version, version_info=version.split("."),
368
           vcs_info=vcs_info,
369
           user_email=v.email,
370
           user_name=v.name)
371

    
372
    for _pkg_name, pkg_info in config['packages'].items():
373
        version_filename = pkg_info['version_file']
374
        if version_filename:
375
            path = os.path.join(toplevel, version_filename)
376
            log.info("Updating version file '%s'" % version_filename)
377
            version_file = file(path, "w+")
378
            version_file.write(content)
379
            version_file.close()
380

    
381

    
382
def bump_version_main():
383
    try:
384
        version = sys.argv[1]
385
        bump_version(version)
386
    except IndexError:
387
        sys.stdout.write("Give me a version %s!\n")
388
        sys.stdout.write("usage: %s version\n" % sys.argv[0])
389

    
390

    
391
def bump_version(new_version):
392
    """Set new base version to base version file and commit"""
393
    v = utils.get_vcs_info()
394
    mode = utils.get_build_mode()
395

    
396
    # Check that new base version is valid
397
    python_version(new_version, v, mode)
398

    
399
    repo = utils.get_repository()
400
    toplevel = repo.working_dir
401

    
402
    old_version = get_base_version(v)
403
    sys.stdout.write("Current base version is '%s'\n" % old_version)
404

    
405
    version_file = os.path.join(toplevel, "version")
406
    sys.stdout.write("Updating version file %s from version '%s' to '%s'\n"
407
                     % (version_file, old_version, new_version))
408

    
409
    f = open(version_file, 'rw+')
410
    lines = f.readlines()
411
    for i in range(0, len(lines)):
412
        if not lines[i].startswith("#"):
413
            lines[i] = lines[i].replace(old_version, new_version)
414
    f.seek(0)
415
    f.truncate(0)
416
    f.writelines(lines)
417
    f.close()
418

    
419
    repo.git.add(version_file)
420
    repo.git.commit(m="Bump version to %s" % new_version)
421
    sys.stdout.write("Update version file and commited\n")
422

    
423

    
424
def main():
425
    v = utils.get_vcs_info()
426
    b = get_base_version(v)
427
    mode = utils.get_build_mode()
428

    
429
    try:
430
        arg = sys.argv[1]
431
        assert arg == "python" or arg == "debian"
432
    except IndexError:
433
        raise ValueError("A single argument, 'python' or 'debian is required")
434

    
435
    if arg == "python":
436
        print python_version(b, v, mode)
437
    elif arg == "debian":
438
        print debian_version(b, v, mode)
439

    
440
if __name__ == "__main__":
441
    sys.exit(main())