aboutsummaryrefslogtreecommitdiff
blob: 2fd65d2c168d71aec9b7315723b27c2694f066b7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
"""Version information."""

import errno
import os
import subprocess
from importlib import import_module

_ver = None


def get_version(project, repo_file, api_version=None):
    """Determine a project's version information.

    Standardized version retrieval for git-based projects. In summary, if the
    api_version isn't specified it imports __version__ from the main module for
    the project. Next it tries to import extended information from a generated
    file (for packages using snakeoil's custom sdist phase) and if that fails
    assumes it's in a git repo and grabs the git info instead.

    :param project: module name
    :param repo_file: file belonging to module
    :param api_version: version for the project, if not specified __version__
        is imported from the main project module
    :return: a string describing the project version
    """
    global _ver  # pylint: disable=global-statement
    if _ver is None:
        version_info = None
        if api_version is None:
            try:
                api_version = getattr(import_module(project), "__version__")
            except ImportError:
                raise ValueError(f"no {project} module in the syspath")
        try:
            version_info = getattr(import_module(f"{project}._verinfo"), "version_info")
        except ImportError:
            # we're probably in a git repo
            path = os.path.dirname(os.path.abspath(repo_file))
            version_info = get_git_version(path)

        if version_info is None:
            s = ""
        elif version_info["tag"] == api_version:
            s = f" -- released {version_info['date']}"
        else:
            rev = version_info["rev"][:7]
            date = version_info["date"]
            commits = version_info.get("commits", None)
            commits = f"-{commits}" if commits is not None else ""
            s = f"{commits}-g{rev} -- {date}"

        _ver = f"{project} {api_version}{s}"
    return _ver


def _run_git(path, cmd):
    env = dict(os.environ)
    for key in env.copy():  # pragma: no cover
        if key.startswith("LC_"):
            del env[key]
    env["LC_CTYPE"] = "C"
    env["LC_ALL"] = "C"

    r = subprocess.Popen(
        ["git"] + list(cmd),
        stdout=subprocess.PIPE,
        env=env,
        stderr=subprocess.DEVNULL,
        cwd=path,
    )

    stdout = r.communicate()[0]
    return stdout, r.returncode


def get_git_version(path):
    """Return git related revision data."""
    path = os.path.abspath(path)
    try:
        stdout, ret = _run_git(path, ["log", "--format=%H\n%aD", "HEAD^..HEAD"])

        if ret != 0:
            return None

        data = stdout.decode().splitlines()
        tag = _get_git_tag(path, data[0])

        # get number of commits since most recent tag
        stdout, ret = _run_git(path, ["describe", "--tags", "--abbrev=0"])
        prev_tag = None
        commits = None
        if ret == 0:
            prev_tag = stdout.decode().strip()
            stdout, ret = _run_git(path, ["log", "--oneline", f"{prev_tag}..HEAD"])
            if ret == 0:
                commits = len(stdout.decode().splitlines())

        return {
            "rev": data[0],
            "date": data[1],
            "tag": tag,
            "commits": commits,
        }
    except EnvironmentError as exc:
        # ENOENT is thrown when the git binary can't be found.
        if exc.errno != errno.ENOENT:
            raise
        return None


def _get_git_tag(path, rev):
    stdout, _ = _run_git(path, ["name-rev", "--tag", rev])
    tag = stdout.decode().split()
    if len(tag) != 2:
        return None
    tag = tag[1]
    if not tag.startswith("tags/"):
        return None
    tag = tag[len("tags/") :]
    if tag.endswith("^0"):
        tag = tag[:-2]
    if tag.startswith("v"):
        tag = tag[1:]
    return tag