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

Christos Stavrakakis's avatar
Christos Stavrakakis committed
36
37
38
39
40
41
42
"""Helper functions for automatic version computation.

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

"""
Vangelis Koukis's avatar
Vangelis Koukis committed
43
44
45
46
47

import os
import re
import sys

Chris Stavrakakis's avatar
Chris Stavrakakis committed
48
from distutils import log  # pylint: disable=E0611
Vangelis Koukis's avatar
Vangelis Koukis committed
49

Christos Stavrakakis's avatar
Christos Stavrakakis committed
50
from devflow import BRANCH_TYPES, BASE_VERSION_FILE, VERSION_RE
51
from devflow import utils
Vangelis Koukis's avatar
Vangelis Koukis committed
52
53


Christos Stavrakakis's avatar
Christos Stavrakakis committed
54
def get_base_version(vcs_info):
Vangelis Koukis's avatar
Vangelis Koukis committed
55
56
57
58
    """Determine the base version from a file in the repository"""

    f = open(os.path.join(vcs_info.toplevel, BASE_VERSION_FILE))
    lines = [l.strip() for l in f.readlines()]
Christos Stavrakakis's avatar
Christos Stavrakakis committed
59
60
    lines = [l for l in lines if not l.startswith("#")]
    if len(lines) != 1:
Vangelis Koukis's avatar
Vangelis Koukis committed
61
        raise ValueError("File '%s' should contain a single non-comment line.")
62
    f.close()
Christos Stavrakakis's avatar
Christos Stavrakakis committed
63
    return lines[0]
Vangelis Koukis's avatar
Vangelis Koukis committed
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185


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

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

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

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

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

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

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

    BRANCH:  /  MODE: snapshot        release
    --------          ------------------------------
    feature           0.14next_150    N/A
    develop           0.14next_151    N/A
    release           0.14rc2_249     0.14rc2
    master            N/A             0.14
    hotfix            0.14.1rc6_121   0.14.1rc6
                      N/A             0.14.1

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

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

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

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

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

    So
        0.13next < 0.14rcW < 0.14 < 0.14next < 0.14.1

    and

    >>> V("0.14next") > V("0.14")
    True
    >>> V("0.14next") > V("0.14rc7")
    True
    >>> V("0.14next") > V("0.14.1")
    False
    >>> V("0.14rc6") > V("0.14")
    False
    >>> V("0.14.2rc6") > V("0.14.1")
    True

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

    So
        0.14next_* < 0.14
        0.14.1_* < 0.14.1
        etc

    and

    >>> V("0.14next_150") < V("0.14next")
    True
    >>> V("0.14.1next_150") < V("0.14.1next")
    True
    >>> V("0.14.1_149") < V("0.14.1")
    True
    >>> V("0.14.1_149") < V("0.14.1_150")
    True

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

    and

    >>> V("0.13next_102") < V("0.13next")
    True
    >>> V("0.13next") < V("0.14rc5_120")
    True
    >>> V("0.14rc3_120") < V("0.14rc3")
    True
    >>> V("0.14rc3") < V("0.14_1")
    True
    >>> V("0.14_120") < V("0.14")
    True
    >>> V("0.14") < V("0.14next_20")
    True
    >>> V("0.14next_20") < V("0.14next")
    True

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


    """

    branch = vcs_info.branch

Christos Stavrakakis's avatar
Fix bug    
Christos Stavrakakis committed
186
187
    brnorm = utils.normalize_branch_name(branch)
    btypestr = utils.get_branch_type(branch)
Vangelis Koukis's avatar
Vangelis Koukis committed
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219

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

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

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

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

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

    if ((snap and not btype.builds_snapshot) or
Christos Stavrakakis's avatar
Christos Stavrakakis committed
220
        (not snap and not btype.builds_release)):  # nopep8
Christos Stavrakakis's avatar
Christos Stavrakakis committed
221
222
        raise ValueError("Invalid mode '%s' in branch type '%s'" %
                         (mode, btypestr))
Vangelis Koukis's avatar
Vangelis Koukis committed
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299

    if snap:
        v = "%s_%d_%s" % (base_version, vcs_info.revno, vcs_info.revid)
    else:
        v = base_version
    return v


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

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

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

    Initial tests:

    >>> debian_version("3") < debian_version("6")
    True
    >>> debian_version("3") < debian_version("2")
    False
    >>> debian_version("1") == debian_version("1")
    True
    >>> debian_version("1") != debian_version("1")
    False
    >>> debian_version("1") >= debian_version("1")
    True
    >>> debian_version("1") <= debian_version("1")
    True

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

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

    >>> D("0.14next") > D("0.14")
    True
    >>> D("0.14next") > D("0.14rc7")
    True
    >>> D("0.14next") > D("0.14.1")
    False
    >>> D("0.14rc6") > D("0.14")
    False
    >>> D("0.14.2rc6") > D("0.14.1")
    True

    and

    >>> D("0.14next_150") < D("0.14next")
    True
    >>> D("0.14.1next_150") < D("0.14.1next")
    True
    >>> D("0.14.1_149") < D("0.14.1")
    True
    >>> D("0.14.1_149") < D("0.14.1_150")
    True

    and

    >>> D("0.13next_102") < D("0.13next")
    True
    >>> D("0.13next") < D("0.14rc5_120")
    True
    >>> D("0.14rc3_120") < D("0.14rc3")
    True
    >>> D("0.14rc3") < D("0.14_1")
    True
    >>> D("0.14_120") < D("0.14")
    True
    >>> D("0.14") < D("0.14next_20")
    True
    >>> D("0.14next_20") < D("0.14next")
    True

    """
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
310
311
    repo = utils.get_repository()
    minor = 1
    while True:
