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"""Urwid UI for pick script.""" 22bf215546Sopenharmony_ci 23bf215546Sopenharmony_ciimport asyncio 24bf215546Sopenharmony_ciimport itertools 25bf215546Sopenharmony_ciimport textwrap 26bf215546Sopenharmony_ciimport typing 27bf215546Sopenharmony_ci 28bf215546Sopenharmony_ciimport attr 29bf215546Sopenharmony_ciimport urwid 30bf215546Sopenharmony_ci 31bf215546Sopenharmony_cifrom . import core 32bf215546Sopenharmony_ci 33bf215546Sopenharmony_ciif typing.TYPE_CHECKING: 34bf215546Sopenharmony_ci WidgetType = typing.TypeVar('WidgetType', bound=urwid.Widget) 35bf215546Sopenharmony_ci 36bf215546Sopenharmony_ciPALETTE = [ 37bf215546Sopenharmony_ci ('a', 'black', 'light gray'), 38bf215546Sopenharmony_ci ('b', 'black', 'dark red'), 39bf215546Sopenharmony_ci ('bg', 'black', 'dark blue'), 40bf215546Sopenharmony_ci ('reversed', 'standout', ''), 41bf215546Sopenharmony_ci] 42bf215546Sopenharmony_ci 43bf215546Sopenharmony_ci 44bf215546Sopenharmony_ciclass RootWidget(urwid.Frame): 45bf215546Sopenharmony_ci 46bf215546Sopenharmony_ci def __init__(self, *args, ui: 'UI', **kwargs): 47bf215546Sopenharmony_ci super().__init__(*args, **kwargs) 48bf215546Sopenharmony_ci self.ui = ui 49bf215546Sopenharmony_ci 50bf215546Sopenharmony_ci def keypress(self, size: int, key: str) -> typing.Optional[str]: 51bf215546Sopenharmony_ci if key == 'q': 52bf215546Sopenharmony_ci raise urwid.ExitMainLoop() 53bf215546Sopenharmony_ci elif key == 'u': 54bf215546Sopenharmony_ci asyncio.ensure_future(self.ui.update()) 55bf215546Sopenharmony_ci elif key == 'a': 56bf215546Sopenharmony_ci self.ui.add() 57bf215546Sopenharmony_ci else: 58bf215546Sopenharmony_ci return super().keypress(size, key) 59bf215546Sopenharmony_ci return None 60bf215546Sopenharmony_ci 61bf215546Sopenharmony_ci 62bf215546Sopenharmony_ciclass CommitWidget(urwid.Text): 63bf215546Sopenharmony_ci 64bf215546Sopenharmony_ci # urwid.Text is normally not interactable, this is required to tell urwid 65bf215546Sopenharmony_ci # to use our keypress method 66bf215546Sopenharmony_ci _selectable = True 67bf215546Sopenharmony_ci 68bf215546Sopenharmony_ci def __init__(self, ui: 'UI', commit: 'core.Commit'): 69bf215546Sopenharmony_ci reason = commit.nomination_type.name.ljust(6) 70bf215546Sopenharmony_ci super().__init__(f'{commit.date()} {reason} {commit.sha[:10]} {commit.description}') 71bf215546Sopenharmony_ci self.ui = ui 72bf215546Sopenharmony_ci self.commit = commit 73bf215546Sopenharmony_ci 74bf215546Sopenharmony_ci async def apply(self) -> None: 75bf215546Sopenharmony_ci async with self.ui.git_lock: 76bf215546Sopenharmony_ci result, err = await self.commit.apply(self.ui) 77bf215546Sopenharmony_ci if not result: 78bf215546Sopenharmony_ci self.ui.chp_failed(self, err) 79bf215546Sopenharmony_ci else: 80bf215546Sopenharmony_ci self.ui.remove_commit(self) 81bf215546Sopenharmony_ci 82bf215546Sopenharmony_ci async def denominate(self) -> None: 83bf215546Sopenharmony_ci async with self.ui.git_lock: 84bf215546Sopenharmony_ci await self.commit.denominate(self.ui) 85bf215546Sopenharmony_ci self.ui.remove_commit(self) 86bf215546Sopenharmony_ci 87bf215546Sopenharmony_ci async def backport(self) -> None: 88bf215546Sopenharmony_ci async with self.ui.git_lock: 89bf215546Sopenharmony_ci await self.commit.backport(self.ui) 90bf215546Sopenharmony_ci self.ui.remove_commit(self) 91bf215546Sopenharmony_ci 92bf215546Sopenharmony_ci def keypress(self, size: int, key: str) -> typing.Optional[str]: 93bf215546Sopenharmony_ci if key == 'c': 94bf215546Sopenharmony_ci asyncio.ensure_future(self.apply()) 95bf215546Sopenharmony_ci elif key == 'd': 96bf215546Sopenharmony_ci asyncio.ensure_future(self.denominate()) 97bf215546Sopenharmony_ci elif key == 'b': 98bf215546Sopenharmony_ci asyncio.ensure_future(self.backport()) 99bf215546Sopenharmony_ci else: 100bf215546Sopenharmony_ci return key 101bf215546Sopenharmony_ci return None 102bf215546Sopenharmony_ci 103bf215546Sopenharmony_ci 104bf215546Sopenharmony_ci@attr.s(slots=True) 105bf215546Sopenharmony_ciclass UI: 106bf215546Sopenharmony_ci 107bf215546Sopenharmony_ci """Main management object. 108bf215546Sopenharmony_ci 109bf215546Sopenharmony_ci :previous_commits: A list of commits to main since this branch was created 110bf215546Sopenharmony_ci :new_commits: Commits added to main since the last time this script was run 111bf215546Sopenharmony_ci """ 112bf215546Sopenharmony_ci 113bf215546Sopenharmony_ci commit_list: typing.List['urwid.Button'] = attr.ib(factory=lambda: urwid.SimpleFocusListWalker([]), init=False) 114bf215546Sopenharmony_ci feedback_box: typing.List['urwid.Text'] = attr.ib(factory=lambda: urwid.SimpleFocusListWalker([]), init=False) 115bf215546Sopenharmony_ci header: 'urwid.Text' = attr.ib(factory=lambda: urwid.Text('Mesa Stable Picker', align='center'), init=False) 116bf215546Sopenharmony_ci body: 'urwid.Columns' = attr.ib(attr.Factory(lambda s: s._make_body(), True), init=False) 117bf215546Sopenharmony_ci footer: 'urwid.Columns' = attr.ib(attr.Factory(lambda s: s._make_footer(), True), init=False) 118bf215546Sopenharmony_ci root: RootWidget = attr.ib(attr.Factory(lambda s: s._make_root(), True), init=False) 119bf215546Sopenharmony_ci mainloop: urwid.MainLoop = attr.ib(None, init=False) 120bf215546Sopenharmony_ci 121bf215546Sopenharmony_ci previous_commits: typing.List['core.Commit'] = attr.ib(factory=list, init=False) 122bf215546Sopenharmony_ci new_commits: typing.List['core.Commit'] = attr.ib(factory=list, init=False) 123bf215546Sopenharmony_ci git_lock: asyncio.Lock = attr.ib(factory=asyncio.Lock, init=False) 124bf215546Sopenharmony_ci 125bf215546Sopenharmony_ci def _make_body(self) -> 'urwid.Columns': 126bf215546Sopenharmony_ci commits = urwid.ListBox(self.commit_list) 127bf215546Sopenharmony_ci feedback = urwid.ListBox(self.feedback_box) 128bf215546Sopenharmony_ci return urwid.Columns([commits, feedback]) 129bf215546Sopenharmony_ci 130bf215546Sopenharmony_ci def _make_footer(self) -> 'urwid.Columns': 131bf215546Sopenharmony_ci body = [ 132bf215546Sopenharmony_ci urwid.Text('[U]pdate'), 133bf215546Sopenharmony_ci urwid.Text('[Q]uit'), 134bf215546Sopenharmony_ci urwid.Text('[C]herry Pick'), 135bf215546Sopenharmony_ci urwid.Text('[D]enominate'), 136bf215546Sopenharmony_ci urwid.Text('[B]ackport'), 137bf215546Sopenharmony_ci urwid.Text('[A]pply additional patch') 138bf215546Sopenharmony_ci ] 139bf215546Sopenharmony_ci return urwid.Columns(body) 140bf215546Sopenharmony_ci 141bf215546Sopenharmony_ci def _make_root(self) -> 'RootWidget': 142bf215546Sopenharmony_ci return RootWidget(self.body, self.header, self.footer, 'body', ui=self) 143bf215546Sopenharmony_ci 144bf215546Sopenharmony_ci def render(self) -> 'WidgetType': 145bf215546Sopenharmony_ci asyncio.ensure_future(self.update()) 146bf215546Sopenharmony_ci return self.root 147bf215546Sopenharmony_ci 148bf215546Sopenharmony_ci def load(self) -> None: 149bf215546Sopenharmony_ci self.previous_commits = core.load() 150bf215546Sopenharmony_ci 151bf215546Sopenharmony_ci async def update(self) -> None: 152bf215546Sopenharmony_ci self.load() 153bf215546Sopenharmony_ci with open('VERSION', 'r') as f: 154bf215546Sopenharmony_ci version = '.'.join(f.read().split('.')[:2]) 155bf215546Sopenharmony_ci if self.previous_commits: 156bf215546Sopenharmony_ci sha = self.previous_commits[0].sha 157bf215546Sopenharmony_ci else: 158bf215546Sopenharmony_ci sha = f'{version}-branchpoint' 159bf215546Sopenharmony_ci 160bf215546Sopenharmony_ci new_commits = await core.get_new_commits(sha) 161bf215546Sopenharmony_ci 162bf215546Sopenharmony_ci if new_commits: 163bf215546Sopenharmony_ci pb = urwid.ProgressBar('a', 'b', done=len(new_commits)) 164bf215546Sopenharmony_ci o = self.mainloop.widget 165bf215546Sopenharmony_ci self.mainloop.widget = urwid.Overlay( 166bf215546Sopenharmony_ci urwid.Filler(urwid.LineBox(pb)), o, 'center', ('relative', 50), 'middle', ('relative', 50)) 167bf215546Sopenharmony_ci self.new_commits = await core.gather_commits( 168bf215546Sopenharmony_ci version, self.previous_commits, new_commits, 169bf215546Sopenharmony_ci lambda: pb.set_completion(pb.current + 1)) 170bf215546Sopenharmony_ci self.mainloop.widget = o 171bf215546Sopenharmony_ci 172bf215546Sopenharmony_ci for commit in reversed(list(itertools.chain(self.new_commits, self.previous_commits))): 173bf215546Sopenharmony_ci if commit.nominated and commit.resolution is core.Resolution.UNRESOLVED: 174bf215546Sopenharmony_ci b = urwid.AttrMap(CommitWidget(self, commit), None, focus_map='reversed') 175bf215546Sopenharmony_ci self.commit_list.append(b) 176bf215546Sopenharmony_ci self.save() 177bf215546Sopenharmony_ci 178bf215546Sopenharmony_ci async def feedback(self, text: str) -> None: 179bf215546Sopenharmony_ci self.feedback_box.append(urwid.AttrMap(urwid.Text(text), None)) 180bf215546Sopenharmony_ci latest_item_index = len(self.feedback_box) - 1 181bf215546Sopenharmony_ci self.feedback_box.set_focus(latest_item_index) 182bf215546Sopenharmony_ci 183bf215546Sopenharmony_ci def remove_commit(self, commit: CommitWidget) -> None: 184bf215546Sopenharmony_ci for i, c in enumerate(self.commit_list): 185bf215546Sopenharmony_ci if c.base_widget is commit: 186bf215546Sopenharmony_ci del self.commit_list[i] 187bf215546Sopenharmony_ci break 188bf215546Sopenharmony_ci 189bf215546Sopenharmony_ci def save(self): 190bf215546Sopenharmony_ci core.save(itertools.chain(self.new_commits, self.previous_commits)) 191bf215546Sopenharmony_ci 192bf215546Sopenharmony_ci def add(self) -> None: 193bf215546Sopenharmony_ci """Add an additional commit which isn't nominated.""" 194bf215546Sopenharmony_ci o = self.mainloop.widget 195bf215546Sopenharmony_ci 196bf215546Sopenharmony_ci def reset_cb(_) -> None: 197bf215546Sopenharmony_ci self.mainloop.widget = o 198bf215546Sopenharmony_ci 199bf215546Sopenharmony_ci async def apply_cb(edit: urwid.Edit) -> None: 200bf215546Sopenharmony_ci text: str = edit.get_edit_text() 201bf215546Sopenharmony_ci 202bf215546Sopenharmony_ci # In case the text is empty 203bf215546Sopenharmony_ci if not text: 204bf215546Sopenharmony_ci return 205bf215546Sopenharmony_ci 206bf215546Sopenharmony_ci sha = await core.full_sha(text) 207bf215546Sopenharmony_ci for c in reversed(list(itertools.chain(self.new_commits, self.previous_commits))): 208bf215546Sopenharmony_ci if c.sha == sha: 209bf215546Sopenharmony_ci commit = c 210bf215546Sopenharmony_ci break 211bf215546Sopenharmony_ci else: 212bf215546Sopenharmony_ci raise RuntimeError(f"Couldn't find {sha}") 213bf215546Sopenharmony_ci 214bf215546Sopenharmony_ci await commit.apply(self) 215bf215546Sopenharmony_ci 216bf215546Sopenharmony_ci q = urwid.Edit("Commit sha\n") 217bf215546Sopenharmony_ci ok_btn = urwid.Button('Ok') 218bf215546Sopenharmony_ci urwid.connect_signal(ok_btn, 'click', lambda _: asyncio.ensure_future(apply_cb(q))) 219bf215546Sopenharmony_ci urwid.connect_signal(ok_btn, 'click', reset_cb) 220bf215546Sopenharmony_ci 221bf215546Sopenharmony_ci can_btn = urwid.Button('Cancel') 222bf215546Sopenharmony_ci urwid.connect_signal(can_btn, 'click', reset_cb) 223bf215546Sopenharmony_ci 224bf215546Sopenharmony_ci cols = urwid.Columns([ok_btn, can_btn]) 225bf215546Sopenharmony_ci pile = urwid.Pile([q, cols]) 226bf215546Sopenharmony_ci box = urwid.LineBox(pile) 227bf215546Sopenharmony_ci 228bf215546Sopenharmony_ci self.mainloop.widget = urwid.Overlay( 229bf215546Sopenharmony_ci urwid.Filler(box), o, 'center', ('relative', 50), 'middle', ('relative', 50) 230bf215546Sopenharmony_ci ) 231bf215546Sopenharmony_ci 232bf215546Sopenharmony_ci def chp_failed(self, commit: 'CommitWidget', err: str) -> None: 233bf215546Sopenharmony_ci o = self.mainloop.widget 234bf215546Sopenharmony_ci 235bf215546Sopenharmony_ci def reset_cb(_) -> None: 236bf215546Sopenharmony_ci self.mainloop.widget = o 237bf215546Sopenharmony_ci 238bf215546Sopenharmony_ci t = urwid.Text(textwrap.dedent(f""" 239bf215546Sopenharmony_ci Failed to apply {commit.commit.sha} {commit.commit.description} with the following error: 240bf215546Sopenharmony_ci 241bf215546Sopenharmony_ci {err} 242bf215546Sopenharmony_ci 243bf215546Sopenharmony_ci You can either cancel, or resolve the conflicts (`git mergetool`), finish the 244bf215546Sopenharmony_ci cherry-pick (`git cherry-pick --continue`) and select ok.""")) 245bf215546Sopenharmony_ci 246bf215546Sopenharmony_ci can_btn = urwid.Button('Cancel') 247bf215546Sopenharmony_ci urwid.connect_signal(can_btn, 'click', reset_cb) 248bf215546Sopenharmony_ci urwid.connect_signal( 249bf215546Sopenharmony_ci can_btn, 'click', lambda _: asyncio.ensure_future(commit.commit.abort_cherry(self, err))) 250bf215546Sopenharmony_ci 251bf215546Sopenharmony_ci ok_btn = urwid.Button('Ok') 252bf215546Sopenharmony_ci urwid.connect_signal(ok_btn, 'click', reset_cb) 253bf215546Sopenharmony_ci urwid.connect_signal( 254bf215546Sopenharmony_ci ok_btn, 'click', lambda _: asyncio.ensure_future(commit.commit.resolve(self))) 255bf215546Sopenharmony_ci urwid.connect_signal( 256bf215546Sopenharmony_ci ok_btn, 'click', lambda _: self.remove_commit(commit)) 257bf215546Sopenharmony_ci 258bf215546Sopenharmony_ci cols = urwid.Columns([ok_btn, can_btn]) 259bf215546Sopenharmony_ci pile = urwid.Pile([t, cols]) 260bf215546Sopenharmony_ci box = urwid.LineBox(pile) 261bf215546Sopenharmony_ci 262bf215546Sopenharmony_ci self.mainloop.widget = urwid.Overlay( 263bf215546Sopenharmony_ci urwid.Filler(box), o, 'center', ('relative', 50), 'middle', ('relative', 50) 264bf215546Sopenharmony_ci ) 265