1bf215546Sopenharmony_ci#!/usr/bin/env python3 2bf215546Sopenharmony_ci# Copyright © 2019-2020 Intel Corporation 3bf215546Sopenharmony_ci 4bf215546Sopenharmony_ci# Permission is hereby granted, free of charge, to any person obtaining a copy 5bf215546Sopenharmony_ci# of this software and associated documentation files (the "Software"), to deal 6bf215546Sopenharmony_ci# in the Software without restriction, including without limitation the rights 7bf215546Sopenharmony_ci# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8bf215546Sopenharmony_ci# copies of the Software, and to permit persons to whom the Software is 9bf215546Sopenharmony_ci# furnished to do so, subject to the following conditions: 10bf215546Sopenharmony_ci 11bf215546Sopenharmony_ci# The above copyright notice and this permission notice shall be included in 12bf215546Sopenharmony_ci# all copies or substantial portions of the Software. 13bf215546Sopenharmony_ci 14bf215546Sopenharmony_ci# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15bf215546Sopenharmony_ci# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16bf215546Sopenharmony_ci# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17bf215546Sopenharmony_ci# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18bf215546Sopenharmony_ci# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19bf215546Sopenharmony_ci# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20bf215546Sopenharmony_ci# SOFTWARE. 21bf215546Sopenharmony_ci 22bf215546Sopenharmony_ci"""Generates release notes for a given version of mesa.""" 23bf215546Sopenharmony_ci 24bf215546Sopenharmony_ciimport asyncio 25bf215546Sopenharmony_ciimport datetime 26bf215546Sopenharmony_ciimport os 27bf215546Sopenharmony_ciimport pathlib 28bf215546Sopenharmony_ciimport re 29bf215546Sopenharmony_ciimport subprocess 30bf215546Sopenharmony_ciimport sys 31bf215546Sopenharmony_ciimport textwrap 32bf215546Sopenharmony_ciimport typing 33bf215546Sopenharmony_ciimport urllib.parse 34bf215546Sopenharmony_ci 35bf215546Sopenharmony_ciimport aiohttp 36bf215546Sopenharmony_cifrom mako.template import Template 37bf215546Sopenharmony_cifrom mako import exceptions 38bf215546Sopenharmony_ci 39bf215546Sopenharmony_ciimport docutils.utils 40bf215546Sopenharmony_ciimport docutils.parsers.rst.states as states 41bf215546Sopenharmony_ci 42bf215546Sopenharmony_ciCURRENT_GL_VERSION = '4.6' 43bf215546Sopenharmony_ciCURRENT_VK_VERSION = '1.3' 44bf215546Sopenharmony_ci 45bf215546Sopenharmony_ciTEMPLATE = Template(textwrap.dedent("""\ 46bf215546Sopenharmony_ci ${header} 47bf215546Sopenharmony_ci ${header_underline} 48bf215546Sopenharmony_ci 49bf215546Sopenharmony_ci %if not bugfix: 50bf215546Sopenharmony_ci Mesa ${this_version} is a new development release. People who are concerned 51bf215546Sopenharmony_ci with stability and reliability should stick with a previous release or 52bf215546Sopenharmony_ci wait for Mesa ${this_version[:-1]}1. 53bf215546Sopenharmony_ci %else: 54bf215546Sopenharmony_ci Mesa ${this_version} is a bug fix release which fixes bugs found since the ${previous_version} release. 55bf215546Sopenharmony_ci %endif 56bf215546Sopenharmony_ci 57bf215546Sopenharmony_ci Mesa ${this_version} implements the OpenGL ${gl_version} API, but the version reported by 58bf215546Sopenharmony_ci glGetString(GL_VERSION) or glGetIntegerv(GL_MAJOR_VERSION) / 59bf215546Sopenharmony_ci glGetIntegerv(GL_MINOR_VERSION) depends on the particular driver being used. 60bf215546Sopenharmony_ci Some drivers don't support all the features required in OpenGL ${gl_version}. OpenGL 61bf215546Sopenharmony_ci ${gl_version} is **only** available if requested at context creation. 62bf215546Sopenharmony_ci Compatibility contexts may report a lower version depending on each driver. 63bf215546Sopenharmony_ci 64bf215546Sopenharmony_ci Mesa ${this_version} implements the Vulkan ${vk_version} API, but the version reported by 65bf215546Sopenharmony_ci the apiVersion property of the VkPhysicalDeviceProperties struct 66bf215546Sopenharmony_ci depends on the particular driver being used. 67bf215546Sopenharmony_ci 68bf215546Sopenharmony_ci SHA256 checksum 69bf215546Sopenharmony_ci --------------- 70bf215546Sopenharmony_ci 71bf215546Sopenharmony_ci :: 72bf215546Sopenharmony_ci 73bf215546Sopenharmony_ci TBD. 74bf215546Sopenharmony_ci 75bf215546Sopenharmony_ci 76bf215546Sopenharmony_ci New features 77bf215546Sopenharmony_ci ------------ 78bf215546Sopenharmony_ci 79bf215546Sopenharmony_ci %for f in features: 80bf215546Sopenharmony_ci - ${rst_escape(f)} 81bf215546Sopenharmony_ci %endfor 82bf215546Sopenharmony_ci 83bf215546Sopenharmony_ci 84bf215546Sopenharmony_ci Bug fixes 85bf215546Sopenharmony_ci --------- 86bf215546Sopenharmony_ci 87bf215546Sopenharmony_ci %for b in bugs: 88bf215546Sopenharmony_ci - ${rst_escape(b)} 89bf215546Sopenharmony_ci %endfor 90bf215546Sopenharmony_ci 91bf215546Sopenharmony_ci 92bf215546Sopenharmony_ci Changes 93bf215546Sopenharmony_ci ------- 94bf215546Sopenharmony_ci %for c, author_line in changes: 95bf215546Sopenharmony_ci %if author_line: 96bf215546Sopenharmony_ci 97bf215546Sopenharmony_ci ${rst_escape(c)} 98bf215546Sopenharmony_ci 99bf215546Sopenharmony_ci %else: 100bf215546Sopenharmony_ci - ${rst_escape(c)} 101bf215546Sopenharmony_ci %endif 102bf215546Sopenharmony_ci %endfor 103bf215546Sopenharmony_ci """)) 104bf215546Sopenharmony_ci 105bf215546Sopenharmony_ci 106bf215546Sopenharmony_ci# copied from https://docutils.sourceforge.io/sandbox/xml2rst/xml2rstlib/markup.py 107bf215546Sopenharmony_ciclass Inliner(states.Inliner): 108bf215546Sopenharmony_ci """ 109bf215546Sopenharmony_ci Recognizer for inline markup. Derive this from the original inline 110bf215546Sopenharmony_ci markup parser for best results. 111bf215546Sopenharmony_ci """ 112bf215546Sopenharmony_ci 113bf215546Sopenharmony_ci # Copy static attributes from super class 114bf215546Sopenharmony_ci vars().update(vars(states.Inliner)) 115bf215546Sopenharmony_ci 116bf215546Sopenharmony_ci def quoteInline(self, text): 117bf215546Sopenharmony_ci """ 118bf215546Sopenharmony_ci `text`: ``str`` 119bf215546Sopenharmony_ci Return `text` with inline markup quoted. 120bf215546Sopenharmony_ci """ 121bf215546Sopenharmony_ci # Method inspired by `states.Inliner.parse` 122bf215546Sopenharmony_ci self.document = docutils.utils.new_document("<string>") 123bf215546Sopenharmony_ci self.document.settings.trim_footnote_reference_space = False 124bf215546Sopenharmony_ci self.document.settings.character_level_inline_markup = False 125bf215546Sopenharmony_ci self.document.settings.pep_references = False 126bf215546Sopenharmony_ci self.document.settings.rfc_references = False 127bf215546Sopenharmony_ci 128bf215546Sopenharmony_ci self.init_customizations(self.document.settings) 129bf215546Sopenharmony_ci 130bf215546Sopenharmony_ci self.reporter = self.document.reporter 131bf215546Sopenharmony_ci self.reporter.stream = None 132bf215546Sopenharmony_ci self.language = None 133bf215546Sopenharmony_ci self.parent = self.document 134bf215546Sopenharmony_ci remaining = docutils.utils.escape2null(text) 135bf215546Sopenharmony_ci checked = "" 136bf215546Sopenharmony_ci processed = [] 137bf215546Sopenharmony_ci unprocessed = [] 138bf215546Sopenharmony_ci messages = [] 139bf215546Sopenharmony_ci while remaining: 140bf215546Sopenharmony_ci original = remaining 141bf215546Sopenharmony_ci match = self.patterns.initial.search(remaining) 142bf215546Sopenharmony_ci if match: 143bf215546Sopenharmony_ci groups = match.groupdict() 144bf215546Sopenharmony_ci method = self.dispatch[groups['start'] or groups['backquote'] 145bf215546Sopenharmony_ci or groups['refend'] or groups['fnend']] 146bf215546Sopenharmony_ci before, inlines, remaining, sysmessages = method(self, match, 0) 147bf215546Sopenharmony_ci checked += before 148bf215546Sopenharmony_ci if inlines: 149bf215546Sopenharmony_ci assert len(inlines) == 1, "More than one inline found" 150bf215546Sopenharmony_ci inline = original[len(before) 151bf215546Sopenharmony_ci :len(original) - len(remaining)] 152bf215546Sopenharmony_ci rolePfx = re.search("^:" + self.simplename + ":(?=`)", 153bf215546Sopenharmony_ci inline) 154bf215546Sopenharmony_ci refSfx = re.search("_+$", inline) 155bf215546Sopenharmony_ci if rolePfx: 156bf215546Sopenharmony_ci # Prefixed roles need to be quoted in the middle 157bf215546Sopenharmony_ci checked += (inline[:rolePfx.end()] + "\\" 158bf215546Sopenharmony_ci + inline[rolePfx.end():]) 159bf215546Sopenharmony_ci elif refSfx and not re.search("^`", inline): 160bf215546Sopenharmony_ci # Pure reference markup needs to be quoted at the end 161bf215546Sopenharmony_ci checked += (inline[:refSfx.start()] + "\\" 162bf215546Sopenharmony_ci + inline[refSfx.start():]) 163bf215546Sopenharmony_ci else: 164bf215546Sopenharmony_ci # Quote other inlines by prefixing 165bf215546Sopenharmony_ci checked += "\\" + inline 166bf215546Sopenharmony_ci else: 167bf215546Sopenharmony_ci checked += remaining 168bf215546Sopenharmony_ci break 169bf215546Sopenharmony_ci # Quote all original backslashes 170bf215546Sopenharmony_ci checked = re.sub('\x00', "\\\x00", checked) 171bf215546Sopenharmony_ci return docutils.utils.unescape(checked, 1) 172bf215546Sopenharmony_ci 173bf215546Sopenharmony_ciinliner = Inliner(); 174bf215546Sopenharmony_ci 175bf215546Sopenharmony_ci 176bf215546Sopenharmony_ciasync def gather_commits(version: str) -> str: 177bf215546Sopenharmony_ci p = await asyncio.create_subprocess_exec( 178bf215546Sopenharmony_ci 'git', 'log', '--oneline', f'mesa-{version}..', '--grep', r'Closes: \(https\|#\).*', 179bf215546Sopenharmony_ci stdout=asyncio.subprocess.PIPE) 180bf215546Sopenharmony_ci out, _ = await p.communicate() 181bf215546Sopenharmony_ci assert p.returncode == 0, f"git log didn't work: {version}" 182bf215546Sopenharmony_ci return out.decode().strip() 183bf215546Sopenharmony_ci 184bf215546Sopenharmony_ci 185bf215546Sopenharmony_ciasync def parse_issues(commits: str) -> typing.List[str]: 186bf215546Sopenharmony_ci issues: typing.List[str] = [] 187bf215546Sopenharmony_ci for commit in commits.split('\n'): 188bf215546Sopenharmony_ci sha, message = commit.split(maxsplit=1) 189bf215546Sopenharmony_ci p = await asyncio.create_subprocess_exec( 190bf215546Sopenharmony_ci 'git', 'log', '--max-count', '1', r'--format=%b', sha, 191bf215546Sopenharmony_ci stdout=asyncio.subprocess.PIPE) 192bf215546Sopenharmony_ci _out, _ = await p.communicate() 193bf215546Sopenharmony_ci out = _out.decode().split('\n') 194bf215546Sopenharmony_ci 195bf215546Sopenharmony_ci for line in reversed(out): 196bf215546Sopenharmony_ci if line.startswith('Closes:'): 197bf215546Sopenharmony_ci bug = line.lstrip('Closes:').strip() 198bf215546Sopenharmony_ci if bug.startswith('https://gitlab.freedesktop.org/mesa/mesa'): 199bf215546Sopenharmony_ci # This means we have a bug in the form "Closes: https://..." 200bf215546Sopenharmony_ci issues.append(os.path.basename(urllib.parse.urlparse(bug).path)) 201bf215546Sopenharmony_ci elif ',' in bug: 202bf215546Sopenharmony_ci issues.extend([b.strip().lstrip('#') for b in bug.split(',')]) 203bf215546Sopenharmony_ci elif bug.startswith('#'): 204bf215546Sopenharmony_ci issues.append(bug.lstrip('#')) 205bf215546Sopenharmony_ci 206bf215546Sopenharmony_ci return issues 207bf215546Sopenharmony_ci 208bf215546Sopenharmony_ci 209bf215546Sopenharmony_ciasync def gather_bugs(version: str) -> typing.List[str]: 210bf215546Sopenharmony_ci commits = await gather_commits(version) 211bf215546Sopenharmony_ci issues = await parse_issues(commits) 212bf215546Sopenharmony_ci 213bf215546Sopenharmony_ci loop = asyncio.get_event_loop() 214bf215546Sopenharmony_ci async with aiohttp.ClientSession(loop=loop) as session: 215bf215546Sopenharmony_ci results = await asyncio.gather(*[get_bug(session, i) for i in issues]) 216bf215546Sopenharmony_ci typing.cast(typing.Tuple[str, ...], results) 217bf215546Sopenharmony_ci bugs = list(results) 218bf215546Sopenharmony_ci if not bugs: 219bf215546Sopenharmony_ci bugs = ['None'] 220bf215546Sopenharmony_ci return bugs 221bf215546Sopenharmony_ci 222bf215546Sopenharmony_ci 223bf215546Sopenharmony_ciasync def get_bug(session: aiohttp.ClientSession, bug_id: str) -> str: 224bf215546Sopenharmony_ci """Query gitlab to get the name of the issue that was closed.""" 225bf215546Sopenharmony_ci # Mesa's gitlab id is 176, 226bf215546Sopenharmony_ci url = 'https://gitlab.freedesktop.org/api/v4/projects/176/issues' 227bf215546Sopenharmony_ci params = {'iids[]': bug_id} 228bf215546Sopenharmony_ci async with session.get(url, params=params) as response: 229bf215546Sopenharmony_ci content = await response.json() 230bf215546Sopenharmony_ci return content[0]['title'] 231bf215546Sopenharmony_ci 232bf215546Sopenharmony_ci 233bf215546Sopenharmony_ciasync def get_shortlog(version: str) -> str: 234bf215546Sopenharmony_ci """Call git shortlog.""" 235bf215546Sopenharmony_ci p = await asyncio.create_subprocess_exec('git', 'shortlog', f'mesa-{version}..', 236bf215546Sopenharmony_ci stdout=asyncio.subprocess.PIPE) 237bf215546Sopenharmony_ci out, _ = await p.communicate() 238bf215546Sopenharmony_ci assert p.returncode == 0, 'error getting shortlog' 239bf215546Sopenharmony_ci assert out is not None, 'just for mypy' 240bf215546Sopenharmony_ci return out.decode() 241bf215546Sopenharmony_ci 242bf215546Sopenharmony_ci 243bf215546Sopenharmony_cidef walk_shortlog(log: str) -> typing.Generator[typing.Tuple[str, bool], None, None]: 244bf215546Sopenharmony_ci for l in log.split('\n'): 245bf215546Sopenharmony_ci if l.startswith(' '): # this means we have a patch description 246bf215546Sopenharmony_ci yield l.lstrip(), False 247bf215546Sopenharmony_ci elif l.strip(): 248bf215546Sopenharmony_ci yield l, True 249bf215546Sopenharmony_ci 250bf215546Sopenharmony_ci 251bf215546Sopenharmony_cidef calculate_next_version(version: str, is_point: bool) -> str: 252bf215546Sopenharmony_ci """Calculate the version about to be released.""" 253bf215546Sopenharmony_ci if '-' in version: 254bf215546Sopenharmony_ci version = version.split('-')[0] 255bf215546Sopenharmony_ci if is_point: 256bf215546Sopenharmony_ci base = version.split('.') 257bf215546Sopenharmony_ci base[2] = str(int(base[2]) + 1) 258bf215546Sopenharmony_ci return '.'.join(base) 259bf215546Sopenharmony_ci return version 260bf215546Sopenharmony_ci 261bf215546Sopenharmony_ci 262bf215546Sopenharmony_cidef calculate_previous_version(version: str, is_point: bool) -> str: 263bf215546Sopenharmony_ci """Calculate the previous version to compare to. 264bf215546Sopenharmony_ci 265bf215546Sopenharmony_ci In the case of -rc to final that verison is the previous .0 release, 266bf215546Sopenharmony_ci (19.3.0 in the case of 20.0.0, for example). for point releases that is 267bf215546Sopenharmony_ci the last point release. This value will be the same as the input value 268bf215546Sopenharmony_ci for a point release, but different for a major release. 269bf215546Sopenharmony_ci """ 270bf215546Sopenharmony_ci if '-' in version: 271bf215546Sopenharmony_ci version = version.split('-')[0] 272bf215546Sopenharmony_ci if is_point: 273bf215546Sopenharmony_ci return version 274bf215546Sopenharmony_ci base = version.split('.') 275bf215546Sopenharmony_ci if base[1] == '0': 276bf215546Sopenharmony_ci base[0] = str(int(base[0]) - 1) 277bf215546Sopenharmony_ci base[1] = '3' 278bf215546Sopenharmony_ci else: 279bf215546Sopenharmony_ci base[1] = str(int(base[1]) - 1) 280bf215546Sopenharmony_ci return '.'.join(base) 281bf215546Sopenharmony_ci 282bf215546Sopenharmony_ci 283bf215546Sopenharmony_cidef get_features(is_point_release: bool) -> typing.Generator[str, None, None]: 284bf215546Sopenharmony_ci p = pathlib.Path(__file__).parent.parent / 'docs' / 'relnotes' / 'new_features.txt' 285bf215546Sopenharmony_ci if p.exists(): 286bf215546Sopenharmony_ci if is_point_release: 287bf215546Sopenharmony_ci print("WARNING: new features being introduced in a point release", file=sys.stderr) 288bf215546Sopenharmony_ci with p.open('rt') as f: 289bf215546Sopenharmony_ci for line in f: 290bf215546Sopenharmony_ci yield line 291bf215546Sopenharmony_ci else: 292bf215546Sopenharmony_ci yield "None" 293bf215546Sopenharmony_ci p.unlink() 294bf215546Sopenharmony_ci else: 295bf215546Sopenharmony_ci yield "None" 296bf215546Sopenharmony_ci 297bf215546Sopenharmony_ci 298bf215546Sopenharmony_ciasync def main() -> None: 299bf215546Sopenharmony_ci v = pathlib.Path(__file__).parent.parent / 'VERSION' 300bf215546Sopenharmony_ci with v.open('rt') as f: 301bf215546Sopenharmony_ci raw_version = f.read().strip() 302bf215546Sopenharmony_ci is_point_release = '-rc' not in raw_version 303bf215546Sopenharmony_ci assert '-devel' not in raw_version, 'Do not run this script on -devel' 304bf215546Sopenharmony_ci version = raw_version.split('-')[0] 305bf215546Sopenharmony_ci previous_version = calculate_previous_version(version, is_point_release) 306bf215546Sopenharmony_ci this_version = calculate_next_version(version, is_point_release) 307bf215546Sopenharmony_ci today = datetime.date.today() 308bf215546Sopenharmony_ci header = f'Mesa {this_version} Release Notes / {today}' 309bf215546Sopenharmony_ci header_underline = '=' * len(header) 310bf215546Sopenharmony_ci 311bf215546Sopenharmony_ci shortlog, bugs = await asyncio.gather( 312bf215546Sopenharmony_ci get_shortlog(previous_version), 313bf215546Sopenharmony_ci gather_bugs(previous_version), 314bf215546Sopenharmony_ci ) 315bf215546Sopenharmony_ci 316bf215546Sopenharmony_ci final = pathlib.Path(__file__).parent.parent / 'docs' / 'relnotes' / f'{this_version}.rst' 317bf215546Sopenharmony_ci with final.open('wt') as f: 318bf215546Sopenharmony_ci try: 319bf215546Sopenharmony_ci f.write(TEMPLATE.render( 320bf215546Sopenharmony_ci bugfix=is_point_release, 321bf215546Sopenharmony_ci bugs=bugs, 322bf215546Sopenharmony_ci changes=walk_shortlog(shortlog), 323bf215546Sopenharmony_ci features=get_features(is_point_release), 324bf215546Sopenharmony_ci gl_version=CURRENT_GL_VERSION, 325bf215546Sopenharmony_ci this_version=this_version, 326bf215546Sopenharmony_ci header=header, 327bf215546Sopenharmony_ci header_underline=header_underline, 328bf215546Sopenharmony_ci previous_version=previous_version, 329bf215546Sopenharmony_ci vk_version=CURRENT_VK_VERSION, 330bf215546Sopenharmony_ci rst_escape=inliner.quoteInline, 331bf215546Sopenharmony_ci )) 332bf215546Sopenharmony_ci except: 333bf215546Sopenharmony_ci print(exceptions.text_error_template().render()) 334bf215546Sopenharmony_ci 335bf215546Sopenharmony_ci subprocess.run(['git', 'add', final]) 336bf215546Sopenharmony_ci subprocess.run(['git', 'commit', '-m', 337bf215546Sopenharmony_ci f'docs: add release notes for {this_version}']) 338bf215546Sopenharmony_ci 339bf215546Sopenharmony_ci 340bf215546Sopenharmony_ciif __name__ == "__main__": 341bf215546Sopenharmony_ci loop = asyncio.get_event_loop() 342bf215546Sopenharmony_ci loop.run_until_complete(main()) 343