Completed
Push — master ( ba6c11...23b579 )
by Gonzalo
01:13
created

git_get_keywords()   F

Complexity

Conditions 9

Size

Total Lines 27

Duplication

Lines 25
Ratio 92.59 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 9
c 2
b 0
f 0
dl 25
loc 27
rs 3
1
# This file helps to compute a version number in source trees obtained from
2
# git-archive tarball (such as those provided by githubs download-from-tag
3
# feature). Distribution tarballs (built by setup.py sdist) and build
4
# directories (produced by setup.py build) will contain a much shorter file
5
# that just contains the computed version number.
6
7
# This file is released into the public domain. Generated by
8
# versioneer-0.18 (https://github.com/warner/python-versioneer)
9
"""Git implementation of _version.py."""
10
11
# Standard library imports
12
import errno
13
import os
14
import re
15
import subprocess
16
import sys
17
18
19
def get_keywords():
20
    """Get the keywords needed to look up the version information."""
21
    # these strings will be replaced by git during git-archive.
22
    # setup.py/versioneer.py will grep for the variable names, so they must
23
    # each be defined on a line of their own. _version.py will just call
24
    # get_keywords().
25
    git_refnames = "$Format:%d$"
26
    git_full = "$Format:%H$"
27
    git_date = "$Format:%ci$"
28
    keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
29
    return keywords
30
31
32
class VersioneerConfig:
33
    """Container for Versioneer configuration parameters."""
34
35
36
def get_config():
37
    """Create, populate and return the VersioneerConfig() object."""
38
    # these strings are filled in when 'setup.py versioneer' creates
39
    # _version.py
40
    cfg = VersioneerConfig()
41
    cfg.VCS = "git"
42
    cfg.style = "pep440"
43
    cfg.tag_prefix = "v"
44
    cfg.parentdir_prefix = "None"
45
    cfg.versionfile_source = "loghub/_version.py"
46
    cfg.verbose = False
47
    return cfg
48
49
50
class NotThisMethod(Exception):
51
    """Exception raised if a method is not valid for the current scenario."""
52
53
54
LONG_VERSION_PY = {}
55
HANDLERS = {}
56
57
58
def register_vcs_handler(vcs, method):  # decorator
59
    """Decorator to mark a method as the handler for a particular VCS."""
60
61
    def decorate(f):
62
        """Store f in HANDLERS[vcs][method]."""
63
        if vcs not in HANDLERS:
64
            HANDLERS[vcs] = {}
65
        HANDLERS[vcs][method] = f
66
        return f
67
68
    return decorate
69
70 View Code Duplication
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
71
def run_command(commands,
72
                args,
73
                cwd=None,
74
                verbose=False,
75
                hide_stderr=False,
76
                env=None):
77
    """Call the given command(s)."""
78
    assert isinstance(commands, list)
79
    p = None
80
    for c in commands:
81
        try:
82
            dispcmd = str([c] + args)
83
            # remember shell=False, so use git.cmd on windows, not just git
84
            p = subprocess.Popen(
85
                [c] + args,
86
                cwd=cwd,
87
                env=env,
88
                stdout=subprocess.PIPE,
89
                stderr=(subprocess.PIPE if hide_stderr else None))
90
            break
91
        except EnvironmentError:
92
            e = sys.exc_info()[1]
93
            if e.errno == errno.ENOENT:
94
                continue
95
            if verbose:
96
                print("unable to run %s" % dispcmd)
97
                print(e)
98
            return None, None
99
    else:
100
        if verbose:
101
            print("unable to find command, tried %s" % (commands, ))
102
        return None, None
103
    stdout = p.communicate()[0].strip()
104
    if sys.version_info[0] >= 3:
105
        stdout = stdout.decode()
106
    if p.returncode != 0:
107 View Code Duplication
        if verbose:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
108
            print("unable to run %s (error)" % dispcmd)
109
            print("stdout was %s" % stdout)
110
        return None, p.returncode
111
    return stdout, p.returncode
112
113
114
def versions_from_parentdir(parentdir_prefix, root, verbose):
115
    """Try to determine the version from the parent directory name.
116
117
    Source tarballs conventionally unpack into a directory that includes both
118
    the project name and a version string. We will also support searching up
119
    two directory levels for an appropriately named parent directory
120
    """
121
    rootdirs = []
122
123
    for i in range(3):
124
        dirname = os.path.basename(root)
125
        if dirname.startswith(parentdir_prefix):
126
            return {
127
                "version": dirname[len(parentdir_prefix):],
128
                "full-revisionid": None,
129
                "dirty": False,
130
                "error": None,
131
                "date": None
132 View Code Duplication
            }
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
133
        else:
134
            rootdirs.append(root)
135
            root = os.path.dirname(root)  # up a level
