Skip to content

Commit 4eaa2e9

Browse files
Speedup of calc_release_version.py (#1383)
* Cleaner argument handling and type-checking in calc_release_version * Speedup of calc_release_version by minimizing subprocesses Instead of continually asking Git whether a particular commit hash is pointed-to by a given tag, ask Git upfront for a mapping between all commit hashes and the associated tags. Then, look for the commit in the mapping as a single step.
1 parent c06942a commit 4eaa2e9

File tree

2 files changed

+122
-80
lines changed

2 files changed

+122
-80
lines changed

build/calc_release_version.py

Lines changed: 116 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
23

34
#
45
# Copyright 2018-present MongoDB, Inc.
@@ -20,6 +21,9 @@
2021
A script that calculates the release version number (based on the current Git
2122
branch and/or recent tags in history) to assign to a tarball generated from the
2223
current Git commit.
24+
25+
This script needs to remain compatible with its target platforms, which currently
26+
includes RHEL 6, which uses Python 2.6!
2327
"""
2428

2529
# XXX NOTE XXX NOTE XXX NOTE XXX
@@ -34,9 +38,13 @@
3438
# of each command is desired, then add the -x option to the bash invocation.
3539
# XXX NOTE XXX NOTE XXX NOTE XXX
3640

41+
# pyright: reportTypeCommentUsage=false
42+
3743
import datetime
44+
import errno
3845
import re
3946
import subprocess
47+
import optparse # No 'argparse' on Python 2.6
4048
import sys
4149

4250
try:
@@ -46,21 +54,39 @@
4654
except ImportError:
4755
# Fallback to deprecated pkg_resources.
4856
try:
49-
from pkg_resources.extern.packaging.version import Version
57+
from pkg_resources.extern.packaging.version import Version # type: ignore
5058
from pkg_resources import parse_version
5159
except ImportError:
5260
# Fallback to deprecated distutils.
5361
from distutils.version import LooseVersion as Version
5462
from distutils.version import LooseVersion as parse_version
5563

56-
DEBUG = len(sys.argv) > 1 and '-d' in sys.argv
57-
if DEBUG:
58-
print('Debugging output enabled.')
64+
parser = optparse.OptionParser(description=__doc__)
65+
parser.add_option("--debug", "-d", action="store_true", help="Enable debug output")
66+
parser.add_option("--previous", "-p", action="store_true", help="Calculate the previous version instead of the current")
67+
parser.add_option("--next-minor", action="store_true", help="Calculate the next minor version instead of the current")
68+
args, pos = parser.parse_args()
69+
assert not pos, "No positional arguments are expected"
70+
71+
72+
_DEBUG = args.debug # type: bool
73+
74+
75+
def debug(msg): # type: (str) -> None
76+
if _DEBUG:
77+
sys.stderr.write(msg)
78+
sys.stderr.write("\n")
79+
sys.stderr.flush()
80+
81+
82+
debug("Debugging output enabled.")
5983

6084
# This option indicates we are to determine the previous release version
61-
PREVIOUS = len(sys.argv) > 1 and '-p' in sys.argv
85+
PREVIOUS = args.previous # type: bool
6286
# This options indicates to output the next minor release version
63-
NEXT_MINOR = len(sys.argv) > 1 and '--next-minor' in sys.argv
87+
NEXT_MINOR = args.next_minor # type: bool
88+
89+
# fmt: off
6490

6591
PREVIOUS_TAG_RE = re.compile('(?P<ver>(?P<vermaj>[0-9]+)\\.(?P<vermin>[0-9]+)'
6692
'\\.(?P<verpatch>[0-9]+)(?:-(?P<verpre>.*))?)')
@@ -69,29 +95,33 @@
6995
RELEASE_BRANCH_RE = re.compile('(?:(?:refs/remotes/)?origin/)?(?P<brname>r'
7096
'(?P<vermaj>[0-9]+)\\.(?P<vermin>[0-9]+))')
7197

72-
def check_output(args):
98+
99+
def check_output(args): # type: (list[str]) -> str
73100
"""
74101
Delegates to subprocess.check_output() if it is available, otherwise
75102
provides a reasonable facsimile.
76103
"""
77-
if 'check_output' in dir(subprocess):
78-
out = subprocess.check_output(args)
79-
else:
104+
debug('Run command: {0}'.format(args))
105+
try:
80106
proc = subprocess.Popen(args, stdout=subprocess.PIPE)
81-
out, err = proc.communicate()
82-
ret = proc.poll()
83-
if ret:
84-
raise subprocess.CalledProcessError(ret, args[0], output=out)
85-
86-
if type(out) is bytes:
87-
# git isn't guaranteed to always return UTF-8, but for our purposes
88-
# this should be fine as tags and hashes should be ASCII only.
89-
out = out.decode('utf-8')
107+
except OSError as e:
108+
suppl = ''
109+
if e.errno == errno.ENOENT:
110+
suppl = 'Does the executable “{0}” not exist?'.format(args[0])
111+
raise RuntimeError("Failed to execute subprocess {0}: {1} [{2}]".format(args, e, suppl))
112+
out = proc.communicate()[0]
113+
ret = proc.poll()
114+
if ret:
115+
raise subprocess.CalledProcessError(ret, args[0])
116+
117+
# git isn't guaranteed to always return UTF-8, but for our purposes
118+
# this should be fine as tags and hashes should be ASCII only.
119+
out = out.decode('utf-8')
90120

91121
return out
92122

93123

94-
def check_head_tag():
124+
def check_head_tag(): # type: () -> str | None
95125
"""
96126
Checks the current HEAD to see if it has been tagged with a tag that matches
97127
the pattern for a release tag. Returns release version calculated from the
@@ -115,21 +145,19 @@ def check_head_tag():
115145
if release_tag_match:
116146
new_version_str = release_tag_match.group('ver')
117147
new_version_parsed = parse_version(new_version_str)
118-
if new_version_parsed > version_parsed:
119-
if DEBUG:
120-
print('HEAD release tag: ' + new_version_str)
148+
if new_version_parsed > version_parsed: # type: ignore
149+
debug('HEAD release tag: ' + new_version_str)
121150
version_str = new_version_str
122151
version_parsed = new_version_parsed
123152
found_tag = True
124153

125154
if found_tag:
126-
if DEBUG:
127-
print('Calculated version: ' + version_str)
155+
debug('Calculated version: ' + version_str)
128156
return version_str
129157

130158
return None
131159

132-
def get_next_minor(prerelease_marker):
160+
def get_next_minor(prerelease_marker): # type: (str) -> str
133161
"""
134162
get_next_minor does the following:
135163
- Inspect the branches that fit the convention for a release branch.
@@ -157,49 +185,70 @@ def get_next_minor(prerelease_marker):
157185
str(version_new['patch']) + '-' + \
158186
version_new['prerelease']
159187
new_version_parsed = parse_version(new_version_str)
160-
if new_version_parsed > version_parsed:
188+
if new_version_parsed > version_parsed: # type: ignore
161189
version_str = new_version_str
162190
version_parsed = new_version_parsed
163-
if DEBUG:
164-
print('Found new best version "' + version_str \
191+
debug('Found new best version "' + version_str \
165192
+ '" based on branch "' \
166193
+ release_branch_match.group('brname') + '"')
167194
return version_str
168195

169-
def get_branch_tags(active_branch_name):
196+
def get_branch_tags(active_branch_name): # type: (str) -> list[str]
170197
"""
171-
Returns the tag or tags (as a single string with newlines between tags)
172-
corresponding to the current branch, which must not be master. If the
173-
specified branch is a release branch then return all tags based on the
174-
major/minor X.Y release version. If the specified branch is neither master
175-
nor a release branch, then walk backwards in history until the first tag
176-
matching the glob '1.*' and return that tag.
198+
Returns a list of tags corresponding to the current branch, which must not
199+
be master. If the specified branch is a release branch then return all tags
200+
based on the major/minor X.Y release version. If the specified branch is
201+
neither master nor a release branch, then walk backwards in history until
202+
the first tag matching the glob '1.*' and return that tag.
177203
"""
178204

179205
if active_branch_name == 'master':
180206
raise Exception('this method is not meant to be called while on "master"')
181-
tags = ''
207+
182208
release_branch_match = RELEASE_BRANCH_RE.match(active_branch_name)
183209
if release_branch_match:
184210
# This is a release branch, so look for tags only on this branch
185211
tag_glob = release_branch_match.group('vermaj') + '.' \
186212
+ release_branch_match.group('vermin') + '.*'
187-
tags = check_output(['git', 'tag', '--list', tag_glob])
188-
else:
189-
# Not a release branch, so look for the most recent tag in history
190-
commits = check_output(['git', 'log', '--pretty=format:%H',
191-
'--no-merges'])
192-
if len(commits) > 0:
193-
for commit in commits.splitlines():
194-
tags = check_output(['git', 'tag', '--points-at',
195-
commit, '--list', '1.*'])
196-
if len(tags) > 0:
197-
# found a tag, we should be done
198-
break
199-
200-
return tags
201-
202-
def process_and_sort_tags(tags):
213+
return check_output(['git', 'tag', '--list', tag_glob]).splitlines()
214+
215+
# Not a release branch, so look for the most recent tag in history
216+
commits = check_output(['git', 'log', '--pretty=format:%H', '--no-merges'])
217+
tags_by_obj = get_object_tags()
218+
for commit in commits.splitlines():
219+
got = tags_by_obj.get(commit)
220+
if got:
221+
return got
222+
# No tags
223+
return []
224+
225+
226+
def iter_tag_lines():
227+
"""
228+
Generate a list of pairs of strings, where the first is a commit hash, and
229+
the second is a tag that is associated with that commit. Duplicate commits
230+
are possible.
231+
"""
232+
output = check_output(['git', 'tag', '--list', '1.*', '--format=%(*objectname)|%(tag)'])
233+
lines = output.splitlines()
234+
for l in lines:
235+
obj, tag = l.split('|', 1)
236+
if tag:
237+
yield obj, tag
238+
239+
240+
def get_object_tags(): # type: () -> dict[str, list[str]]
241+
"""
242+
Obtain a mapping between commit hashes and a list of tags that point to
243+
that commit. Untagged commits will not be included in the resulting map.
244+
"""
245+
ret = {} # type: dict[str, list[str]]
246+
for obj, tag in iter_tag_lines():
247+
ret.setdefault(obj, []).append(tag)
248+
return ret
249+
250+
251+
def process_and_sort_tags(tags): # type: (list[str]) -> list[str]
203252
"""
204253
Given a string (as returned from get_branch_tags), return a sorted list of
205254
zero or more tags (sorted based on the Version comparison) which meet
@@ -209,23 +258,22 @@ def process_and_sort_tags(tags):
209258
1.x.y-preX iff 1.x.y does not already exist)
210259
"""
211260

212-
processed_and_sorted_tags = []
261+
processed_and_sorted_tags = [] # type: list[str]
213262
if not tags or len(tags) == 0:
214263
return processed_and_sorted_tags
215264

216-
raw_tags = tags.splitlines()
217265
# find all the final release tags
218-
for tag in raw_tags:
266+
for tag in tags:
219267
release_tag_match = RELEASE_TAG_RE.match(tag)
220268
if release_tag_match and not release_tag_match.group('verpre'):
221269
processed_and_sorted_tags.append(tag)
222270
# collect together final release tags and pre-release tags for
223271
# versions that have not yet had a final release
224-
for tag in raw_tags:
272+
for tag in tags:
225273
tag_parts = tag.split('-')
226274
if len(tag_parts) >= 2 and tag_parts[0] not in processed_and_sorted_tags:
227275
processed_and_sorted_tags.append(tag)
228-
processed_and_sorted_tags.sort(key=Version)
276+
processed_and_sorted_tags.sort(key=Version) # type: ignore
229277

230278
return processed_and_sorted_tags
231279

@@ -254,8 +302,7 @@ def main():
254302
+ '+git' + head_commit_short
255303

256304
if NEXT_MINOR:
257-
if DEBUG:
258-
print('Calculating next minor release')
305+
debug('Calculating next minor release')
259306
return get_next_minor(prerelease_marker)
260307

261308
head_tag_ver = check_head_tag()
@@ -264,8 +311,7 @@ def main():
264311

265312
active_branch_name = check_output(['git', 'rev-parse',
266313
'--abbrev-ref', 'HEAD']).strip()
267-
if DEBUG:
268-
print('Calculating release version for branch: ' + active_branch_name)
314+
debug('Calculating release version for branch: ' + active_branch_name)
269315
if active_branch_name == 'master':
270316
return get_next_minor(prerelease_marker)
271317

@@ -287,27 +333,25 @@ def main():
287333
str(version_new['patch']) + '-' + \
288334
version_new['prerelease']
289335
new_version_parsed = parse_version(new_version_str)
290-
if new_version_parsed > version_parsed:
336+
if new_version_parsed > version_parsed: # type: ignore
291337
version_str = new_version_str
292338
version_parsed = new_version_parsed
293-
if DEBUG:
294-
print('Found new best version "' + version_str \
339+
debug('Found new best version "' + version_str \
295340
+ '" from tag "' + release_tag_match.group('ver') + '"')
296341

297342
return version_str
298343

299-
def previous(rel_ver):
344+
def previous(rel_ver): # type: (str) -> str
300345
"""
301346
Given a release version, find the previous version based on the latest Git
302347
tag that is strictly a lower version than the given release version.
303348
"""
304-
if DEBUG:
305-
print('Calculating previous release version (option -p was specified).')
349+
debug('Calculating previous release version (option -p was specified).')
306350
version_str = '0.0.0'
307351
version_parsed = parse_version(version_str)
308352
rel_ver_str = rel_ver
309353
rel_ver_parsed = parse_version(rel_ver_str)
310-
tags = check_output(['git', 'tag', '--list', '1.*'])
354+
tags = check_output(['git', 'tag', '--list', '1.*']).splitlines()
311355
processed_and_sorted_tags = process_and_sort_tags(tags)
312356
for tag in processed_and_sorted_tags:
313357
previous_tag_match = PREVIOUS_TAG_RE.match(tag)
@@ -323,17 +367,15 @@ def previous(rel_ver):
323367
if version_new['prerelease'] is not None:
324368
new_version_str += '-' + version_new['prerelease']
325369
new_version_parsed = parse_version(new_version_str)
326-
if new_version_parsed < rel_ver_parsed and new_version_parsed > version_parsed:
370+
if new_version_parsed < rel_ver_parsed and new_version_parsed > version_parsed: # type: ignore
327371
version_str = new_version_str
328372
version_parsed = new_version_parsed
329-
if DEBUG:
330-
print('Found new best version "' + version_str \
373+
debug('Found new best version "' + version_str \
331374
+ '" from tag "' + tag + '"')
332375

333376
return version_str
334377

335378
RELEASE_VER = previous(main()) if PREVIOUS else main()
336379

337-
if DEBUG:
338-
print('Final calculated release version:')
380+
debug('Final calculated release version:')
339381
print(RELEASE_VER)

build/calc_release_version_selftest.sh

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ cp calc_release_version.py calc_release_version_test.py
4040
echo "Test a tagged commit ... begin"
4141
{
4242
git checkout 1.23.4 --quiet
43-
got=$("${PYTHON_INTERP}" calc_release_version_test.py)
43+
got=$("${PYTHON_INTERP}" calc_release_version_test.py --debug)
4444
assert_eq "$got" "1.23.4"
45-
got=$("${PYTHON_INTERP}" calc_release_version_test.py -p)
45+
got=$("${PYTHON_INTERP}" calc_release_version_test.py --debug -p)
4646
assert_eq "$got" "1.23.3"
4747
git checkout - --quiet
4848
}
@@ -53,9 +53,9 @@ echo "Test an untagged commit ... begin"
5353
{
5454
# 42a818429d6d586a6abf22367ac6fea1e9ce3f2c is commit before 1.23.4
5555
git checkout 42a818429d6d586a6abf22367ac6fea1e9ce3f2c --quiet
56-
got=$("${PYTHON_INTERP}" calc_release_version_test.py)
56+
got=$("${PYTHON_INTERP}" calc_release_version_test.py --debug)
5757
assert_eq "$got" "1.23.4-$DATE+git42a818429d"
58-
got=$("${PYTHON_INTERP}" calc_release_version_test.py -p)
58+
got=$("${PYTHON_INTERP}" calc_release_version_test.py --debug -p)
5959
assert_eq "$got" "1.23.4"
6060
git checkout - --quiet
6161
}
@@ -64,14 +64,14 @@ echo "Test an untagged commit ... end"
6464
echo "Test next minor version ... begin"
6565
{
6666
CURRENT_SHORTREF=$(git rev-parse --revs-only --short=10 HEAD)
67-
got=$("${PYTHON_INTERP}" calc_release_version_test.py --next-minor)
67+
got=$("${PYTHON_INTERP}" calc_release_version_test.py --debug --next-minor)
6868
# XXX NOTE XXX NOTE XXX
6969
# If you find yourself looking at this line because the assertion below
7070
# failed, then it is probably because a new major/minor release was made.
7171
# Update the expected output to represent the correct next version.
7272
# XXX NOTE XXX NOTE XXX
7373
assert_eq "$got" "1.25.0-$DATE+git$CURRENT_SHORTREF"
74-
got=$("${PYTHON_INTERP}" calc_release_version_test.py --next-minor -p)
74+
got=$("${PYTHON_INTERP}" calc_release_version_test.py --debug --next-minor -p)
7575
# XXX NOTE XXX NOTE XXX
7676
# If you find yourself looking at this line because the assertion below
7777
# failed, then it is probably because a new major/minor release was made.

0 commit comments

Comments
 (0)