1 # Copyright © 2019-2020 Intel Corporation
3 # Permission is hereby granted, free of charge, to any person obtaining a copy
4 # of this software and associated documentation files (the "Software"), to deal
5 # in the Software without restriction, including without limitation the rights
6 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 # copies of the Software, and to permit persons to whom the Software is
8 # furnished to do so, subject to the following conditions:
10 # The above copyright notice and this permission notice shall be included in
11 # all copies or substantial portions of the Software.
13 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 """Urwid UI for pick script."""
33 if typing
.TYPE_CHECKING
:
34 WidgetType
= typing
.TypeVar('WidgetType', bound
=urwid
.Widget
)
37 ('a', 'black', 'light gray'),
38 ('b', 'black', 'dark red'),
39 ('bg', 'black', 'dark blue'),
40 ('reversed', 'standout', ''),
44 class RootWidget(urwid
.Frame
):
46 def __init__(self
, *args
, ui
: 'UI' = None, **kwargs
):
47 super().__init
__(*args
, **kwargs
)
51 def keypress(self
, size
: int, key
: str) -> typing
.Optional
[str]:
53 raise urwid
.ExitMainLoop()
55 asyncio
.ensure_future(self
.ui
.update())
59 return super().keypress(size
, key
)
63 class CommitWidget(urwid
.Text
):
65 # urwid.Text is normally not interactable, this is required to tell urwid
66 # to use our keypress method
69 def __init__(self
, ui
: 'UI', commit
: 'core.Commit'):
70 super().__init
__(f
'{commit.sha[:10]} {commit.description}')
74 async def apply(self
) -> None:
75 async with self
.ui
.git_lock
:
76 result
, err
= await self
.commit
.apply(self
.ui
)
78 self
.ui
.chp_failed(self
, err
)
80 self
.ui
.remove_commit(self
)
82 async def denominate(self
) -> None:
83 async with self
.ui
.git_lock
:
84 await self
.commit
.denominate(self
.ui
)
85 self
.ui
.remove_commit(self
)
87 async def backport(self
) -> None:
88 async with self
.ui
.git_lock
:
89 await self
.commit
.backport(self
.ui
)
90 self
.ui
.remove_commit(self
)
92 def keypress(self
, size
: int, key
: str) -> typing
.Optional
[str]:
94 asyncio
.ensure_future(self
.apply())
96 asyncio
.ensure_future(self
.denominate())
98 asyncio
.ensure_future(self
.backport())
107 """Main management object.
109 :previous_commits: A list of commits to master since this branch was created
110 :new_commits: Commits added to master since the last time this script was run
113 commit_list
: typing
.List
['urwid.Button'] = attr
.ib(factory
=lambda: urwid
.SimpleFocusListWalker([]), init
=False)
114 feedback_box
: typing
.List
['urwid.Text'] = attr
.ib(factory
=lambda: urwid
.SimpleFocusListWalker([]), init
=False)
115 header
: 'urwid.Text' = attr
.ib(factory
=lambda: urwid
.Text('Mesa Stable Picker', align
='center'), init
=False)
116 body
: 'urwid.Columns' = attr
.ib(attr
.Factory(lambda s
: s
._make
_body
(), True), init
=False)
117 footer
: 'urwid.Columns' = attr
.ib(attr
.Factory(lambda s
: s
._make
_footer
(), True), init
=False)
118 root
: RootWidget
= attr
.ib(attr
.Factory(lambda s
: s
._make
_root
(), True), init
=False)
119 mainloop
: urwid
.MainLoop
= attr
.ib(None, init
=False)
121 previous_commits
: typing
.List
['core.Commit'] = attr
.ib(factory
=list, init
=False)
122 new_commits
: typing
.List
['core.Commit'] = attr
.ib(factory
=list, init
=False)
123 git_lock
: asyncio
.Lock
= attr
.ib(factory
=asyncio
.Lock
, init
=False)
125 def _make_body(self
) -> 'urwid.Columns':
126 commits
= urwid
.ListBox(self
.commit_list
)
127 feedback
= urwid
.ListBox(self
.feedback_box
)
128 return urwid
.Columns([commits
, feedback
])
130 def _make_footer(self
) -> 'urwid.Columns':
132 urwid
.Text('[U]pdate'),
133 urwid
.Text('[Q]uit'),
134 urwid
.Text('[C]herry Pick'),
135 urwid
.Text('[D]enominate'),
136 urwid
.Text('[B]ackport'),
137 urwid
.Text('[A]pply additional patch')
139 return urwid
.Columns(body
)
141 def _make_root(self
) -> 'RootWidget':
142 return RootWidget(self
.body
, self
.header
, self
.footer
, 'body', ui
=self
)
144 def render(self
) -> 'WidgetType':
145 asyncio
.ensure_future(self
.update())
148 def load(self
) -> None:
149 self
.previous_commits
= core
.load()
151 async def update(self
) -> None:
153 with
open('VERSION', 'r') as f
:
154 version
= '.'.join(f
.read().split('.')[:2])
155 if self
.previous_commits
:
156 sha
= self
.previous_commits
[0].sha
158 sha
= f
'{version}-branchpoint'
160 new_commits
= await core
.get_new_commits(sha
)
163 pb
= urwid
.ProgressBar('a', 'b', done
=len(new_commits
))
164 o
= self
.mainloop
.widget
165 self
.mainloop
.widget
= urwid
.Overlay(
166 urwid
.Filler(urwid
.LineBox(pb
)), o
, 'center', ('relative', 50), 'middle', ('relative', 50))
167 self
.new_commits
= await core
.gather_commits(
168 version
, self
.previous_commits
, new_commits
,
169 lambda: pb
.set_completion(pb
.current
+ 1))
170 self
.mainloop
.widget
= o
172 for commit
in reversed(list(itertools
.chain(self
.new_commits
, self
.previous_commits
))):
173 if commit
.nominated
and commit
.resolution
is core
.Resolution
.UNRESOLVED
:
174 b
= urwid
.AttrMap(CommitWidget(self
, commit
), None, focus_map
='reversed')
175 self
.commit_list
.append(b
)
178 async def feedback(self
, text
: str) -> None:
179 self
.feedback_box
.append(urwid
.AttrMap(urwid
.Text(text
), None))
180 latest_item_index
= len(self
.feedback_box
) - 1
181 self
.feedback_box
.set_focus(latest_item_index
)
183 def remove_commit(self
, commit
: CommitWidget
) -> None:
184 for i
, c
in enumerate(self
.commit_list
):
185 if c
.base_widget
is commit
:
186 del self
.commit_list
[i
]
190 core
.save(itertools
.chain(self
.new_commits
, self
.previous_commits
))
192 def add(self
) -> None:
193 """Add an additional commit which isn't nominated."""
194 o
= self
.mainloop
.widget
196 def reset_cb(_
) -> None:
197 self
.mainloop
.widget
= o
199 async def apply_cb(edit
: urwid
.Edit
) -> None:
200 text
: str = edit
.get_edit_text()
202 # In case the text is empty
206 sha
= await core
.full_sha(text
)
207 for c
in reversed(list(itertools
.chain(self
.new_commits
, self
.previous_commits
))):
212 raise RuntimeError(f
"Couldn't find {sha}")
214 await commit
.apply(self
)
216 q
= urwid
.Edit("Commit sha\n")
217 ok_btn
= urwid
.Button('Ok')
218 urwid
.connect_signal(ok_btn
, 'click', lambda _
: asyncio
.ensure_future(apply_cb(q
)))
219 urwid
.connect_signal(ok_btn
, 'click', reset_cb
)
221 can_btn
= urwid
.Button('Cancel')
222 urwid
.connect_signal(can_btn
, 'click', reset_cb
)
224 cols
= urwid
.Columns([ok_btn
, can_btn
])
225 pile
= urwid
.Pile([q
, cols
])
226 box
= urwid
.LineBox(pile
)
228 self
.mainloop
.widget
= urwid
.Overlay(
229 urwid
.Filler(box
), o
, 'center', ('relative', 50), 'middle', ('relative', 50)
232 def chp_failed(self
, commit
: 'CommitWidget', err
: str) -> None:
233 o
= self
.mainloop
.widget
235 def reset_cb(_
) -> None:
236 self
.mainloop
.widget
= o
238 t
= urwid
.Text(textwrap
.dedent(f
"""
239 Failed to apply {commit.commit.sha} {commit.commit.description} with the following error:
243 You can either cancel, or resolve the conflicts, commit the
244 changes and select ok."""))
246 can_btn
= urwid
.Button('Cancel')
247 urwid
.connect_signal(can_btn
, 'click', reset_cb
)
248 urwid
.connect_signal(
249 can_btn
, 'click', lambda _
: asyncio
.ensure_future(commit
.commit
.abort_cherry(self
, err
)))
251 ok_btn
= urwid
.Button('Ok')
252 urwid
.connect_signal(ok_btn
, 'click', reset_cb
)
253 urwid
.connect_signal(
254 ok_btn
, 'click', lambda _
: asyncio
.ensure_future(commit
.commit
.resolve(self
)))
255 urwid
.connect_signal(
256 ok_btn
, 'click', lambda _
: self
.remove_commit(commit
))
258 cols
= urwid
.Columns([ok_btn
, can_btn
])
259 pile
= urwid
.Pile([t
, cols
])
260 box
= urwid
.LineBox(pile
)
262 self
.mainloop
.widget
= urwid
.Overlay(
263 urwid
.Filler(box
), o
, 'center', ('relative', 50), 'middle', ('relative', 50)