136
137
    if verbose:
138
        print("Tried directories %s but none started with prefix %s" %
139
              (str(rootdirs), parentdir_prefix))
140
    raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
141
142
143
@register_vcs_handler("git", "get_keywords")
144
def git_get_keywords(versionfile_abs):
145
    """Extract version information from the given file."""
146
    # the code embedded in _version.py can just fetch the value of these
147
    # keywords. When used from setup.py, we don't want to import _version.py,
148
    # so we do it with a regexp instead. This function is not used from
149
    # _version.py.
150
    keywords = {}
151
    try:
152
        f = open(versionfile_abs, "r")
153
        for line in f.readlines():
154
            if line.strip().startswith("git_refnames ="):
155
                mo = re.search(r'=\s*"(.*)"', line)
156
                if mo:
157
                    keywords["refnames"] = mo.group(1)
158
            if line.strip().startswith("git_full ="):
159
                mo = re.search(r'=\s*"(.*)"', line)
160
                if mo:
161 View Code Duplication
                    keywords["full"] = mo.group(1)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
162
            if line.strip().startswith("git_date ="):
163
                mo = re.search(r'=\s*"(.*)"', line)
164
                if mo:
165
                    keywords["date"] = mo.group(1)
166
        f.close()
167
    except EnvironmentError:
168
        pass
169
    return keywords
170
171
172
@register_vcs_handler("git", "keywords")
173
def git_versions_from_keywords(keywords, tag_prefix, verbose):
174
    """Get version information from git keywords."""
175
    if not keywords:
176
        raise NotThisMethod("no keywords at all, weird")
177
    date = keywords.get("date")
178
    if date is not None:
179
        # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant
180
        # datestamp. However we prefer "%ci" (which expands to an "ISO-8601
181
        # -like" string, which we must then edit to make compliant), because
182
        # it's been around since git-1.5.3, and it's too difficult to
183
        # discover which version we're using, or to work around using an
184
        # older one.
185
        date = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
186
    refnames = keywords["refnames"].strip()
187
    if refnames.startswith("$Format"):
188
        if verbose:
189
            print("keywords are unexpanded, not using")
190
        raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
191
    refs = set([r.strip() for r in refnames.strip("()").split(",")])
192
    # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
193
    # just "foo-1.0". If we see a "tag: " prefix, prefer those.
194
    TAG = "tag: "
195
    tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)])
196
    if not tags:
197
        # Either we're using git < 1.8.3, or there really are no tags. We use
198
        # a heuristic: assume all version tags have a digit. The old git %d
199
        # expansion behaves like git log --decorate=short and strips out the
200
        # refs/heads/ and refs/tags/ prefixes that would let us distinguish
201
        # between branches and tags. By ignoring refnames without digits, we
202
        # filter out many common branch names like "release" and
203
        # "stabilization", as well as "HEAD" and "master".
204
        tags = set([r for r in refs if re.search(r'\d', r)])
205
        if verbose:
206
            print("discarding '%s', no digits" % ",".join(refs - tags))
207
    if verbose:
208
        print("likely tags: %s" % ",".join(sorted(tags)))
209
    for ref in sorted(tags):
210
        # sorting will prefer e.g. "2.0" over "2.0rc1"
211
        if ref.startswith(tag_prefix):
212
            r = ref[len(tag_prefix):]
213
            if verbose:
214
                print("picking %s" % r)
215
            return {
216 View Code Duplication
                "version": r,
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
217
                "full-revisionid": keywords["full"].strip(),
218
                "dirty": False,
219
                "error": None,
220
                "date": date
221
            }
222
    # no suitable tags, so version is "0+unknown", but full hex is still there
223
    if verbose:
224
        print("no suitable tags, using unknown + full revision id")
225
    return {
226
        "version": "0+unknown",
227
        "full-revisionid": keywords["full"].strip(),
228
        "dirty": False,
229
        "error": "no suitable tags",
230
        "date": None
231
    }
232
233
234
@register_vcs_handler("git", "pieces_from_vcs")
235
def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
236
    """Get version from 'git describe' in the root of the source tree.
237
238
    This only gets called if the git-archive 'subst' keywords were *not*
239
    expanded, and _version.py hasn't already been rewritten with a short
240
    version string, meaning we're inside a checked out source tree.
241
    """
242
    GITS = ["git"]
243
    if sys.platform == "win32":
244
        GITS = ["git.cmd", "git.exe"]
245
246
    out, rc = run_command(
247
        GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True)
248
    if rc != 0:
249
        if verbose:
250
            print("Directory %s not under git control" % root)
251
        raise NotThisMethod("'git rev-parse --git-dir' returned error")
252
253
    # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