312
        tag = "debian/" + version_tag + "-" + str(minor) + codename
313
314
315
316
        if tag in repo.tags:
            minor += 1
        else:
            return minor
Vangelis Koukis's avatar
Vangelis Koukis committed
317
318


Christos Stavrakakis's avatar
Christos Stavrakakis committed
319
def get_python_version():
320
    v = utils.get_vcs_info()
Christos Stavrakakis's avatar
Christos Stavrakakis committed
321
    b = get_base_version(v)
322
    mode = utils.get_build_mode()
Christos Stavrakakis's avatar
Christos Stavrakakis committed
323
324
325
    return python_version(b, v, mode)


Vangelis Koukis's avatar
Vangelis Koukis committed
326
327
328
329
330
def debian_version(base_version, vcs_info, mode):
    p = python_version(base_version, vcs_info, mode)
    return debian_version_from_python_version(p)


Christos Stavrakakis's avatar
Christos Stavrakakis committed
331
def get_debian_version():
332
    v = utils.get_vcs_info()
Christos Stavrakakis's avatar
Christos Stavrakakis committed
333
    b = get_base_version(v)
334
    mode = utils.get_build_mode()
Christos Stavrakakis's avatar
Christos Stavrakakis committed
335
336
337
    return debian_version(b, v, mode)


338
def update_version():
Christos Stavrakakis's avatar
Christos Stavrakakis committed
339
340
341
    """Generate or replace version files

    Helper function for generating/replacing version files containing version
342
    information.
Vangelis Koukis's avatar
Vangelis Koukis committed
343
344
345

    """

346
    v = utils.get_vcs_info()
347
    toplevel = v.toplevel
Christos Stavrakakis's avatar
Christos Stavrakakis committed
348

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

Christos Stavrakakis's avatar
Christos Stavrakakis committed
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
379
            version_file.write(content)
            version_file.close()
Vangelis Koukis's avatar
Vangelis Koukis committed
380
381


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


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

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

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

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

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

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

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


Christos Stavrakakis's avatar
Christos Stavrakakis committed
424
def main():
425
    v = utils.get_vcs_info()
Christos Stavrakakis's avatar
Christos Stavrakakis committed
426
    b = get_base_version(v)
427
    mode = utils.get_build_mode()
Vangelis Koukis's avatar
Vangelis Koukis committed
428
429
430
431
432
433
434
435
436
437
438

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

    if arg == "python":
        print python_version(b, v, mode)
    elif arg == "debian":
        print debian_version(b, v, mode)
Christos Stavrakakis's avatar
Christos Stavrakakis committed
439
440
441

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