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