254
    # if there isn't one, this yields HEX[-dirty] (no NUM)
255
    describe_out, rc = run_command(
256
        GITS, [
257
            "describe", "--tags", "--dirty", "--always", "--long", "--match",
258
            "%s*" % tag_prefix
259
        ],
260
        cwd=root)
261
    # --long was added in git-1.5.5
262
    if describe_out is None:
263
        raise NotThisMethod("'git describe' failed")
264
    describe_out = describe_out.strip()
265
    full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
266
    if full_out is None:
267
        raise NotThisMethod("'git rev-parse' failed")
268
    full_out = full_out.strip()
269
270
    pieces = {}
271
    pieces["long"] = full_out
272
    pieces["short"] = full_out[:7]  # maybe improved later
273
    pieces["error"] = None
274
275
    # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
276
    # TAG might have hyphens.
277
    git_describe = describe_out
278
279
    # look for -dirty suffix
280
    dirty = git_describe.endswith("-dirty")
281
    pieces["dirty"] = dirty
282
    if dirty:
283
        git_describe = git_describe[:git_describe.rindex("-dirty")]
284
285
    # now we have TAG-NUM-gHEX or HEX
286
287
    if "-" in git_describe:
288
        # TAG-NUM-gHEX
289
        mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
290
        if not mo:
291
            # unparseable. Maybe git-describe is misbehaving?
292
            pieces["error"] = ("unable to parse git-describe output: '%s'" %
293
                               describe_out)
294
            return pieces
295
296
        # tag
297
        full_tag = mo.group(1)
298
        if not full_tag.startswith(tag_prefix):
299
            if verbose:
300
                fmt = "tag '%s' doesn't start with prefix '%s'"
301
                print(fmt % (full_tag, tag_prefix))
302
            pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" %
303
                               (full_tag, tag_prefix))
304
            return pieces
305
        pieces["closest-tag"] = full_tag[len(tag_prefix):]
306
307
        # distance: number of commits since tag
308
        pieces["distance"] = int(mo.group(2))
309
310
        # commit: short hex revision ID
311
        pieces["short"] = mo.group(3)
312
313
    else:
314
        # HEX: no tags
315
        pieces["closest-tag"] = None
316
        count_out, rc = run_command(
317
            GITS, ["rev-list", "HEAD", "--count"], cwd=root)
318
        pieces["distance"] = int(count_out)  # total number of commits
319
320
    # commit date: see ISO-8601 comment in git_versions_from_keywords()
