17ec681f3Smrg# Copyright © 2019-2020 Intel Corporation 27ec681f3Smrg 37ec681f3Smrg# Permission is hereby granted, free of charge, to any person obtaining a copy 47ec681f3Smrg# of this software and associated documentation files (the "Software"), to deal 57ec681f3Smrg# in the Software without restriction, including without limitation the rights 67ec681f3Smrg# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 77ec681f3Smrg# copies of the Software, and to permit persons to whom the Software is 87ec681f3Smrg# furnished to do so, subject to the following conditions: 97ec681f3Smrg 107ec681f3Smrg# The above copyright notice and this permission notice shall be included in 117ec681f3Smrg# all copies or substantial portions of the Software. 127ec681f3Smrg 137ec681f3Smrg# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 147ec681f3Smrg# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 157ec681f3Smrg# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 167ec681f3Smrg# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 177ec681f3Smrg# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 187ec681f3Smrg# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 197ec681f3Smrg# SOFTWARE. 207ec681f3Smrg 217ec681f3Smrg"""Urwid UI for pick script.""" 227ec681f3Smrg 237ec681f3Smrgimport asyncio 247ec681f3Smrgimport itertools 257ec681f3Smrgimport textwrap 267ec681f3Smrgimport typing 277ec681f3Smrg 287ec681f3Smrgimport attr 297ec681f3Smrgimport urwid 307ec681f3Smrg 317ec681f3Smrgfrom . import core 327ec681f3Smrg 337ec681f3Smrgif typing.TYPE_CHECKING: 347ec681f3Smrg WidgetType = typing.TypeVar('WidgetType', bound=urwid.Widget) 357ec681f3Smrg 367ec681f3SmrgPALETTE = [ 377ec681f3Smrg ('a', 'black', 'light gray'), 387ec681f3Smrg ('b', 'black', 'dark red'), 397ec681f3Smrg ('bg', 'black', 'dark blue'), 407ec681f3Smrg ('reversed', 'standout', ''), 417ec681f3Smrg] 427ec681f3Smrg 437ec681f3Smrg 447ec681f3Smrgclass RootWidget(urwid.Frame): 457ec681f3Smrg 467ec681f3Smrg def __init__(self, *args, ui: 'UI', **kwargs): 477ec681f3Smrg super().__init__(*args, **kwargs) 487ec681f3Smrg self.ui = ui 497ec681f3Smrg 507ec681f3Smrg def keypress(self, size: int, key: str) -> typing.Optional[str]: 517ec681f3Smrg if key == 'q': 527ec681f3Smrg raise urwid.ExitMainLoop() 537ec681f3Smrg elif key == 'u': 547ec681f3Smrg asyncio.ensure_future(self.ui.update()) 557ec681f3Smrg elif key == 'a': 567ec681f3Smrg self.ui.add() 577ec681f3Smrg else: 587ec681f3Smrg return super().keypress(size, key) 597ec681f3Smrg return None 607ec681f3Smrg 617ec681f3Smrg 627ec681f3Smrgclass CommitWidget(urwid.Text): 637ec681f3Smrg 647ec681f3Smrg # urwid.Text is normally not interactable, this is required to tell urwid 657ec681f3Smrg # to use our keypress method 667ec681f3Smrg _selectable = True 677ec681f3Smrg 687ec681f3Smrg def __init__(self, ui: 'UI', commit: 'core.Commit'): 697ec681f3Smrg reason = commit.nomination_type.name.ljust(6) 707ec681f3Smrg super().__init__(f'{commit.date()} {reason} {commit.sha[:10]} {commit.description}') 717ec681f3Smrg self.ui = ui 727ec681f3Smrg self.commit = commit 737ec681f3Smrg 747ec681f3Smrg async def apply(self) -> None: 757ec681f3Smrg async with self.ui.git_lock: 767ec681f3Smrg result, err = await self.commit.apply(self.ui) 777ec681f3Smrg if not result: 787ec681f3Smrg self.ui.chp_failed(self, err) 797ec681f3Smrg else: 807ec681f3Smrg self.ui.remove_commit(self) 817ec681f3Smrg 827ec681f3Smrg async def denominate(self) -> None: 837ec681f3Smrg async with self.ui.git_lock: 847ec681f3Smrg await self.commit.denominate(self.ui) 857ec681f3Smrg self.ui.remove_commit(self) 867ec681f3Smrg 877ec681f3Smrg async def backport(self) -> None: 887ec681f3Smrg async with self.ui.git_lock: 897ec681f3Smrg await self.commit.backport(self.ui) 907ec681f3Smrg self.ui.remove_commit(self) 917ec681f3Smrg 927ec681f3Smrg def keypress(self, size: int, key: str) -> typing.Optional[str]: 937ec681f3Smrg if key == 'c': 947ec681f3Smrg asyncio.ensure_future(self.apply()) 957ec681f3Smrg elif key == 'd': 967ec681f3Smrg asyncio.ensure_future(self.denominate()) 977ec681f3Smrg elif key == 'b': 987ec681f3Smrg asyncio.ensure_future(self.backport()) 997ec681f3Smrg else: 1007ec681f3Smrg return key 1017ec681f3Smrg return None 1027ec681f3Smrg 1037ec681f3Smrg 1047ec681f3Smrg@attr.s(slots=True) 1057ec681f3Smrgclass UI: 1067ec681f3Smrg 1077ec681f3Smrg """Main management object. 1087ec681f3Smrg 1097ec681f3Smrg :previous_commits: A list of commits to main since this branch was created 1107ec681f3Smrg :new_commits: Commits added to main since the last time this script was run 1117ec681f3Smrg """ 1127ec681f3Smrg 1137ec681f3Smrg commit_list: typing.List['urwid.Button'] = attr.ib(factory=lambda: urwid.SimpleFocusListWalker([]), init=False) 1147ec681f3Smrg feedback_box: typing.List['urwid.Text'] = attr.ib(factory=lambda: urwid.SimpleFocusListWalker([]), init=False) 1157ec681f3Smrg header: 'urwid.Text' = attr.ib(factory=lambda: urwid.Text('Mesa Stable Picker', align='center'), init=False) 1167ec681f3Smrg body: 'urwid.Columns' = attr.ib(attr.Factory(lambda s: s._make_body(), True), init=False) 1177ec681f3Smrg footer: 'urwid.Columns' = attr.ib(attr.Factory(lambda s: s._make_footer(), True), init=False) 1187ec681f3Smrg root: RootWidget = attr.ib(attr.Factory(lambda s: s._make_root(), True), init=False) 1197ec681f3Smrg mainloop: urwid.MainLoop = attr.ib(None, init=False) 1207ec681f3Smrg 1217ec681f3Smrg previous_commits: typing.List['core.Commit'] = attr.ib(factory=list, init=False) 1227ec681f3Smrg new_commits: typing.List['core.Commit'] = attr.ib(factory=list, init=False) 1237ec681f3Smrg git_lock: asyncio.Lock = attr.ib(factory=asyncio.Lock, init=False) 1247ec681f3Smrg 1257ec681f3Smrg def _make_body(self) -> 'urwid.Columns': 1267ec681f3Smrg commits = urwid.ListBox(self.commit_list) 1277ec681f3Smrg feedback = urwid.ListBox(self.feedback_box) 1287ec681f3Smrg return urwid.Columns([commits, feedback]) 1297ec681f3Smrg 1307ec681f3Smrg def _make_footer(self) -> 'urwid.Columns': 1317ec681f3Smrg body = [ 1327ec681f3Smrg urwid.Text('[U]pdate'), 1337ec681f3Smrg urwid.Text('[Q]uit'), 1347ec681f3Smrg urwid.Text('[C]herry Pick'), 1357ec681f3Smrg urwid.Text('[D]enominate'), 1367ec681f3Smrg urwid.Text('[B]ackport'), 1377ec681f3Smrg urwid.Text('[A]pply additional patch') 1387ec681f3Smrg ] 1397ec681f3Smrg return urwid.Columns(body) 1407ec681f3Smrg 1417ec681f3Smrg def _make_root(self) -> 'RootWidget': 1427ec681f3Smrg return RootWidget(self.body, self.header, self.footer, 'body', ui=self) 1437ec681f3Smrg 1447ec681f3Smrg def render(self) -> 'WidgetType': 1457ec681f3Smrg asyncio.ensure_future(self.update()) 1467ec681f3Smrg return self.root 1477ec681f3Smrg 1487ec681f3Smrg def load(self) -> None: 1497ec681f3Smrg self.previous_commits = core.load() 1507ec681f3Smrg 1517ec681f3Smrg async def update(self) -> None: 1527ec681f3Smrg self.load() 1537ec681f3Smrg with open('VERSION', 'r') as f: 1547ec681f3Smrg version = '.'.join(f.read().split('.')[:2]) 1557ec681f3Smrg if self.previous_commits: 1567ec681f3Smrg sha = self.previous_commits[0].sha 1577ec681f3Smrg else: 1587ec681f3Smrg sha = f'{version}-branchpoint' 1597ec681f3Smrg 1607ec681f3Smrg new_commits = await core.get_new_commits(sha) 1617ec681f3Smrg 1627ec681f3Smrg if new_commits: 1637ec681f3Smrg pb = urwid.ProgressBar('a', 'b', done=len(new_commits)) 1647ec681f3Smrg o = self.mainloop.widget 1657ec681f3Smrg self.mainloop.widget = urwid.Overlay( 1667ec681f3Smrg urwid.Filler(urwid.LineBox(pb)), o, 'center', ('relative', 50), 'middle', ('relative', 50)) 1677ec681f3Smrg self.new_commits = await core.gather_commits( 1687ec681f3Smrg version, self.previous_commits, new_commits, 1697ec681f3Smrg lambda: pb.set_completion(pb.current + 1)) 1707ec681f3Smrg self.mainloop.widget = o 1717ec681f3Smrg 1727ec681f3Smrg for commit in reversed(list(itertools.chain(self.new_commits, self.previous_commits))): 1737ec681f3Smrg if commit.nominated and commit.resolution is core.Resolution.UNRESOLVED: 1747ec681f3Smrg b = urwid.AttrMap(CommitWidget(self, commit), None, focus_map='reversed') 1757ec681f3Smrg self.commit_list.append(b) 1767ec681f3Smrg self.save() 1777ec681f3Smrg 1787ec681f3Smrg async def feedback(self, text: str) -> None: 1797ec681f3Smrg self.feedback_box.append(urwid.AttrMap(urwid.Text(text), None)) 1807ec681f3Smrg latest_item_index = len(self.feedback_box) - 1 1817ec681f3Smrg self.feedback_box.set_focus(latest_item_index) 1827ec681f3Smrg 1837ec681f3Smrg def remove_commit(self, commit: CommitWidget) -> None: 1847ec681f3Smrg for i, c in enumerate(self.commit_list): 1857ec681f3Smrg if c.base_widget is commit: 1867ec681f3Smrg del self.commit_list[i] 1877ec681f3Smrg break 1887ec681f3Smrg 1897ec681f3Smrg def save(self): 1907ec681f3Smrg core.save(itertools.chain(self.new_commits, self.previous_commits)) 1917ec681f3Smrg 1927ec681f3Smrg def add(self) -> None: 1937ec681f3Smrg """Add an additional commit which isn't nominated.""" 1947ec681f3Smrg o = self.mainloop.widget 1957ec681f3Smrg 1967ec681f3Smrg def reset_cb(_) -> None: 1977ec681f3Smrg self.mainloop.widget = o 1987ec681f3Smrg 1997ec681f3Smrg async def apply_cb(edit: urwid.Edit) -> None: 2007ec681f3Smrg text: str = edit.get_edit_text() 2017ec681f3Smrg 2027ec681f3Smrg # In case the text is empty 2037ec681f3Smrg if not text: 2047ec681f3Smrg return 2057ec681f3Smrg 2067ec681f3Smrg sha = await core.full_sha(text) 2077ec681f3Smrg for c in reversed(list(itertools.chain(self.new_commits, self.previous_commits))): 2087ec681f3Smrg if c.sha == sha: 2097ec681f3Smrg commit = c 2107ec681f3Smrg break 2117ec681f3Smrg else: 2127ec681f3Smrg raise RuntimeError(f"Couldn't find {sha}") 2137ec681f3Smrg 2147ec681f3Smrg await commit.apply(self) 2157ec681f3Smrg 2167ec681f3Smrg q = urwid.Edit("Commit sha\n") 2177ec681f3Smrg ok_btn = urwid.Button('Ok') 2187ec681f3Smrg urwid.connect_signal(ok_btn, 'click', lambda _: asyncio.ensure_future(apply_cb(q))) 2197ec681f3Smrg urwid.connect_signal(ok_btn, 'click', reset_cb) 2207ec681f3Smrg 2217ec681f3Smrg can_btn = urwid.Button('Cancel') 2227ec681f3Smrg urwid.connect_signal(can_btn, 'click', reset_cb) 2237ec681f3Smrg 2247ec681f3Smrg cols = urwid.Columns([ok_btn, can_btn]) 2257ec681f3Smrg pile = urwid.Pile([q, cols]) 2267ec681f3Smrg box = urwid.LineBox(pile) 2277ec681f3Smrg 2287ec681f3Smrg self.mainloop.widget = urwid.Overlay( 2297ec681f3Smrg urwid.Filler(box), o, 'center', ('relative', 50), 'middle', ('relative', 50) 2307ec681f3Smrg ) 2317ec681f3Smrg 2327ec681f3Smrg def chp_failed(self, commit: 'CommitWidget', err: str) -> None: 2337ec681f3Smrg o = self.mainloop.widget 2347ec681f3Smrg 2357ec681f3Smrg def reset_cb(_) -> None: 2367ec681f3Smrg self.mainloop.widget = o 2377ec681f3Smrg 2387ec681f3Smrg t = urwid.Text(textwrap.dedent(f""" 2397ec681f3Smrg Failed to apply {commit.commit.sha} {commit.commit.description} with the following error: 2407ec681f3Smrg 2417ec681f3Smrg {err} 2427ec681f3Smrg 2437ec681f3Smrg You can either cancel, or resolve the conflicts (`git mergetool`), finish the 2447ec681f3Smrg cherry-pick (`git cherry-pick --continue`) and select ok.""")) 2457ec681f3Smrg 2467ec681f3Smrg can_btn = urwid.Button('Cancel') 2477ec681f3Smrg urwid.connect_signal(can_btn, 'click', reset_cb) 2487ec681f3Smrg urwid.connect_signal( 2497ec681f3Smrg can_btn, 'click', lambda _: asyncio.ensure_future(commit.commit.abort_cherry(self, err))) 2507ec681f3Smrg 2517ec681f3Smrg ok_btn = urwid.Button('Ok') 2527ec681f3Smrg urwid.connect_signal(ok_btn, 'click', reset_cb) 2537ec681f3Smrg urwid.connect_signal( 2547ec681f3Smrg ok_btn, 'click', lambda _: asyncio.ensure_future(commit.commit.resolve(self))) 2557ec681f3Smrg urwid.connect_signal( 2567ec681f3Smrg ok_btn, 'click', lambda _: self.remove_commit(commit)) 2577ec681f3Smrg 2587ec681f3Smrg cols = urwid.Columns([ok_btn, can_btn]) 2597ec681f3Smrg pile = urwid.Pile([t, cols]) 2607ec681f3Smrg box = urwid.LineBox(pile) 2617ec681f3Smrg 2627ec681f3Smrg self.mainloop.widget = urwid.Overlay( 2637ec681f3Smrg urwid.Filler(box), o, 'center', ('relative', 50), 'middle', ('relative', 50) 2647ec681f3Smrg ) 265