Statistics
| Branch: | Tag: | Revision:

root / devflow / versioning.py @ 42868817

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
import pprint
48

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

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

    
54

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

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

    
66

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

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

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

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

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

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

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

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

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

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

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

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

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

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

121
    and
122

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

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

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

145
    and
146

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

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

160
    and
161

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

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

182

183
    """
184

    
185
    branch = vcs_info.branch
186

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

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

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

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

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

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

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

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

    
231

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

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

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

241
    Initial tests:
242

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

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

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

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

272
    and
273

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

283
    and
284

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

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

    
306

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

    
319

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

    
326

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

    
331

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

    
338

    
339
def user_info():
340
    import getpass
341
    import socket
342
    return "%s@%s" % (getpass.getuser(), socket.getfqdn())
343

    
344

    
345
def update_version():
346
    """Generate or replace version files
347

348
    Helper function for generating/replacing version files containing version
349
    information.
350

351
    """
352

    
353
    v = utils.get_vcs_info()
354
    toplevel = v.toplevel
355

    
356
    config = utils.get_config()
357
    if not v:
358
        # Return early if not in development environment
359
        raise RuntimeError("Can not compute version outside of a git"
360
                           " repository.")
361
    b = get_base_version(v)
362
    mode = utils.get_build_mode()
363
    version = python_version(b, v, mode)
364
    vcs_info_dict = dict(v._asdict())  # pylint: disable=W0212
365
    content = """__version__ = "%(version)s"
366
__version_info__ = %(version_info)s
367
__version_vcs_info__ = %(vcs_info)s
368
__version_user_info__ = "%(user_info)s"
369
""" % dict(version=version, version_info=version.split("."),
370
           vcs_info=pprint.PrettyPrinter().pformat(vcs_info_dict),
371
           user_info=user_info())
372

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

    
382

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

    
391

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

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

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

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

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

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

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

    
424

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

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

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

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