Statistics
| Branch: | Tag: | Revision:

root / devflow / versioning.py @ 680fed02

History | View | Annotate | Download (14.6 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
DEFAULT_VERSION_FILE = """
55
__version__ = "%(DEVFLOW_VERSION)s"
56
__version_vcs_info__ = {
57
    'branch': '%(DEVFLOW_BRANCH)s',
58
    'revid': '%(DEVFLOW_REVISION_ID)s',
59
    'revno': %(DEVFLOW_REVISION_NUMBER)s}
60
__version_user_email__ = "%(DEVFLOW_USER_EMAIL)s"
61
__version_user_name__ = "%(DEVFLOW_USER_NAME)s"
62
"""
63

    
64

    
65
def get_base_version(vcs_info):
66
    """Determine the base version from a file in the repository"""
67

    
68
    f = open(os.path.join(vcs_info.toplevel, BASE_VERSION_FILE))
69
    lines = [l.strip() for l in f.readlines()]
70
    lines = [l for l in lines if not l.startswith("#")]
71
    if len(lines) != 1:
72
        raise ValueError("File '%s' should contain a single non-comment line.")
73
    f.close()
74
    return lines[0]
75

    
76
def validate_version(base_version, vcs_info):
77
    branch = vcs_info.branch
78

    
79
    brnorm = utils.normalize_branch_name(branch)
80
    btypestr = utils.get_branch_type(branch)
81

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

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

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

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

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

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

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

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

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

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

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

133
    BRANCH:  /  MODE: snapshot        release
134
    --------          ------------------------------
135
    feature           0.14next_150    N/A
136
    develop           0.14next_151    N/A
137
    release           0.14rc2_249     0.14rc2
138
    master            N/A             0.14
139
    hotfix            0.14.1rc6_121   0.14.1rc6
140
                      N/A             0.14.1
141

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

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

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

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

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

158
    So
159
        0.13next < 0.14rcW < 0.14 < 0.14next < 0.14.1
160

161
    and
162

163
    >>> V("0.14next") > V("0.14")
164
    True
165
    >>> V("0.14next") > V("0.14rc7")
166
    True
167
    >>> V("0.14next") > V("0.14.1")
168
    False
169
    >>> V("0.14rc6") > V("0.14")
170
    False
171
    >>> V("0.14.2rc6") > V("0.14.1")
172
    True
173

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

180
    So
181
        0.14next_* < 0.14
182
        0.14.1_* < 0.14.1
183
        etc
184

185
    and
186

187
    >>> V("0.14next_150") < V("0.14next")
188
    True
189
    >>> V("0.14.1next_150") < V("0.14.1next")
190
    True
191
    >>> V("0.14.1_149") < V("0.14.1")
192
    True
193
    >>> V("0.14.1_149") < V("0.14.1_150")
194
    True
195

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

200
    and
201

202
    >>> V("0.13next_102") < V("0.13next")
203
    True
204
    >>> V("0.13next") < V("0.14rc5_120")
205
    True
206
    >>> V("0.14rc3_120") < V("0.14rc3")
207
    True
208
    >>> V("0.14rc3") < V("0.14_1")
209
    True
210
    >>> V("0.14_120") < V("0.14")
211
    True
212
    >>> V("0.14") < V("0.14next_20")
213
    True
214
    >>> V("0.14next_20") < V("0.14next")
215
    True
216

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

222

223
    """
224
    validate_version(base_version, vcs_info)
225
    branch = vcs_info.branch
226
    btypestr = utils.get_branch_type(branch)
227
    #this cannot fail
228
    btype = BRANCH_TYPES[btypestr]
229

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

    
235
    if (snap and not btype.builds_snapshot) or\
236
       (not snap and not btype.builds_release):  # nopep8
237
        raise ValueError("Invalid mode '%s' in branch type '%s'" %
238
                         (mode, btypestr))
239

    
240
    if snap:
241
        v = "%s_%d_%s" % (base_version, vcs_info.revno, vcs_info.revid)
242
    else:
243
        v = base_version
244
    return v
245

    
246

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

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

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

256
    Initial tests:
257

258
    >>> debian_version("3") < debian_version("6")
259
    True
260
    >>> debian_version("3") < debian_version("2")
261
    False
262
    >>> debian_version("1") == debian_version("1")
263
    True
264
    >>> debian_version("1") != debian_version("1")
265
    False
266
    >>> debian_version("1") >= debian_version("1")
267
    True
268
    >>> debian_version("1") <= debian_version("1")
269
    True
270

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

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

276
    >>> D("0.14next") > D("0.14")
277
    True
278
    >>> D("0.14next") > D("0.14rc7")
279
    True
280
    >>> D("0.14next") > D("0.14.1")
281
    False
282
    >>> D("0.14rc6") > D("0.14")
283
    False
284
    >>> D("0.14.2rc6") > D("0.14.1")
285
    True
286

287
    and
288

289
    >>> D("0.14next_150") < D("0.14next")
290
    True
291
    >>> D("0.14.1next_150") < D("0.14.1next")
292
    True
293
    >>> D("0.14.1_149") < D("0.14.1")
294
    True
295
    >>> D("0.14.1_149") < D("0.14.1_150")
296
    True
297

298
    and
299

300
    >>> D("0.13next_102") < D("0.13next")
301
    True
302
    >>> D("0.13next") < D("0.14rc5_120")
303
    True
304
    >>> D("0.14rc3_120") < D("0.14rc3")
305
    True
306
    >>> D("0.14rc3") < D("0.14_1")
307
    True
308
    >>> D("0.14_120") < D("0.14")
309
    True
310
    >>> D("0.14") < D("0.14next_20")
311
    True
312
    >>> D("0.14next_20") < D("0.14next")
313
    True
314

315
    """
316
    version = pyver.replace("_", "~").replace("rc", "~rc")
317
    codename = utils.get_distribution_codename()
318
    minor = str(get_revision(version, codename))
319
    return version + "-" + minor + "~" + codename
320

    
321

    
322
def get_revision(version, codename):
323
    """Find revision for a debian version"""
324
    version_tag = utils.version_to_tag(version)
325
    repo = utils.get_repository()
326
    minor = 1
327
    while True:
328
        tag = "debian/" + version_tag + "-" + str(minor) + codename
329
        if tag in repo.tags:
330
            minor += 1
331
        else:
332
            return minor
333

    
334

    
335
def get_python_version():
336
    v = utils.get_vcs_info()
337
    b = get_base_version(v)
338
    mode = utils.get_build_mode()
339
    return python_version(b, v, mode)
340

    
341

    
342
def debian_version(base_version, vcs_info, mode):
343
    p = python_version(base_version, vcs_info, mode)
344
    return debian_version_from_python_version(p)
345

    
346

    
347
def get_debian_version():
348
    v = utils.get_vcs_info()
349
    b = get_base_version(v)
350
    mode = utils.get_build_mode()
351
    return debian_version(b, v, mode)
352

    
353

    
354
def update_version():
355
    """Generate or replace version files
356

357
    Helper function for generating/replacing version files containing version
358
    information.
359

360
    """
361

    
362
    v = utils.get_vcs_info()
363
    toplevel = v.toplevel
364

    
365
    config = utils.get_config()
366
    if not v:
367
        # Return early if not in development environment
368
        raise RuntimeError("Can not compute version outside of a git"
369
                           " repository.")
370
    b = get_base_version(v)
371
    mode = utils.get_build_mode()
372
    version = python_version(b, v, mode)
373
    debian_version_ = debian_version_from_python_version(version)
374
    env = {"DEVFLOW_VERSION": version,
375
           "DEVFLOW_DEBIAN_VERSION": debian_version_,
376
           "DEVFLOW_BRANCH": v.branch,
377
           "DEVFLOW_REVISION_ID": v.revid,
378
           "DEVFLOW_REVISION_NUMBER": v.revno,
379
           "DEVFLOW_USER_EMAIL": v.email,
380
           "DEVFLOW_USER_NAME": v.name}
381

    
382
    for _pkg_name, pkg_info in config['packages'].items():
383
        version_filename = pkg_info.get('version_file')
384
        if not version_filename:
385
            continue
386
        version_template = pkg_info.get('version_template')
387
        if version_template:
388
            vtemplate_file = os.path.join(toplevel, version_template)
389
            try:
390
                with file(vtemplate_file) as f:
391
                    content = f.read(-1) % env
392
            except IOError as e:
393
                if e.errno == 2:
394
                    raise RuntimeError("devflow.conf contains '%s' as a"
395
                                       " version template file, but file does"
396
                                       " not exists!" % vtemplate_file)
397
                else:
398
                    raise
399
        else:
400
            content = DEFAULT_VERSION_FILE % env
401
        with file(os.path.join(toplevel, version_filename), 'w+') as f:
402
            log.info("Updating version file '%s'" % version_filename)
403
            f.write(content)
404

    
405

    
406
def bump_version_main():
407
    try:
408
        version = sys.argv[1]
409
        bump_version(version)
410
    except IndexError:
411
        sys.stdout.write("Give me a version %s!\n")
412
        sys.stdout.write("usage: %s version\n" % sys.argv[0])
413

    
414

    
415
def _bump_version(new_version, v):
416
    repo = utils.get_repository()
417
    toplevel = repo.working_dir
418
    old_version = get_base_version(v)
419
    sys.stdout.write("Current base version is '%s'\n" % old_version)
420

    
421
    version_file = os.path.join(toplevel, "version")
422
    sys.stdout.write("Updating version file %s from version '%s' to '%s'\n"
423
                     % (version_file, old_version, new_version))
424

    
425
    f = open(version_file, 'rw+')
426
    lines = f.readlines()
427
    for i in range(0, len(lines)):
428
        if not lines[i].startswith("#"):
429
            lines[i] = lines[i].replace(old_version, new_version)
430
    f.seek(0)
431
    f.truncate(0)
432
    f.writelines(lines)
433
    f.close()
434

    
435
    repo.git.add(version_file)
436
    repo.git.commit(m="Bump version to %s" % new_version)
437
    sys.stdout.write("Update version file and commited\n")
438

    
439

    
440
def bump_version(new_version):
441
    """Set new base version to base version file and commit"""
442
    v = utils.get_vcs_info()
443

    
444
    # Check that new base version is valid
445
    validate_version(new_version, v)
446
    _bump_version(new_version, v)
447

    
448

    
449

    
450
def main():
451
    v = utils.get_vcs_info()
452
    b = get_base_version(v)
453
    mode = utils.get_build_mode()
454

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

    
461
    if arg == "python":
462
        print python_version(b, v, mode)
463
    elif arg == "debian":
464
        print debian_version(b, v, mode)
465

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