1
1
#!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
2
3
3
4
#
4
5
# Copyright 2018-present MongoDB, Inc.
20
21
A script that calculates the release version number (based on the current Git
21
22
branch and/or recent tags in history) to assign to a tarball generated from the
22
23
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!
23
27
"""
24
28
25
29
# XXX NOTE XXX NOTE XXX NOTE XXX
34
38
# of each command is desired, then add the -x option to the bash invocation.
35
39
# XXX NOTE XXX NOTE XXX NOTE XXX
36
40
41
+ # pyright: reportTypeCommentUsage=false
42
+
37
43
import datetime
44
+ import errno
38
45
import re
39
46
import subprocess
47
+ import optparse # No 'argparse' on Python 2.6
40
48
import sys
41
49
42
50
try :
46
54
except ImportError :
47
55
# Fallback to deprecated pkg_resources.
48
56
try :
49
- from pkg_resources .extern .packaging .version import Version
57
+ from pkg_resources .extern .packaging .version import Version # type: ignore
50
58
from pkg_resources import parse_version
51
59
except ImportError :
52
60
# Fallback to deprecated distutils.
53
61
from distutils .version import LooseVersion as Version
54
62
from distutils .version import LooseVersion as parse_version
55
63
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." )
59
83
60
84
# 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
62
86
# 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
64
90
65
91
PREVIOUS_TAG_RE = re .compile ('(?P<ver>(?P<vermaj>[0-9]+)\\ .(?P<vermin>[0-9]+)'
66
92
'\\ .(?P<verpatch>[0-9]+)(?:-(?P<verpre>.*))?)' )
69
95
RELEASE_BRANCH_RE = re .compile ('(?:(?:refs/remotes/)?origin/)?(?P<brname>r'
70
96
'(?P<vermaj>[0-9]+)\\ .(?P<vermin>[0-9]+))' )
71
97
72
- def check_output (args ):
98
+
99
+ def check_output (args ): # type: (list[str]) -> str
73
100
"""
74
101
Delegates to subprocess.check_output() if it is available, otherwise
75
102
provides a reasonable facsimile.
76
103
"""
77
- if 'check_output' in dir (subprocess ):
78
- out = subprocess .check_output (args )
79
- else :
104
+ debug ('Run command: {0}' .format (args ))
105
+ try :
80
106
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' )
90
120
91
121
return out
92
122
93
123
94
- def check_head_tag ():
124
+ def check_head_tag (): # type: () -> str | None
95
125
"""
96
126
Checks the current HEAD to see if it has been tagged with a tag that matches
97
127
the pattern for a release tag. Returns release version calculated from the
@@ -115,21 +145,19 @@ def check_head_tag():
115
145
if release_tag_match :
116
146
new_version_str = release_tag_match .group ('ver' )
117
147
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 )
121
150
version_str = new_version_str
122
151
version_parsed = new_version_parsed
123
152
found_tag = True
124
153
125
154
if found_tag :
126
- if DEBUG :
127
- print ('Calculated version: ' + version_str )
155
+ debug ('Calculated version: ' + version_str )
128
156
return version_str
129
157
130
158
return None
131
159
132
- def get_next_minor (prerelease_marker ):
160
+ def get_next_minor (prerelease_marker ): # type: (str) -> str
133
161
"""
134
162
get_next_minor does the following:
135
163
- Inspect the branches that fit the convention for a release branch.
@@ -157,49 +185,70 @@ def get_next_minor(prerelease_marker):
157
185
str (version_new ['patch' ]) + '-' + \
158
186
version_new ['prerelease' ]
159
187
new_version_parsed = parse_version (new_version_str )
160
- if new_version_parsed > version_parsed :
188
+ if new_version_parsed > version_parsed : # type: ignore
161
189
version_str = new_version_str
162
190
version_parsed = new_version_parsed
163
- if DEBUG :
164
- print ('Found new best version "' + version_str \
191
+ debug ('Found new best version "' + version_str \
165
192
+ '" based on branch "' \
166
193
+ release_branch_match .group ('brname' ) + '"' )
167
194
return version_str
168
195
169
- def get_branch_tags (active_branch_name ):
196
+ def get_branch_tags (active_branch_name ): # type: (str) -> list[str]
170
197
"""
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.
177
203
"""
178
204
179
205
if active_branch_name == 'master' :
180
206
raise Exception ('this method is not meant to be called while on "master"' )
181
- tags = ''
207
+
182
208
release_branch_match = RELEASE_BRANCH_RE .match (active_branch_name )
183
209
if release_branch_match :
184
210
# This is a release branch, so look for tags only on this branch
185
211
tag_glob = release_branch_match .group ('vermaj' ) + '.' \
186
212
+ 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]
203
252
"""
204
253
Given a string (as returned from get_branch_tags), return a sorted list of
205
254
zero or more tags (sorted based on the Version comparison) which meet
@@ -209,23 +258,22 @@ def process_and_sort_tags(tags):
209
258
1.x.y-preX iff 1.x.y does not already exist)
210
259
"""
211
260
212
- processed_and_sorted_tags = []
261
+ processed_and_sorted_tags = [] # type: list[str]
213
262
if not tags or len (tags ) == 0 :
214
263
return processed_and_sorted_tags
215
264
216
- raw_tags = tags .splitlines ()
217
265
# find all the final release tags
218
- for tag in raw_tags :
266
+ for tag in tags :
219
267
release_tag_match = RELEASE_TAG_RE .match (tag )
220
268
if release_tag_match and not release_tag_match .group ('verpre' ):
221
269
processed_and_sorted_tags .append (tag )
222
270
# collect together final release tags and pre-release tags for
223
271
# versions that have not yet had a final release
224
- for tag in raw_tags :
272
+ for tag in tags :
225
273
tag_parts = tag .split ('-' )
226
274
if len (tag_parts ) >= 2 and tag_parts [0 ] not in processed_and_sorted_tags :
227
275
processed_and_sorted_tags .append (tag )
228
- processed_and_sorted_tags .sort (key = Version )
276
+ processed_and_sorted_tags .sort (key = Version ) # type: ignore
229
277
230
278
return processed_and_sorted_tags
231
279
@@ -254,8 +302,7 @@ def main():
254
302
+ '+git' + head_commit_short
255
303
256
304
if NEXT_MINOR :
257
- if DEBUG :
258
- print ('Calculating next minor release' )
305
+ debug ('Calculating next minor release' )
259
306
return get_next_minor (prerelease_marker )
260
307
261
308
head_tag_ver = check_head_tag ()
@@ -264,8 +311,7 @@ def main():
264
311
265
312
active_branch_name = check_output (['git' , 'rev-parse' ,
266
313
'--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 )
269
315
if active_branch_name == 'master' :
270
316
return get_next_minor (prerelease_marker )
271
317
@@ -287,27 +333,25 @@ def main():
287
333
str (version_new ['patch' ]) + '-' + \
288
334
version_new ['prerelease' ]
289
335
new_version_parsed = parse_version (new_version_str )
290
- if new_version_parsed > version_parsed :
336
+ if new_version_parsed > version_parsed : # type: ignore
291
337
version_str = new_version_str
292
338
version_parsed = new_version_parsed
293
- if DEBUG :
294
- print ('Found new best version "' + version_str \
339
+ debug ('Found new best version "' + version_str \
295
340
+ '" from tag "' + release_tag_match .group ('ver' ) + '"' )
296
341
297
342
return version_str
298
343
299
- def previous (rel_ver ):
344
+ def previous (rel_ver ): # type: (str) -> str
300
345
"""
301
346
Given a release version, find the previous version based on the latest Git
302
347
tag that is strictly a lower version than the given release version.
303
348
"""
304
- if DEBUG :
305
- print ('Calculating previous release version (option -p was specified).' )
349
+ debug ('Calculating previous release version (option -p was specified).' )
306
350
version_str = '0.0.0'
307
351
version_parsed = parse_version (version_str )
308
352
rel_ver_str = rel_ver
309
353
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 ()
311
355
processed_and_sorted_tags = process_and_sort_tags (tags )
312
356
for tag in processed_and_sorted_tags :
313
357
previous_tag_match = PREVIOUS_TAG_RE .match (tag )
@@ -323,17 +367,15 @@ def previous(rel_ver):
323
367
if version_new ['prerelease' ] is not None :
324
368
new_version_str += '-' + version_new ['prerelease' ]
325
369
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
327
371
version_str = new_version_str
328
372
version_parsed = new_version_parsed
329
- if DEBUG :
330
- print ('Found new best version "' + version_str \
373
+ debug ('Found new best version "' + version_str \
331
374
+ '" from tag "' + tag + '"' )
332
375
333
376
return version_str
334
377
335
378
RELEASE_VER = previous (main ()) if PREVIOUS else main ()
336
379
337
- if DEBUG :
338
- print ('Final calculated release version:' )
380
+ debug ('Final calculated release version:' )
339
381
print (RELEASE_VER )
0 commit comments