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