1
# Licensed under a 3-clause BSD style license - see LICENSE.rst
2

3 11
"""
4
Utilities for retrieving revision information from a project's git repository.
5
"""
6

7
# Do not remove the following comment; it is used by
8
# astropy_helpers.version_helpers to determine the beginning of the code in
9
# this module
10

11
# BEGIN
12

13 11
import locale
14 11
import os
15 11
import subprocess
16 11
import warnings
17

18 11
__all__ = ['get_git_devstr']
19

20

21 11
def _decode_stdio(stream):
22 11
    try:
23 11
        stdio_encoding = locale.getdefaultlocale()[1] or 'utf-8'
24 0
    except ValueError:
25 0
        stdio_encoding = 'utf-8'
26

27 11
    try:
28 11
        text = stream.decode(stdio_encoding)
29 0
    except UnicodeDecodeError:
30
        # Final fallback
31 0
        text = stream.decode('latin1')
32

33 11
    return text
34

35

36 11
def update_git_devstr(version, path=None):
37
    """
38
    Updates the git revision string if and only if the path is being imported
39
    directly from a git working copy.  This ensures that the revision number in
40
    the version string is accurate.
41
    """
42

43 0
    try:
44
        # Quick way to determine if we're in git or not - returns '' if not
45 0
        devstr = get_git_devstr(sha=True, show_warning=False, path=path)
46 0
    except OSError:
47 0
        return version
48

49 0
    if not devstr:
50
        # Probably not in git so just pass silently
51 0
        return version
52

53 0
    if 'dev' in version:  # update to the current git revision
54 0
        version_base = version.split('.dev', 1)[0]
55 0
        devstr = get_git_devstr(sha=False, show_warning=False, path=path)
56

57 0
        return version_base + '.dev' + devstr
58
    else:
59
        # otherwise it's already the true/release version
60 0
        return version
61

62

63 11
def get_git_devstr(sha=False, show_warning=True, path=None):
64
    """
65
    Determines the number of revisions in this repository.
66

67
    Parameters
68
    ----------
69
    sha : bool
70
        If True, the full SHA1 hash will be returned. Otherwise, the total
71
        count of commits in the repository will be used as a "revision
72
        number".
73

74
    show_warning : bool
75
        If True, issue a warning if git returns an error code, otherwise errors
76
        pass silently.
77

78
    path : str or None
79
        If a string, specifies the directory to look in to find the git
80
        repository.  If `None`, the current working directory is used, and must
81
        be the root of the git repository.
82
        If given a filename it uses the directory containing that file.
83

84
    Returns
85
    -------
86
    devversion : str
87
        Either a string with the revision number (if `sha` is False), the
88
        SHA1 hash of the current commit (if `sha` is True), or an empty string
89
        if git version info could not be identified.
90

91
    """
92

93 11
    if path is None:
94 11
        path = os.getcwd()
95

96 11
    if not os.path.isdir(path):
97 0
        path = os.path.abspath(os.path.dirname(path))
98

99 11
    if sha:
100
        # Faster for getting just the hash of HEAD
101 11
        cmd = ['rev-parse', 'HEAD']
102
    else:
103 11
        cmd = ['rev-list', '--count', 'HEAD']
104

105 11
    def run_git(cmd):
106 11
        try:
107 11
            p = subprocess.Popen(['git'] + cmd, cwd=path,
108
                                 stdout=subprocess.PIPE,
109
                                 stderr=subprocess.PIPE,
110
                                 stdin=subprocess.PIPE)
111 11
            stdout, stderr = p.communicate()
112 0
        except OSError as e:
113 0
            if show_warning:
114 0
                warnings.warn('Error running git: ' + str(e))
115 0
            return (None, b'', b'')
116

117 11
        if p.returncode == 128:
118 11
            if show_warning:
119 11
                warnings.warn('No git repository present at {0!r}! Using '
120
                              'default dev version.'.format(path))
121 11
            return (p.returncode, b'', b'')
122 11
        if p.returncode == 129:
123 0
            if show_warning:
124 0
                warnings.warn('Your git looks old (does it support {0}?); '
125
                              'consider upgrading to v1.7.2 or '
126
                              'later.'.format(cmd[0]))
127 0
            return (p.returncode, stdout, stderr)
128 11
        elif p.returncode != 0:
129 0
            if show_warning:
130 0
                warnings.warn('Git failed while determining revision '
131
                              'count: {0}'.format(_decode_stdio(stderr)))
132 0
            return (p.returncode, stdout, stderr)
133

134 11
        return p.returncode, stdout, stderr
135

136 11
    returncode, stdout, stderr = run_git(cmd)
137

138 11
    if not sha and returncode == 128:
139
        # git returns 128 if the command is not run from within a git
140
        # repository tree. In this case, a warning is produced above but we
141
        # return the default dev version of '0'.
142 11
        return '0'
143 11
    elif not sha and returncode == 129:
144
        # git returns 129 if a command option failed to parse; in
145
        # particular this could happen in git versions older than 1.7.2
146
        # where the --count option is not supported
147
        # Also use --abbrev-commit and --abbrev=0 to display the minimum
148
        # number of characters needed per-commit (rather than the full hash)
149 0
        cmd = ['rev-list', '--abbrev-commit', '--abbrev=0', 'HEAD']
150 0
        returncode, stdout, stderr = run_git(cmd)
151
        # Fall back on the old method of getting all revisions and counting
152
        # the lines
153 0
        if returncode == 0:
154 0
            return str(stdout.count(b'\n'))
155
        else:
156 0
            return ''
157 11
    elif sha:
158 11
        return _decode_stdio(stdout)[:40]
159
    else:
160 11
        return _decode_stdio(stdout).strip()
161

162

163
# This function is tested but it is only ever executed within a subprocess when
164
# creating a fake package, so it doesn't get picked up by coverage metrics.
165
def _get_repo_path(pathname, levels=None):  # pragma: no cover
166
    """
167
    Given a file or directory name, determine the root of the git repository
168
    this path is under.  If given, this won't look any higher than ``levels``
169
    (that is, if ``levels=0`` then the given path must be the root of the git
170
    repository and is returned if so.
171

172
    Returns `None` if the given path could not be determined to belong to a git
173
    repo.
174
    """
175

176
    if os.path.isfile(pathname):
177
        current_dir = os.path.abspath(os.path.dirname(pathname))
178
    elif os.path.isdir(pathname):
179
        current_dir = os.path.abspath(pathname)
180
    else:
181
        return None
182

183
    current_level = 0
184

185
    while levels is None or current_level <= levels:
186
        if os.path.exists(os.path.join(current_dir, '.git')):
187
            return current_dir
188

189
        current_level += 1
190
        if current_dir == os.path.dirname(current_dir):
191
            break
192

193
        current_dir = os.path.dirname(current_dir)
194

195
    return None

Read our documentation on viewing source code .

Loading