1 # Adapted from https://github.com/jbweston/miniver
3 # LICENSE: CC0 1.0 Universal license.
4 # http://creativecommons.org/publicdomain/zero/1.0/
6 # This file is in the public domain.
12 # When in the context of a stgit git repository or a stgit git archive
13 # tarball, this code will dynamically determine the stgit version.
15 # At build-time, this file is overwritten such that stgit distributions
16 # only contain the __version__ attribute. Obtaining the version from a
17 # rewritten _version.py is extremely inexpensive at runtime.
19 # The _get_version() function in this module is capable of determining
20 # the stgit package version in a variety of contexts, including:
22 # * When running stgit from its Git worktree, the version is dynamically
23 # determined by _get_version() which runs `git describe`. As such, the
24 # version is based on the last tag.
26 # * When running `setup.py build` or `setup.py sdist` from a stgit
27 # worktree, the version is dynamically determined from `git describe`
28 # as above, BUT the generated build artifacts are seeded with an
29 # updated _version.py file containing only this dynamically determined
30 # version. Thus stgit distributions contain a static version such that
31 # the version can be determined at runtime without consulting Git or
32 # any other external resources.
34 # * When running `setup.py` from a stgit source distribution, i.e. an
35 # unpacked tarball previously obtained by running `setup.py sdist`,
36 # the version is determined statically from a rewritten version of
37 # this file (_version.py). A key feature here is that obtaining the
38 # version from _version.py is done by exec()ing that file and not
39 # importing it as a stgit submodule. This is important at pip
40 # install-time when the stgit package cannot (yet) be imported.
42 # * When running an installed version of stgit, the version is also
43 # statically determined from a rewritten _version.py.
45 # * When running `setup.py build` or `setup.py sdist` from a Git archive
46 # generated with, for example,
48 # git archive --prefix=stgit/ --output stgit.tar.gz HEAD
50 # the _version.py file is updated by `git archive` with ref names and
51 # the commit hash using the export-subst gitattribute mechanism.
52 # _get_version_from_git_archive() is able to construct the version
53 # from the substituted ref names and commit id information.
57 from collections
import namedtuple
61 _Version
= namedtuple("_Version", ("release", "dev", "labels"))
65 package_root
= os
.path
.dirname(os
.path
.realpath(__file__
))
66 worktree_path
= os
.path
.dirname(package_root
)
68 version
= _get_version_from_git(worktree_path
)
69 if version
is not None:
70 return _pep440_format(version
)
72 version
= _get_version_from_git_archive()
73 if version
is not None:
74 return _pep440_format(version
)
76 return _pep440_format(_Version("unknown", None, None))
79 def _pep440_format(version
):
80 release
, dev
, labels
= version
84 if release
.endswith("-dev") or release
.endswith(".dev"):
86 else: # prefer PEP440 over strict adhesion to semver
87 parts
.append(".dev{}".format(dev
))
91 parts
.append(".".join(labels
))
96 def _parse_git_describe(description
, worktree_path
=None):
97 if description
.startswith('v'):
98 description
= description
[1:]
100 parts
= description
.split('-', 2)
103 release
, dev
, git_hash
= parts
105 # No tags, only the git hash
106 git_hash
= 'g' + parts
[0]
114 labels
.append(git_hash
)
116 if worktree_path
is not None:
118 p
= subprocess
.run(["git", "diff", "--quiet"], cwd
=worktree_path
)
120 labels
.append("confused") # This should never happen.
122 if p
.returncode
== 1:
123 labels
.append("dirty")
125 return _Version(release
, dev
, labels
)
128 def _get_version_from_git(worktree_path
):
131 ["git", "rev-parse", "--show-toplevel"],
133 stdout
=subprocess
.PIPE
,
134 stderr
=subprocess
.DEVNULL
,
138 if p
.returncode
!= 0:
140 if not os
.path
.samefile(p
.stdout
.decode().rstrip("\n"), worktree_path
):
141 # The top-level directory of the current Git worktree is not the same
142 # as the root directory of the distribution: do not extract the version
146 # git describe --first-parent does not take into account tags from branches
147 # that were merged-in. The '--long' flag gets us the 'dev' version and
148 # git hash, '--always' returns the git hash even if there are no tags.
149 for opts
in [["--first-parent"], []]:
152 ["git", "describe", "--long", "--always"] + opts
,
154 stdout
=subprocess
.PIPE
,
155 stderr
=subprocess
.PIPE
,
159 if p
.returncode
== 0:
164 description
= p
.stdout
.decode().strip()
166 return _parse_git_describe(description
, worktree_path
)
169 def _get_version_from_git_archive():
170 # git archive replaces these export-subst placeholders.
171 refnames
= "$Format:%D$"
172 git_hash
= "$Format:%h$"
173 describe
= "$Format:%(describe:match=v*)$"
175 if not describe
.startswith('$Format'):
176 # As of Git 2.32, the %(describe) is a valid export-subst placeholder
177 return _parse_git_describe(describe
, worktree_path
=None)
179 # Older versions of Git can only provide either a tag if the archive is
180 # from a tagged commit, or the commit id without information about the
181 # preceeding tag for archives from a non-tagged commit.
183 if git_hash
.startswith("$Format") or refnames
.startswith("$Format"):
184 # variables not expanded during 'git archive'
188 refs
= set(r
.strip() for r
in refnames
.split(","))
189 version_tags
= set(r
[len(vtag
) :] for r
in refs
if r
.startswith(vtag
))
191 release
= sorted(version_tags
)[0] # prefer e.g. "2.0" over "2.0rc1"
192 return _Version(release
, dev
=None, labels
=None)
194 return _Version("unknown", dev
=None, labels
=["g" + git_hash
])
197 __version__
= _get_version()