Statistics
| Branch: | Tag: | Revision:

root / devflow / versioning.py @ 595d480a

History | View | Annotate | Download (14.3 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

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

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

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

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

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

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

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

103
    BRANCH:  /  MODE: snapshot        release
104
    --------          ------------------------------
105
    feature           0.14next_150    N/A
106
    develop           0.14next_151    N/A
107
    release           0.14rc2_249     0.14rc2
108
    master            N/A             0.14
109
    hotfix            0.14.1rc6_121   0.14.1rc6
110
                      N/A             0.14.1
111

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

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

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

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

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

128
    So
129
        0.13next < 0.14rcW < 0.14 < 0.14next < 0.14.1
130

131
    and
132

133
    >>> V("0.14next") > V("0.14")
134
    True
135
    >>> V("0.14next") > V("0.14rc7")
136
    True
137
    >>> V("0.14next") > V("0.14.1")
138
    False
139
    >>> V("0.14rc6") > V("0.14")
140
    False
141
    >>> V("0.14.2rc6") > V("0.14.1")
142
    True
143

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

150
    So
151
        0.14next_* < 0.14
152
        0.14.1_* < 0.14.1
153
        etc
154

155
    and
156

157
    >>> V("0.14next_150") < V("0.14next")
158
    True
159
    >>> V("0.14.1next_150") < V("0.14.1next")
160
    True
161
    >>> V("0.14.1_149") < V("0.14.1")
162
    True
163
    >>> V("0.14.1_149") < V("0.14.1_150")
164
    True
165

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

170
    and
171

172
    >>> V("0.13next_102") < V("0.13next")
173
    True
174
    >>> V("0.13next") < V("0.14rc5_120")
175
    True
176
    >>> V("0.14rc3_120") < V("0.14rc3")
177
    True
178
    >>> V("0.14rc3") < V("0.14_1")
179
    True
180
    >>> V("0.14_120") < V("0.14")
181
    True
182
    >>> V("0.14") < V("0.14next_20")
183
    True
184
    >>> V("0.14next_20") < V("0.14next")
185
    True
186

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

192

193
    """
194

    
195
    branch = vcs_info.branch
196

    
197
    brnorm = utils.normalize_branch_name(branch)
198
    btypestr = utils.get_branch_type(branch)
199

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

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

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

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

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

    
230
    if (snap and not btype.builds_snapshot) or\
231
       (not snap and not btype.builds_release):  # nopep8
232
        raise ValueError("Invalid mode '%s' in branch type '%s'" %
233
                         (mode, btypestr))
234

    
235
    if snap:
236
        v = "%s_%d_%s" % (base_version, vcs_info.revno, vcs_info.revid)
237
    else:
238
        v = base_version
239
    return v
240

    
241

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

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

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

251
    Initial tests:
252

253
    >>> debian_version("3") < debian_version("6")
254
    True
255
    >>> debian_version("3") < debian_version("2")
256
    False
257
    >>> debian_version("1") == debian_version("1")
258
    True
259
    >>> debian_version("1") != debian_version("1")
260
    False
261
    >>> debian_version("1") >= debian_version("1")
262
    True
263
    >>> debian_version("1") <= debian_version("1")
264
    True
265

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

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

271
    >>> D("0.14next") > D("0.14")
272
    True
273
    >>> D("0.14next") > D("0.14rc7")
274
    True
275
    >>> D("0.14next") > D("0.14.1")
276
    False
277
    >>> D("0.14rc6") > D("0.14")
278
    False
279
    >>> D("0.14.2rc6") > D("0.14.1")
280
    True
281

282
    and
283

284
    >>> D("0.14next_150") < D("0.14next")
285
    True
286
    >>> D("0.14.1next_150") < D("0.14.1next")
287
    True
288
    >>> D("0.14.1_149") < D("0.14.1")
289
    True
290
    >>> D("0.14.1_149") < D("0.14.1_150")
291
    True
292

293
    and
294

295
    >>> D("0.13next_102") < D("0.13next")
296
    True
297
    >>> D("0.13next") < D("0.14rc5_120")
298
    True
299
    >>> D("0.14rc3_120") < D("0.14rc3")
300
    True
301
    >>> D("0.14rc3") < D("0.14_1")
302
    True
303
    >>> D("0.14_120") < D("0.14")
304
    True
305
    >>> D("0.14") < D("0.14next_20")
306
    True
307
    >>> D("0.14next_20") < D("0.14next")
308
    True
309

310
    """
311
    version = pyver.replace("_", "~").replace("rc", "~rc")
312
    codename = utils.get_distribution_codename()
313
    minor = str(get_revision(version, codename))
314
    return version + "-" + minor + "~" + codename
315

    
316

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

    
329

    
330
def get_python_version():
331
    v = utils.get_vcs_info()
332
    b = get_base_version(v)
333
    mode = utils.get_build_mode()
334
    return python_version(b, v, mode)
335

    
336

    
337
def debian_version(base_version, vcs_info, mode):
338
    p = python_version(base_version, vcs_info, mode)
339
    return debian_version_from_python_version(p)
340

    
341

    
342
def get_debian_version():
343
    v = utils.get_vcs_info()
344
    b = get_base_version(v)
345
    mode = utils.get_build_mode()
346
    return debian_version(b, v, mode)
347

    
348

    
349
def update_version():
350
    """Generate or replace version files
351

352
    Helper function for generating/replacing version files containing version
353
    information.
354

355
    """
356

    
357
    v = utils.get_vcs_info()
358
    toplevel = v.toplevel
359

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

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

    
400

    
401
def bump_version_main():
402
    try:
403
        version = sys.argv[1]
404
        bump_version(version)
405
    except IndexError:
406
        sys.stdout.write("Give me a version %s!\n")
407
        sys.stdout.write("usage: %s version\n" % sys.argv[0])
408

    
409

    
410
def bump_version(new_version):
411
    """Set new base version to base version file and commit"""
412
    v = utils.get_vcs_info()
413
    mode = utils.get_build_mode()
414

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

    
418
    repo = utils.get_repository()
419
    toplevel = repo.working_dir
420

    
421
    old_version = get_base_version(v)
422
    sys.stdout.write("Current base version is '%s'\n" % old_version)
423

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

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

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

    
442

    
443
def main():
444
    v = utils.get_vcs_info()
445
    b = get_base_version(v)
446
    mode = utils.get_build_mode()
447

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

    
454
    if arg == "python":
455
        print python_version(b, v, mode)
456
    elif arg == "debian":
457
        print debian_version(b, v, mode)
458

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