1bf215546Sopenharmony_ci# Copyright © 2019-2020 Intel Corporation
2bf215546Sopenharmony_ci
3bf215546Sopenharmony_ci# Permission is hereby granted, free of charge, to any person obtaining a copy
4bf215546Sopenharmony_ci# of this software and associated documentation files (the "Software"), to deal
5bf215546Sopenharmony_ci# in the Software without restriction, including without limitation the rights
6bf215546Sopenharmony_ci# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7bf215546Sopenharmony_ci# copies of the Software, and to permit persons to whom the Software is
8bf215546Sopenharmony_ci# furnished to do so, subject to the following conditions:
9bf215546Sopenharmony_ci
10bf215546Sopenharmony_ci# The above copyright notice and this permission notice shall be included in
11bf215546Sopenharmony_ci# all copies or substantial portions of the Software.
12bf215546Sopenharmony_ci
13bf215546Sopenharmony_ci# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14bf215546Sopenharmony_ci# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15bf215546Sopenharmony_ci# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16bf215546Sopenharmony_ci# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17bf215546Sopenharmony_ci# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18bf215546Sopenharmony_ci# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19bf215546Sopenharmony_ci# SOFTWARE.
20bf215546Sopenharmony_ci
21bf215546Sopenharmony_ci"""Core data structures and routines for pick."""
22bf215546Sopenharmony_ci
23bf215546Sopenharmony_ciimport asyncio
24bf215546Sopenharmony_ciimport enum
25bf215546Sopenharmony_ciimport json
26bf215546Sopenharmony_ciimport pathlib
27bf215546Sopenharmony_ciimport re
28bf215546Sopenharmony_ciimport subprocess
29bf215546Sopenharmony_ciimport typing
30bf215546Sopenharmony_ci
31bf215546Sopenharmony_ciimport attr
32bf215546Sopenharmony_ci
33bf215546Sopenharmony_ciif typing.TYPE_CHECKING:
34bf215546Sopenharmony_ci    from .ui import UI
35bf215546Sopenharmony_ci
36bf215546Sopenharmony_ci    import typing_extensions
37bf215546Sopenharmony_ci
38bf215546Sopenharmony_ci    class CommitDict(typing_extensions.TypedDict):
39bf215546Sopenharmony_ci
40bf215546Sopenharmony_ci        sha: str
41bf215546Sopenharmony_ci        description: str
42bf215546Sopenharmony_ci        nominated: bool
43bf215546Sopenharmony_ci        nomination_type: typing.Optional[int]
44bf215546Sopenharmony_ci        resolution: typing.Optional[int]
45bf215546Sopenharmony_ci        main_sha: typing.Optional[str]
46bf215546Sopenharmony_ci        because_sha: typing.Optional[str]
47bf215546Sopenharmony_ci
48bf215546Sopenharmony_ciIS_FIX = re.compile(r'^\s*fixes:\s*([a-f0-9]{6,40})', flags=re.MULTILINE | re.IGNORECASE)
49bf215546Sopenharmony_ci# FIXME: I dislike the duplication in this regex, but I couldn't get it to work otherwise
50bf215546Sopenharmony_ciIS_CC = re.compile(r'^\s*cc:\s*["\']?([0-9]{2}\.[0-9])?["\']?\s*["\']?([0-9]{2}\.[0-9])?["\']?\s*\<?mesa-stable',
51bf215546Sopenharmony_ci                   flags=re.MULTILINE | re.IGNORECASE)
52bf215546Sopenharmony_ciIS_REVERT = re.compile(r'This reverts commit ([0-9a-f]{40})')
53bf215546Sopenharmony_ci
54bf215546Sopenharmony_ci# XXX: hack
55bf215546Sopenharmony_ciSEM = asyncio.Semaphore(50)
56bf215546Sopenharmony_ci
57bf215546Sopenharmony_ciCOMMIT_LOCK = asyncio.Lock()
58bf215546Sopenharmony_ci
59bf215546Sopenharmony_cigit_toplevel = subprocess.check_output(['git', 'rev-parse', '--show-toplevel'],
60bf215546Sopenharmony_ci                                       stderr=subprocess.DEVNULL).decode("ascii").strip()
61bf215546Sopenharmony_cipick_status_json = pathlib.Path(git_toplevel) / '.pick_status.json'
62bf215546Sopenharmony_ci
63bf215546Sopenharmony_ci
64bf215546Sopenharmony_ciclass PickUIException(Exception):
65bf215546Sopenharmony_ci    pass
66bf215546Sopenharmony_ci
67bf215546Sopenharmony_ci
68bf215546Sopenharmony_ci@enum.unique
69bf215546Sopenharmony_ciclass NominationType(enum.Enum):
70bf215546Sopenharmony_ci
71bf215546Sopenharmony_ci    CC = 0
72bf215546Sopenharmony_ci    FIXES = 1
73bf215546Sopenharmony_ci    REVERT = 2
74bf215546Sopenharmony_ci
75bf215546Sopenharmony_ci
76bf215546Sopenharmony_ci@enum.unique
77bf215546Sopenharmony_ciclass Resolution(enum.Enum):
78bf215546Sopenharmony_ci
79bf215546Sopenharmony_ci    UNRESOLVED = 0
80bf215546Sopenharmony_ci    MERGED = 1
81bf215546Sopenharmony_ci    DENOMINATED = 2
82bf215546Sopenharmony_ci    BACKPORTED = 3
83bf215546Sopenharmony_ci    NOTNEEDED = 4
84bf215546Sopenharmony_ci
85bf215546Sopenharmony_ci
86bf215546Sopenharmony_ciasync def commit_state(*, amend: bool = False, message: str = 'Update') -> bool:
87bf215546Sopenharmony_ci    """Commit the .pick_status.json file."""
88bf215546Sopenharmony_ci    async with COMMIT_LOCK:
89bf215546Sopenharmony_ci        p = await asyncio.create_subprocess_exec(
90bf215546Sopenharmony_ci            'git', 'add', pick_status_json.as_posix(),
91bf215546Sopenharmony_ci            stdout=asyncio.subprocess.DEVNULL,
92bf215546Sopenharmony_ci            stderr=asyncio.subprocess.DEVNULL,
93bf215546Sopenharmony_ci        )
94bf215546Sopenharmony_ci        v = await p.wait()
95bf215546Sopenharmony_ci        if v != 0:
96bf215546Sopenharmony_ci            return False
97bf215546Sopenharmony_ci
98bf215546Sopenharmony_ci        if amend:
99bf215546Sopenharmony_ci            cmd = ['--amend', '--no-edit']
100bf215546Sopenharmony_ci        else:
101bf215546Sopenharmony_ci            cmd = ['--message', f'.pick_status.json: {message}']
102bf215546Sopenharmony_ci        p = await asyncio.create_subprocess_exec(
103bf215546Sopenharmony_ci            'git', 'commit', *cmd,
104bf215546Sopenharmony_ci            stdout=asyncio.subprocess.DEVNULL,
105bf215546Sopenharmony_ci            stderr=asyncio.subprocess.DEVNULL,
106bf215546Sopenharmony_ci        )
107bf215546Sopenharmony_ci        v = await p.wait()
108bf215546Sopenharmony_ci        if v != 0:
109bf215546Sopenharmony_ci            return False
110bf215546Sopenharmony_ci    return True
111bf215546Sopenharmony_ci
112bf215546Sopenharmony_ci
113bf215546Sopenharmony_ci@attr.s(slots=True)
114bf215546Sopenharmony_ciclass Commit:
115bf215546Sopenharmony_ci
116bf215546Sopenharmony_ci    sha: str = attr.ib()
117bf215546Sopenharmony_ci    description: str = attr.ib()
118bf215546Sopenharmony_ci    nominated: bool = attr.ib(False)
119bf215546Sopenharmony_ci    nomination_type: typing.Optional[NominationType] = attr.ib(None)
120bf215546Sopenharmony_ci    resolution: Resolution = attr.ib(Resolution.UNRESOLVED)
121bf215546Sopenharmony_ci    main_sha: typing.Optional[str] = attr.ib(None)
122bf215546Sopenharmony_ci    because_sha: typing.Optional[str] = attr.ib(None)
123bf215546Sopenharmony_ci
124bf215546Sopenharmony_ci    def to_json(self) -> 'CommitDict':
125bf215546Sopenharmony_ci        d: typing.Dict[str, typing.Any] = attr.asdict(self)
126bf215546Sopenharmony_ci        if self.nomination_type is not None:
127bf215546Sopenharmony_ci            d['nomination_type'] = self.nomination_type.value
128bf215546Sopenharmony_ci        if self.resolution is not None:
129bf215546Sopenharmony_ci            d['resolution'] = self.resolution.value
130bf215546Sopenharmony_ci        return typing.cast('CommitDict', d)
131bf215546Sopenharmony_ci
132bf215546Sopenharmony_ci    @classmethod
133bf215546Sopenharmony_ci    def from_json(cls, data: 'CommitDict') -> 'Commit':
134bf215546Sopenharmony_ci        c = cls(data['sha'], data['description'], data['nominated'], main_sha=data['main_sha'], because_sha=data['because_sha'])
135bf215546Sopenharmony_ci        if data['nomination_type'] is not None:
136bf215546Sopenharmony_ci            c.nomination_type = NominationType(data['nomination_type'])
137bf215546Sopenharmony_ci        if data['resolution'] is not None:
138bf215546Sopenharmony_ci            c.resolution = Resolution(data['resolution'])
139bf215546Sopenharmony_ci        return c
140bf215546Sopenharmony_ci
141bf215546Sopenharmony_ci    def date(self) -> str:
142bf215546Sopenharmony_ci        # Show commit date, ie. when the commit actually landed
143bf215546Sopenharmony_ci        # (as opposed to when it was first written)
144bf215546Sopenharmony_ci        return subprocess.check_output(
145bf215546Sopenharmony_ci            ['git', 'show', '--no-patch', '--format=%cs', self.sha],
146bf215546Sopenharmony_ci            stderr=subprocess.DEVNULL
147bf215546Sopenharmony_ci        ).decode("ascii").strip()
148bf215546Sopenharmony_ci
149bf215546Sopenharmony_ci    async def apply(self, ui: 'UI') -> typing.Tuple[bool, str]:
150bf215546Sopenharmony_ci        # FIXME: This isn't really enough if we fail to cherry-pick because the
151bf215546Sopenharmony_ci        # git tree will still be dirty
152bf215546Sopenharmony_ci        async with COMMIT_LOCK:
153bf215546Sopenharmony_ci            p = await asyncio.create_subprocess_exec(
154bf215546Sopenharmony_ci                'git', 'cherry-pick', '-x', self.sha,
155bf215546Sopenharmony_ci                stdout=asyncio.subprocess.DEVNULL,
156bf215546Sopenharmony_ci                stderr=asyncio.subprocess.PIPE,
157bf215546Sopenharmony_ci            )
158bf215546Sopenharmony_ci            _, err = await p.communicate()
159bf215546Sopenharmony_ci
160bf215546Sopenharmony_ci        if p.returncode != 0:
161bf215546Sopenharmony_ci            return (False, err.decode())
162bf215546Sopenharmony_ci
163bf215546Sopenharmony_ci        self.resolution = Resolution.MERGED
164bf215546Sopenharmony_ci        await ui.feedback(f'{self.sha} ({self.description}) applied successfully')
165bf215546Sopenharmony_ci
166bf215546Sopenharmony_ci        # Append the changes to the .pickstatus.json file
167bf215546Sopenharmony_ci        ui.save()
168bf215546Sopenharmony_ci        v = await commit_state(amend=True)
169bf215546Sopenharmony_ci        return (v, '')
170bf215546Sopenharmony_ci
171bf215546Sopenharmony_ci    async def abort_cherry(self, ui: 'UI', err: str) -> None:
172bf215546Sopenharmony_ci        await ui.feedback(f'{self.sha} ({self.description}) failed to apply\n{err}')
173bf215546Sopenharmony_ci        async with COMMIT_LOCK:
174bf215546Sopenharmony_ci            p = await asyncio.create_subprocess_exec(
175bf215546Sopenharmony_ci                'git', 'cherry-pick', '--abort',
176bf215546Sopenharmony_ci                stdout=asyncio.subprocess.DEVNULL,
177bf215546Sopenharmony_ci                stderr=asyncio.subprocess.DEVNULL,
178bf215546Sopenharmony_ci            )
179bf215546Sopenharmony_ci            r = await p.wait()
180bf215546Sopenharmony_ci        await ui.feedback(f'{"Successfully" if r == 0 else "Failed to"} abort cherry-pick.')
181bf215546Sopenharmony_ci
182bf215546Sopenharmony_ci    async def denominate(self, ui: 'UI') -> bool:
183bf215546Sopenharmony_ci        self.resolution = Resolution.DENOMINATED
184bf215546Sopenharmony_ci        ui.save()
185bf215546Sopenharmony_ci        v = await commit_state(message=f'Mark {self.sha} as denominated')
186bf215546Sopenharmony_ci        assert v
187bf215546Sopenharmony_ci        await ui.feedback(f'{self.sha} ({self.description}) denominated successfully')
188bf215546Sopenharmony_ci        return True
189bf215546Sopenharmony_ci
190bf215546Sopenharmony_ci    async def backport(self, ui: 'UI') -> bool:
191bf215546Sopenharmony_ci        self.resolution = Resolution.BACKPORTED
192bf215546Sopenharmony_ci        ui.save()
193bf215546Sopenharmony_ci        v = await commit_state(message=f'Mark {self.sha} as backported')
194bf215546Sopenharmony_ci        assert v
195bf215546Sopenharmony_ci        await ui.feedback(f'{self.sha} ({self.description}) backported successfully')
196bf215546Sopenharmony_ci        return True
197bf215546Sopenharmony_ci
198bf215546Sopenharmony_ci    async def resolve(self, ui: 'UI') -> None:
199bf215546Sopenharmony_ci        self.resolution = Resolution.MERGED
200bf215546Sopenharmony_ci        ui.save()
201bf215546Sopenharmony_ci        v = await commit_state(amend=True)
202bf215546Sopenharmony_ci        assert v
203bf215546Sopenharmony_ci        await ui.feedback(f'{self.sha} ({self.description}) committed successfully')
204bf215546Sopenharmony_ci
205bf215546Sopenharmony_ci
206bf215546Sopenharmony_ciasync def get_new_commits(sha: str) -> typing.List[typing.Tuple[str, str]]:
207bf215546Sopenharmony_ci    # Try to get the authoritative upstream main
208bf215546Sopenharmony_ci    p = await asyncio.create_subprocess_exec(
209bf215546Sopenharmony_ci        'git', 'for-each-ref', '--format=%(upstream)', 'refs/heads/main',
210bf215546Sopenharmony_ci        stdout=asyncio.subprocess.PIPE,
211bf215546Sopenharmony_ci        stderr=asyncio.subprocess.DEVNULL)
212bf215546Sopenharmony_ci    out, _ = await p.communicate()
213bf215546Sopenharmony_ci    upstream = out.decode().strip()
214bf215546Sopenharmony_ci
215bf215546Sopenharmony_ci    p = await asyncio.create_subprocess_exec(
216bf215546Sopenharmony_ci        'git', 'log', '--pretty=oneline', f'{sha}..{upstream}',
217bf215546Sopenharmony_ci        stdout=asyncio.subprocess.PIPE,
218bf215546Sopenharmony_ci        stderr=asyncio.subprocess.DEVNULL)
219bf215546Sopenharmony_ci    out, _ = await p.communicate()
220bf215546Sopenharmony_ci    assert p.returncode == 0, f"git log didn't work: {sha}"
221bf215546Sopenharmony_ci    return list(split_commit_list(out.decode().strip()))
222bf215546Sopenharmony_ci
223bf215546Sopenharmony_ci
224bf215546Sopenharmony_cidef split_commit_list(commits: str) -> typing.Generator[typing.Tuple[str, str], None, None]:
225bf215546Sopenharmony_ci    if not commits:
226bf215546Sopenharmony_ci        return
227bf215546Sopenharmony_ci    for line in commits.split('\n'):
228bf215546Sopenharmony_ci        v = tuple(line.split(' ', 1))
229bf215546Sopenharmony_ci        assert len(v) == 2, 'this is really just for mypy'
230bf215546Sopenharmony_ci        yield typing.cast(typing.Tuple[str, str], v)
231bf215546Sopenharmony_ci
232bf215546Sopenharmony_ci
233bf215546Sopenharmony_ciasync def is_commit_in_branch(sha: str) -> bool:
234bf215546Sopenharmony_ci    async with SEM:
235bf215546Sopenharmony_ci        p = await asyncio.create_subprocess_exec(
236bf215546Sopenharmony_ci            'git', 'merge-base', '--is-ancestor', sha, 'HEAD',
237bf215546Sopenharmony_ci            stdout=asyncio.subprocess.DEVNULL,
238bf215546Sopenharmony_ci            stderr=asyncio.subprocess.DEVNULL,
239bf215546Sopenharmony_ci        )
240bf215546Sopenharmony_ci        await p.wait()
241bf215546Sopenharmony_ci    return p.returncode == 0
242bf215546Sopenharmony_ci
243bf215546Sopenharmony_ci
244bf215546Sopenharmony_ciasync def full_sha(sha: str) -> str:
245bf215546Sopenharmony_ci    async with SEM:
246bf215546Sopenharmony_ci        p = await asyncio.create_subprocess_exec(
247bf215546Sopenharmony_ci            'git', 'rev-parse', sha,
248bf215546Sopenharmony_ci            stdout=asyncio.subprocess.PIPE,
249bf215546Sopenharmony_ci            stderr=asyncio.subprocess.DEVNULL,
250bf215546Sopenharmony_ci        )
251bf215546Sopenharmony_ci        out, _ = await p.communicate()
252bf215546Sopenharmony_ci    if p.returncode:
253bf215546Sopenharmony_ci        raise PickUIException(f'Invalid Sha {sha}')
254bf215546Sopenharmony_ci    return out.decode().strip()
255bf215546Sopenharmony_ci
256bf215546Sopenharmony_ci
257bf215546Sopenharmony_ciasync def resolve_nomination(commit: 'Commit', version: str) -> 'Commit':
258bf215546Sopenharmony_ci    async with SEM:
259bf215546Sopenharmony_ci        p = await asyncio.create_subprocess_exec(
260bf215546Sopenharmony_ci            'git', 'log', '--format=%B', '-1', commit.sha,
261bf215546Sopenharmony_ci            stdout=asyncio.subprocess.PIPE,
262bf215546Sopenharmony_ci            stderr=asyncio.subprocess.DEVNULL,
263bf215546Sopenharmony_ci        )
264bf215546Sopenharmony_ci        _out, _ = await p.communicate()
265bf215546Sopenharmony_ci        assert p.returncode == 0, f'git log for {commit.sha} failed'
266bf215546Sopenharmony_ci    out = _out.decode()
267bf215546Sopenharmony_ci
268bf215546Sopenharmony_ci    # We give precedence to fixes and cc tags over revert tags.
269bf215546Sopenharmony_ci    # XXX: not having the walrus operator available makes me sad :=
270bf215546Sopenharmony_ci    m = IS_FIX.search(out)
271bf215546Sopenharmony_ci    if m:
272bf215546Sopenharmony_ci        # We set the nomination_type and because_sha here so that we can later
273bf215546Sopenharmony_ci        # check to see if this fixes another staged commit.
274bf215546Sopenharmony_ci        try:
275bf215546Sopenharmony_ci            commit.because_sha = fixed = await full_sha(m.group(1))
276bf215546Sopenharmony_ci        except PickUIException:
277bf215546Sopenharmony_ci            pass
278bf215546Sopenharmony_ci        else:
279bf215546Sopenharmony_ci            commit.nomination_type = NominationType.FIXES
280bf215546Sopenharmony_ci            if await is_commit_in_branch(fixed):
281bf215546Sopenharmony_ci                commit.nominated = True
282bf215546Sopenharmony_ci                return commit
283bf215546Sopenharmony_ci
284bf215546Sopenharmony_ci    m = IS_CC.search(out)
285bf215546Sopenharmony_ci    if m:
286bf215546Sopenharmony_ci        if m.groups() == (None, None) or version in m.groups():
287bf215546Sopenharmony_ci            commit.nominated = True
288bf215546Sopenharmony_ci            commit.nomination_type = NominationType.CC
289bf215546Sopenharmony_ci            return commit
290bf215546Sopenharmony_ci
291bf215546Sopenharmony_ci    m = IS_REVERT.search(out)
292bf215546Sopenharmony_ci    if m:
293bf215546Sopenharmony_ci        # See comment for IS_FIX path
294bf215546Sopenharmony_ci        try:
295bf215546Sopenharmony_ci            commit.because_sha = reverted = await full_sha(m.group(1))
296bf215546Sopenharmony_ci        except PickUIException:
297bf215546Sopenharmony_ci            pass
298bf215546Sopenharmony_ci        else:
299bf215546Sopenharmony_ci            commit.nomination_type = NominationType.REVERT
300bf215546Sopenharmony_ci            if await is_commit_in_branch(reverted):
301bf215546Sopenharmony_ci                commit.nominated = True
302bf215546Sopenharmony_ci                return commit
303bf215546Sopenharmony_ci
304bf215546Sopenharmony_ci    return commit
305bf215546Sopenharmony_ci
306bf215546Sopenharmony_ci
307bf215546Sopenharmony_ciasync def resolve_fixes(commits: typing.List['Commit'], previous: typing.List['Commit']) -> None:
308bf215546Sopenharmony_ci    """Determine if any of the undecided commits fix/revert a staged commit.
309bf215546Sopenharmony_ci
310bf215546Sopenharmony_ci    The are still needed if they apply to a commit that is staged for
311bf215546Sopenharmony_ci    inclusion, but not yet included.
312bf215546Sopenharmony_ci
313bf215546Sopenharmony_ci    This must be done in order, because a commit 3 might fix commit 2 which
314bf215546Sopenharmony_ci    fixes commit 1.
315bf215546Sopenharmony_ci    """
316bf215546Sopenharmony_ci    shas: typing.Set[str] = set(c.sha for c in previous if c.nominated)
317bf215546Sopenharmony_ci    assert None not in shas, 'None in shas'
318bf215546Sopenharmony_ci
319bf215546Sopenharmony_ci    for commit in reversed(commits):
320bf215546Sopenharmony_ci        if not commit.nominated and commit.nomination_type is NominationType.FIXES:
321bf215546Sopenharmony_ci            commit.nominated = commit.because_sha in shas
322bf215546Sopenharmony_ci
323bf215546Sopenharmony_ci        if commit.nominated:
324bf215546Sopenharmony_ci            shas.add(commit.sha)
325bf215546Sopenharmony_ci
326bf215546Sopenharmony_ci    for commit in commits:
327bf215546Sopenharmony_ci        if (commit.nomination_type is NominationType.REVERT and
328bf215546Sopenharmony_ci                commit.because_sha in shas):
329bf215546Sopenharmony_ci            for oldc in reversed(commits):
330bf215546Sopenharmony_ci                if oldc.sha == commit.because_sha:
331bf215546Sopenharmony_ci                    # In this case a commit that hasn't yet been applied is
332bf215546Sopenharmony_ci                    # reverted, we don't want to apply that commit at all
333bf215546Sopenharmony_ci                    oldc.nominated = False
334bf215546Sopenharmony_ci                    oldc.resolution = Resolution.DENOMINATED
335bf215546Sopenharmony_ci                    commit.nominated = False
336bf215546Sopenharmony_ci                    commit.resolution = Resolution.DENOMINATED
337bf215546Sopenharmony_ci                    shas.remove(commit.because_sha)
338bf215546Sopenharmony_ci                    break
339bf215546Sopenharmony_ci
340bf215546Sopenharmony_ci
341bf215546Sopenharmony_ciasync def gather_commits(version: str, previous: typing.List['Commit'],
342bf215546Sopenharmony_ci                         new: typing.List[typing.Tuple[str, str]], cb) -> typing.List['Commit']:
343bf215546Sopenharmony_ci    # We create an array of the final size up front, then we pass that array
344bf215546Sopenharmony_ci    # to the "inner" co-routine, which is turned into a list of tasks and
345bf215546Sopenharmony_ci    # collected by asyncio.gather. We do this to allow the tasks to be
346bf215546Sopenharmony_ci    # asynchronously gathered, but to also ensure that the commits list remains
347bf215546Sopenharmony_ci    # in order.
348bf215546Sopenharmony_ci    m_commits: typing.List[typing.Optional['Commit']] = [None] * len(new)
349bf215546Sopenharmony_ci    tasks = []
350bf215546Sopenharmony_ci
351bf215546Sopenharmony_ci    async def inner(commit: 'Commit', version: str,
352bf215546Sopenharmony_ci                    commits: typing.List[typing.Optional['Commit']],
353bf215546Sopenharmony_ci                    index: int, cb) -> None:
354bf215546Sopenharmony_ci        commits[index] = await resolve_nomination(commit, version)
355bf215546Sopenharmony_ci        cb()
356bf215546Sopenharmony_ci
357bf215546Sopenharmony_ci    for i, (sha, desc) in enumerate(new):
358bf215546Sopenharmony_ci        tasks.append(asyncio.ensure_future(
359bf215546Sopenharmony_ci            inner(Commit(sha, desc), version, m_commits, i, cb)))
360bf215546Sopenharmony_ci
361bf215546Sopenharmony_ci    await asyncio.gather(*tasks)
362bf215546Sopenharmony_ci    assert None not in m_commits
363bf215546Sopenharmony_ci    commits = typing.cast(typing.List[Commit], m_commits)
364bf215546Sopenharmony_ci
365bf215546Sopenharmony_ci    await resolve_fixes(commits, previous)
366bf215546Sopenharmony_ci
367bf215546Sopenharmony_ci    for commit in commits:
368bf215546Sopenharmony_ci        if commit.resolution is Resolution.UNRESOLVED and not commit.nominated:
369bf215546Sopenharmony_ci            commit.resolution = Resolution.NOTNEEDED
370bf215546Sopenharmony_ci
371bf215546Sopenharmony_ci    return commits
372bf215546Sopenharmony_ci
373bf215546Sopenharmony_ci
374bf215546Sopenharmony_cidef load() -> typing.List['Commit']:
375bf215546Sopenharmony_ci    if not pick_status_json.exists():
376bf215546Sopenharmony_ci        return []
377bf215546Sopenharmony_ci    with pick_status_json.open('r') as f:
378bf215546Sopenharmony_ci        raw = json.load(f)
379bf215546Sopenharmony_ci        return [Commit.from_json(c) for c in raw]
380bf215546Sopenharmony_ci
381bf215546Sopenharmony_ci
382bf215546Sopenharmony_cidef save(commits: typing.Iterable['Commit']) -> None:
383bf215546Sopenharmony_ci    commits = list(commits)
384bf215546Sopenharmony_ci    with pick_status_json.open('wt') as f:
385bf215546Sopenharmony_ci        json.dump([c.to_json() for c in commits], f, indent=4)
386bf215546Sopenharmony_ci
387bf215546Sopenharmony_ci    asyncio.ensure_future(commit_state(message=f'Update to {commits[0].sha}'))
388