diff options
Diffstat (limited to 'bin/pick/ui.py')
-rw-r--r-- | bin/pick/ui.py | 262 |
1 files changed, 262 insertions, 0 deletions
diff --git a/bin/pick/ui.py b/bin/pick/ui.py new file mode 100644 index 00000000000..a6f2fc7006e --- /dev/null +++ b/bin/pick/ui.py @@ -0,0 +1,262 @@ +# Copyright © 2019-2020 Intel Corporation + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Urwid UI for pick script.""" + +import asyncio +import itertools +import textwrap +import typing + +import attr +import urwid + +from . import core + +if typing.TYPE_CHECKING: + WidgetType = typing.TypeVar('WidgetType', bound=urwid.Widget) + +PALETTE = [ + ('a', 'black', 'light gray'), + ('b', 'black', 'dark red'), + ('bg', 'black', 'dark blue'), + ('reversed', 'standout', ''), +] + + +class RootWidget(urwid.Frame): + + def __init__(self, *args, ui: 'UI' = None, **kwargs): + super().__init__(*args, **kwargs) + assert ui is not None + self.ui = ui + + def keypress(self, size: int, key: str) -> typing.Optional[str]: + if key == 'q': + raise urwid.ExitMainLoop() + elif key == 'u': + asyncio.ensure_future(self.ui.update()) + elif key == 'a': + self.ui.add() + else: + return super().keypress(size, key) + return None + + +class CommitWidget(urwid.Text): + + # urwid.Text is normally not interactable, this is required to tell urwid + # to use our keypress method + _selectable = True + + def __init__(self, ui: 'UI', commit: 'core.Commit'): + super().__init__(commit.description) + self.ui = ui + self.commit = commit + + async def apply(self) -> None: + async with self.ui.git_lock: + result, err = await self.commit.apply(self.ui) + if not result: + self.ui.chp_failed(self, err) + else: + self.ui.remove_commit(self) + + async def denominate(self) -> None: + async with self.ui.git_lock: + await self.commit.denominate(self.ui) + self.ui.remove_commit(self) + + async def backport(self) -> None: + async with self.ui.git_lock: + await self.commit.backport(self.ui) + self.ui.remove_commit(self) + + def keypress(self, size: int, key: str) -> typing.Optional[str]: + if key == 'c': + asyncio.ensure_future(self.apply()) + elif key == 'd': + asyncio.ensure_future(self.denominate()) + elif key == 'b': + asyncio.ensure_future(self.backport()) + else: + return key + return None + + [email protected](slots=True) +class UI: + + """Main management object. + + :previous_commits: A list of commits to master since this branch was created + :new_commits: Commits added to master since the last time this script was run + """ + + commit_list: typing.List['urwid.Button'] = attr.ib(factory=lambda: urwid.SimpleFocusListWalker([]), init=False) + feedback_box: typing.List['urwid.Text'] = attr.ib(factory=lambda: urwid.SimpleFocusListWalker([]), init=False) + header: 'urwid.Text' = attr.ib(factory=lambda: urwid.Text('Mesa Stable Picker', align='center'), init=False) + body: 'urwid.Columns' = attr.ib(attr.Factory(lambda s: s._make_body(), True), init=False) + footer: 'urwid.Columns' = attr.ib(attr.Factory(lambda s: s._make_footer(), True), init=False) + root: RootWidget = attr.ib(attr.Factory(lambda s: s._make_root(), True), init=False) + mainloop: urwid.MainLoop = attr.ib(None, init=False) + + previous_commits: typing.List['core.Commit'] = attr.ib(factory=list, init=False) + new_commits: typing.List['core.Commit'] = attr.ib(factory=list, init=False) + git_lock: asyncio.Lock = attr.ib(factory=asyncio.Lock, init=False) + + def _make_body(self) -> 'urwid.Columns': + commits = urwid.ListBox(self.commit_list) + feedback = urwid.ListBox(self.feedback_box) + return urwid.Columns([commits, feedback]) + + def _make_footer(self) -> 'urwid.Columns': + body = [ + urwid.Text('[U]pdate'), + urwid.Text('[Q]uit'), + urwid.Text('[C]herry Pick'), + urwid.Text('[D]enominate'), + urwid.Text('[B]ackport'), + urwid.Text('[A]pply additional patch') + ] + return urwid.Columns(body) + + def _make_root(self) -> 'RootWidget': + return RootWidget(self.body, self.header, self.footer, 'body', ui=self) + + def render(self) -> 'WidgetType': + asyncio.ensure_future(self.update()) + return self.root + + def load(self) -> None: + self.previous_commits = core.load() + + async def update(self) -> None: + self.load() + with open('VERSION', 'r') as f: + version = '.'.join(f.read().split('.')[:2]) + if self.previous_commits: + sha = self.previous_commits[0].sha + else: + sha = f'{version}-branchpoint' + + new_commits = await core.get_new_commits(sha) + + if new_commits: + pb = urwid.ProgressBar('a', 'b', done=len(new_commits)) + o = self.mainloop.widget + self.mainloop.widget = urwid.Overlay( + urwid.Filler(urwid.LineBox(pb)), o, 'center', ('relative', 50), 'middle', ('relative', 50)) + self.new_commits = await core.gather_commits( + version, self.previous_commits, new_commits, + lambda: pb.set_completion(pb.current + 1)) + self.mainloop.widget = o + + for commit in reversed(list(itertools.chain(self.new_commits, self.previous_commits))): + if commit.nominated and commit.resolution is core.Resolution.UNRESOLVED: + b = urwid.AttrMap(CommitWidget(self, commit), None, focus_map='reversed') + self.commit_list.append(b) + self.save() + + async def feedback(self, text: str) -> None: + self.feedback_box.append(urwid.AttrMap(urwid.Text(text), None)) + + def remove_commit(self, commit: CommitWidget) -> None: + for i, c in enumerate(self.commit_list): + if c.base_widget is commit: + del self.commit_list[i] + break + + def save(self): + core.save(itertools.chain(self.new_commits, self.previous_commits)) + + def add(self) -> None: + """Add an additional commit which isn't nominated.""" + o = self.mainloop.widget + + def reset_cb(_) -> None: + self.mainloop.widget = o + + async def apply_cb(edit: urwid.Edit) -> None: + text: str = edit.get_edit_text() + + # In case the text is empty + if not text: + return + + sha = await core.full_sha(text) + for c in reversed(list(itertools.chain(self.new_commits, self.previous_commits))): + if c.sha == sha: + commit = c + break + else: + raise RuntimeError(f"Couldn't find {sha}") + + await commit.apply(self) + + q = urwid.Edit("Commit sha\n") + ok_btn = urwid.Button('Ok') + urwid.connect_signal(ok_btn, 'click', lambda _: asyncio.ensure_future(apply_cb(q))) + urwid.connect_signal(ok_btn, 'click', reset_cb) + + can_btn = urwid.Button('Cancel') + urwid.connect_signal(can_btn, 'click', reset_cb) + + cols = urwid.Columns([ok_btn, can_btn]) + pile = urwid.Pile([q, cols]) + box = urwid.LineBox(pile) + + self.mainloop.widget = urwid.Overlay( + urwid.Filler(box), o, 'center', ('relative', 50), 'middle', ('relative', 50) + ) + + def chp_failed(self, commit: 'CommitWidget', err: str) -> None: + o = self.mainloop.widget + + def reset_cb(_) -> None: + self.mainloop.widget = o + + t = urwid.Text(textwrap.dedent(f""" + Failed to apply {commit.commit.sha} {commit.commit.description} with the following error: + + {err} + + You can either cancel, or resolve the conflicts, commit the + changes and select ok.""")) + + can_btn = urwid.Button('Cancel') + urwid.connect_signal(can_btn, 'click', reset_cb) + urwid.connect_signal( + can_btn, 'click', lambda _: asyncio.ensure_future(commit.commit.abort_cherry(self, err))) + + ok_btn = urwid.Button('Ok') + urwid.connect_signal(ok_btn, 'click', reset_cb) + urwid.connect_signal( + ok_btn, 'click', lambda _: asyncio.ensure_future(commit.commit.resolve(self))) + urwid.connect_signal( + ok_btn, 'click', lambda _: self.remove_commit(commit)) + + cols = urwid.Columns([ok_btn, can_btn]) + pile = urwid.Pile([t, cols]) + box = urwid.LineBox(pile) + + self.mainloop.widget = urwid.Overlay( + urwid.Filler(box), o, 'center', ('relative', 50), 'middle', ('relative', 50) + ) |