321
    date = run_command(
322
        GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip()
323
    pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
324
325
    return pieces
326
327
328
def plus_or_dot(pieces):
329
    """Return a + if we don't already have one, else return a ."""
330
    if "+" in pieces.get("closest-tag", ""):
331
        return "."
332
    return "+"
333
334
335 View Code Duplication
def render_pep440(pieces):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
336
    """Build up version string, with post-release "local version identifier".
337
338
    Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
339
    get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
340
341
    Exceptions:
342
    1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
343
    """
344
    if pieces["closest-tag"]:
345
        rendered = pieces["closest-tag"]
346
        if pieces["distance"] or pieces["dirty"]:
347
            rendered += plus_or_dot(pieces)
348
            rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
349
            if pieces["dirty"]:
350
                rendered += ".dirty"
351
    else:
352
        # exception #1
353
        rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"])
354
        if pieces["dirty"]:
355
            rendered += ".dirty"
356 View Code Duplication
    return rendered
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
357
358
359
def render_pep440_pre(pieces):
360
    """TAG[.post.devDISTANCE] -- No -dirty.
361
362
    Exceptions:
363
    1: no tags. 0.post.devDISTANCE
364
    """
365
    if pieces["closest-tag"]:
366
        rendered = pieces["closest-tag"]
367
        if pieces["distance"]:
368
            rendered += ".post.dev%d" % pieces["distance"]
369
    else:
370
        # exception #1
371
        rendered = "0.post.dev%d" % pieces["distance"]
372
    return rendered
373
374
375
def render_pep440_post(pieces):
376
    """TAG[.postDISTANCE[.dev0]+gHEX] .
377
378
    The ".dev0" means dirty. Note that .dev0 sorts backwards
379
    (a dirty tree will appear "older" than the corresponding clean one),
380
    but you shouldn't be releasing software with -dirty anyways.
381
382
    Exceptions:
383 View Code Duplication
    1: no tags. 0.postDISTANCE[.dev0]
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
384
    """
385
    if pieces["closest-tag"]:
386
        rendered = pieces["closest-tag"]
387
        if pieces["distance"] or pieces["dirty"]:
388
            rendered += ".post%d" % pieces["distance"]
389
            if pieces["dirty"]:
390
                rendered += ".dev0"
391
            rendered += plus_or_dot(pieces)
392
            rendered += "g%s" % pieces["short"]
393
    else:
394
        # exception #1
395
        rendered = "0.post%d" % pieces["distance"]
396
        if pieces["dirty"]:
397
            rendered += ".dev0"
398
        rendered += "+g%s" % pieces["short"]
399
    return rendered
400
401
402
def render_pep440_old(pieces):
403
    """TAG[.postDISTANCE[.dev0]] .
404
405
    The ".dev0" means dirty.
406
407
    Eexceptions:
408
    1: no tags. 0.postDISTANCE[.dev0]
409
    """
410
    if pieces["closest-tag"]:
411
        rendered = pieces["closest-tag"]
412
        if pieces["distance"] or pieces["dirty"]:
413
            rendered += ".post%d" % pieces["distance"]
414
            if pieces["dirty"]:
415
                rendered += ".dev0"
416
    else:
417
        # exception #1
418
        rendered = "0.post%d" % pieces["distance"]
419
        if pieces["dirty"]:
420
            rendered += ".dev0"
421
    return rendered
422
423
424
def render_git_describe(pieces):
425
    """TAG[-DISTANCE-gHEX][-dirty].
426
427
    Like 'git describe --tags --dirty --always'.
428
429
    Exceptions:
430
    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
431
    """
432
    if pieces["closest-tag"]:
433
        rendered = pieces["closest-tag"]
434
        if pieces["distance"]:
435
            rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
436
    else:
437
        # exception #1
438
        rendered = pieces["short"]
439
    if pieces["dirty"]:
440
        rendered += "-dirty"
441
    return rendered
442
443
444
def render_git_describe_long(pieces):
445 View Code Duplication
    """TAG-DISTANCE-gHEX[-dirty].
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
446
447
    Like 'git describe --tags --dirty --always -long'.
448
    The distance/hash is unconditional.
449
450
    Exceptions:
451
    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
452
    """
453
    if pieces["closest-tag"]:
454
        rendered = pieces["closest-tag"]
455
        rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
456
    else:
457
        # exception #1
458
        rendered = pieces["short"]
459
    if pieces["dirty"]:
460
        rendered += "-dirty"
461
    return rendered
462
463
464
def render(pieces, style):
465
    """Render the given version pieces into the requested style."""
466
    if pieces["error"]:
467
        return {
468
            "version": "unknown",
469
            "full-revisionid": pieces.get("long"),
470
            "dirty": None,
471
            "error": pieces["error"],
472
            "date": None
473
        }
474
475
    if not style or style == "default":
476
        style = "pep440"  # the default
477
478
    if style == "pep440":
479
        rendered = render_pep440(pieces)
480
    elif style == "pep440-pre":
481
        rendered = render_pep440_pre(pieces)
482
    elif style == "pep440-post":
483
        rendered = render_pep440_post(pieces)
484
    elif style == "pep440-old":
485
        rendered = render_pep440_old(pieces)
486
    elif style == "git-describe":
487
        rendered = render_git_describe(pieces)
488
    elif style == "git-describe-long":
489
        rendered = render_git_describe_long(pieces)
490
    else:
491
        raise ValueError("unknown style '%s'" % style)
492
493
    return {
494
        "version": rendered,
495
        "full-revisionid": pieces["long"],
496
        "dirty": pieces["dirty"],
497
        "error": None,
498
        "date": pieces.get("date")
499
    }
500
501
502
def get_versions():
503
    """Get version information or return default if unable to do so."""
504
    # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
505
    # __file__, we can work backwards from there to the root. Some
506
    # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
507
    # case we can only use expanded keywords.
508
509
    cfg = get_config()
510
    verbose = cfg.verbose
511
512
    try:
513
        return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
514
                                          verbose)
515
    except NotThisMethod:
516
        pass
517
518
    try:
519
        root = os.path.realpath(__file__)
520
        # versionfile_source is the relative path from the top of the source
521
        # tree (where the .git directory might live) to this file. Invert
522
        # this to find the root from __file__.
523
        for i in cfg.versionfile_source.split('/'):
524
            root = os.path.dirname(root)
525
    except NameError:
526
        return {
527
            "version": "0+unknown",
528
            "full-revisionid": None,
529
            "dirty": None,
530
            "error": "unable to find root of source tree",
531
            "date": None
532
        }
533
534
    try:
535
        pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
536
        return render(pieces, cfg.style)
537
    except NotThisMethod:
538
        pass
539
540
    try:
541
        if cfg.parentdir_prefix:
542
            return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
543
    except NotThisMethod:
544
        pass
545
546
    return {
547
        "version": "0+unknown",
548
        "full-revisionid": None,
549
        "dirty": None,
550
        "error": "unable to compute version",
551
        "date": None
552
    }
553