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
|