From 0630a599fc7ab3b2c83a703dd1ddb71c535b3d9c Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 20 Apr 2023 11:27:21 -0700 Subject: [PATCH] bin/pick: Add support for adding notes on patches This is pretty useful for keeping track of why a patch isn't landed, or who I'm waiting on feedback from. Part-of: --- bin/pick/core.py | 13 ++++++++++- bin/pick/ui.py | 57 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/bin/pick/core.py b/bin/pick/core.py index a097dabc1ec..b0db62bdefa 100644 --- a/bin/pick/core.py +++ b/bin/pick/core.py @@ -44,6 +44,7 @@ if typing.TYPE_CHECKING: resolution: typing.Optional[int] main_sha: typing.Optional[str] because_sha: typing.Optional[str] + notes: typing.Optional[str] = attr.ib(None) IS_FIX = re.compile(r'^\s*fixes:\s*([a-f0-9]{6,40})', flags=re.MULTILINE | re.IGNORECASE) # FIXME: I dislike the duplication in this regex, but I couldn't get it to work otherwise @@ -121,6 +122,7 @@ class Commit: resolution: Resolution = attr.ib(Resolution.UNRESOLVED) main_sha: typing.Optional[str] = attr.ib(None) because_sha: typing.Optional[str] = attr.ib(None) + notes: typing.Optional[str] = attr.ib(None) def to_json(self) -> 'CommitDict': d: typing.Dict[str, typing.Any] = attr.asdict(self) @@ -131,7 +133,8 @@ class Commit: @classmethod def from_json(cls, data: 'CommitDict') -> 'Commit': - c = cls(data['sha'], data['description'], data['nominated'], main_sha=data['main_sha'], because_sha=data['because_sha']) + c = cls(data['sha'], data['description'], data['nominated'], main_sha=data['main_sha'], + because_sha=data['because_sha'], notes=data['notes']) c.nomination_type = NominationType(data['nomination_type']) if data['resolution'] is not None: c.resolution = Resolution(data['resolution']) @@ -201,6 +204,14 @@ class Commit: assert v await ui.feedback(f'{self.sha} ({self.description}) committed successfully') + async def update_notes(self, ui: 'UI', notes: typing.Optional[str]) -> None: + self.notes = notes + async with ui.git_lock: + ui.save() + v = await commit_state(message=f'Updates notes for {self.sha}') + assert v + await ui.feedback(f'{self.sha} ({self.description}) notes updated successfully') + async def get_new_commits(sha: str) -> typing.List[typing.Tuple[str, str]]: # Try to get the authoritative upstream main diff --git a/bin/pick/ui.py b/bin/pick/ui.py index e30908fdf56..e666e829624 100644 --- a/bin/pick/ui.py +++ b/bin/pick/ui.py @@ -47,6 +47,13 @@ class RootWidget(urwid.Frame): super().__init__(*args, **kwargs) self.ui = ui + +class CommitList(urwid.ListBox): + + def __init__(self, *args, ui: 'UI', **kwargs): + super().__init__(*args, **kwargs) + self.ui = ui + def keypress(self, size: int, key: str) -> typing.Optional[str]: if key == 'q': raise urwid.ExitMainLoop() @@ -101,6 +108,23 @@ class CommitWidget(urwid.Text): return None +class FocusAwareEdit(urwid.Edit): + + """An Edit type that signals when it comes into and leaves focus.""" + + signals = urwid.Edit.signals + ['focus_changed'] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__is_focus = False + + def render(self, size: typing.Tuple[int], focus: bool = False) -> urwid.Canvas: + if focus != self.__is_focus: + self._emit("focus_changed", focus) + self.__is_focus = focus + return super().render(size, focus) + + @attr.s(slots=True) class UI: @@ -112,6 +136,7 @@ class UI: 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) + notes: 'FocusAwareEdit' = attr.ib(factory=lambda: FocusAwareEdit('', multiline=True), 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) @@ -122,10 +147,36 @@ class UI: new_commits: typing.List['core.Commit'] = attr.ib(factory=list, init=False) git_lock: asyncio.Lock = attr.ib(factory=asyncio.Lock, init=False) + def _get_current_commit(self) -> typing.Optional['core.Commit']: + entry = self.commit_list.get_focus()[0] + return entry.original_widget.commit if entry is not None else None + + def _change_notes_cb(self) -> None: + commit = self._get_current_commit() + if commit and commit.notes: + self.notes.set_edit_text(commit.notes) + else: + self.notes.set_edit_text('') + + def _change_notes_focus_cb(self, notes: 'FocusAwareEdit', focus: 'bool') -> 'None': + # in the case of coming into focus we don't want to do anything + if focus: + return + commit = self._get_current_commit() + if commit is None: + return + text: str = notes.get_edit_text() + if text != commit.notes: + asyncio.ensure_future(commit.update_notes(self, text)) + def _make_body(self) -> 'urwid.Columns': - commits = urwid.ListBox(self.commit_list) + commits = CommitList(self.commit_list, ui=self) feedback = urwid.ListBox(self.feedback_box) - return urwid.Columns([urwid.LineBox(commits), urwid.LineBox(feedback)]) + urwid.connect_signal(self.commit_list, 'modified', self._change_notes_cb) + notes = urwid.Filler(self.notes) + urwid.connect_signal(self.notes, 'focus_changed', self._change_notes_focus_cb) + + return urwid.Columns([urwid.LineBox(commits), urwid.Pile([urwid.LineBox(notes), urwid.LineBox(feedback)])]) def _make_footer(self) -> 'urwid.Columns': body = [ @@ -134,7 +185,7 @@ class UI: urwid.Text('[C]herry Pick'), urwid.Text('[D]enominate'), urwid.Text('[B]ackport'), - urwid.Text('[A]pply additional patch') + urwid.Text('[A]pply additional patch'), ] return urwid.Columns(body)