gitpane 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (353) hide show
  1. gitpane/__init__.py +0 -0
  2. gitpane/account/__init__.py +0 -0
  3. gitpane/account/account_choice.py +297 -0
  4. gitpane/account/address_owner.py +67 -0
  5. gitpane/account/authentication.py +1811 -0
  6. gitpane/account/authentication_worker.py +836 -0
  7. gitpane/account/credential_manager.py +480 -0
  8. gitpane/account/credential_manager_install.py +171 -0
  9. gitpane/account/keyring_reader.py +135 -0
  10. gitpane/account/keyring_state.py +158 -0
  11. gitpane/account/ssh.py +312 -0
  12. gitpane/account/ssh_askpass.py +216 -0
  13. gitpane/account/ssh_prompt.py +222 -0
  14. gitpane/application.py +684 -0
  15. gitpane/commit/__init__.py +0 -0
  16. gitpane/commit/commit_metadata.py +88 -0
  17. gitpane/commit/commit_push.py +276 -0
  18. gitpane/commit/commit_write.py +830 -0
  19. gitpane/config.py +1066 -0
  20. gitpane/controller/__init__.py +0 -0
  21. gitpane/controller/account_controller.py +677 -0
  22. gitpane/controller/clone_controller.py +776 -0
  23. gitpane/controller/close_readiness.py +212 -0
  24. gitpane/controller/commit_timing_controller.py +566 -0
  25. gitpane/controller/conflict_controller.py +885 -0
  26. gitpane/controller/console_registry.py +38 -0
  27. gitpane/controller/emoji_window_controller.py +95 -0
  28. gitpane/controller/file_action_controller.py +479 -0
  29. gitpane/controller/history_read_controller.py +671 -0
  30. gitpane/controller/history_rewrite_controller.py +739 -0
  31. gitpane/controller/navigation_controller.py +658 -0
  32. gitpane/controller/opening_controller.py +375 -0
  33. gitpane/controller/operation_policy.py +76 -0
  34. gitpane/controller/rebase_controller.py +797 -0
  35. gitpane/controller/rebase_editor_controller.py +309 -0
  36. gitpane/controller/reference_form_controller.py +1677 -0
  37. gitpane/controller/reference_read_controller.py +242 -0
  38. gitpane/controller/refresh_controller.py +477 -0
  39. gitpane/controller/replay_controller.py +805 -0
  40. gitpane/controller/session_registry.py +337 -0
  41. gitpane/controller/setting_controller.py +592 -0
  42. gitpane/controller/shutdown_controller.py +345 -0
  43. gitpane/controller/staging_controller.py +199 -0
  44. gitpane/controller/staging_session_registry.py +47 -0
  45. gitpane/controller/stash_controller.py +367 -0
  46. gitpane/controller/transport_controller.py +813 -0
  47. gitpane/controller/welcome_controller.py +102 -0
  48. gitpane/controller/window_activation.py +116 -0
  49. gitpane/controller/worktree_state_controller.py +169 -0
  50. gitpane/emoji/__init__.py +0 -0
  51. gitpane/emoji/emoji_catalogue.py +156 -0
  52. gitpane/git_config/__init__.py +0 -0
  53. gitpane/git_config/git_setting.py +441 -0
  54. gitpane/history/__init__.py +0 -0
  55. gitpane/history/avatar.py +241 -0
  56. gitpane/history/file_history.py +359 -0
  57. gitpane/history/graph.py +145 -0
  58. gitpane/history/history_filter.py +31 -0
  59. gitpane/history/history_parse.py +605 -0
  60. gitpane/history/repository.py +1600 -0
  61. gitpane/history/repository_value.py +341 -0
  62. gitpane/history/revision.py +180 -0
  63. gitpane/history/revision_file.py +182 -0
  64. gitpane/history_rewrite/__init__.py +0 -0
  65. gitpane/history_rewrite/graph.py +210 -0
  66. gitpane/history_rewrite/guard.py +288 -0
  67. gitpane/history_rewrite/index.py +125 -0
  68. gitpane/history_rewrite/prepare.py +378 -0
  69. gitpane/history_rewrite/protocol.py +320 -0
  70. gitpane/history_rewrite/publish.py +444 -0
  71. gitpane/history_rewrite/recovery.py +481 -0
  72. gitpane/history_rewrite/value.py +229 -0
  73. gitpane/operation/__init__.py +0 -0
  74. gitpane/operation/console_line.py +313 -0
  75. gitpane/operation/console_tone.py +76 -0
  76. gitpane/operation/diagnostic.py +954 -0
  77. gitpane/operation/launch_signature.py +254 -0
  78. gitpane/operation/operation_status.py +87 -0
  79. gitpane/operation/operation_trace.py +766 -0
  80. gitpane/operation/repository_operation.py +781 -0
  81. gitpane/preference/__init__.py +0 -0
  82. gitpane/preference/preference_location.py +40 -0
  83. gitpane/preference/preference_store.py +590 -0
  84. gitpane/process/__init__.py +0 -0
  85. gitpane/process/exact_arguments.py +77 -0
  86. gitpane/process/git_output_text.py +20 -0
  87. gitpane/process/process_group.py +851 -0
  88. gitpane/process/process_log.py +114 -0
  89. gitpane/process/process_runner.py +1033 -0
  90. gitpane/process/released_child_watch.py +138 -0
  91. gitpane/recent/__init__.py +0 -0
  92. gitpane/recent/mount_table.py +422 -0
  93. gitpane/recent/recent_availability.py +414 -0
  94. gitpane/recent/recent_name.py +11 -0
  95. gitpane/recent/recent_search.py +114 -0
  96. gitpane/reference/__init__.py +0 -0
  97. gitpane/reference/reference_edit.py +2030 -0
  98. gitpane/reference/reference_snapshot.py +196 -0
  99. gitpane/reference/stash.py +730 -0
  100. gitpane/reference/stash_list.py +164 -0
  101. gitpane/sequencer/__init__.py +0 -0
  102. gitpane/sequencer/commit_message_rule.py +154 -0
  103. gitpane/sequencer/conflict.py +506 -0
  104. gitpane/sequencer/merge_recovery.py +163 -0
  105. gitpane/sequencer/merge_state.py +166 -0
  106. gitpane/sequencer/mergetool.py +856 -0
  107. gitpane/sequencer/rebase.py +750 -0
  108. gitpane/sequencer/rebase_edit_file.py +224 -0
  109. gitpane/sequencer/rebase_state.py +523 -0
  110. gitpane/sequencer/replay.py +1435 -0
  111. gitpane/sequencer/replay_legacy.py +253 -0
  112. gitpane/sequencer/replay_state.py +747 -0
  113. gitpane/submodule/__init__.py +0 -0
  114. gitpane/submodule/checkout_refusal.py +102 -0
  115. gitpane/submodule/checkout_refusal_notice.py +111 -0
  116. gitpane/submodule/cloned_without_checkout.py +65 -0
  117. gitpane/submodule/submodule_discovery.py +1180 -0
  118. gitpane/submodule/submodule_loss.py +218 -0
  119. gitpane/system/__init__.py +0 -0
  120. gitpane/system/appearance_portal.py +125 -0
  121. gitpane/system/application_identity.py +48 -0
  122. gitpane/system/command_line.py +36 -0
  123. gitpane/system/desktop_icon_fact.py +128 -0
  124. gitpane/system/desktop_icon_support.py +270 -0
  125. gitpane/system/desktop_shortcut.py +536 -0
  126. gitpane/system/icns_image.py +35 -0
  127. gitpane/system/installed_launcher.py +136 -0
  128. gitpane/system/linux_distribution.py +151 -0
  129. gitpane/system/network_folder.py +46 -0
  130. gitpane/system/path_identity.py +95 -0
  131. gitpane/system/private_tree_removal.py +47 -0
  132. gitpane/system/process_program.py +63 -0
  133. gitpane/system/qt_folder.py +38 -0
  134. gitpane/system/ready_channel_watch.py +67 -0
  135. gitpane/system/startup_activation.py +65 -0
  136. gitpane/system/terminal_detach.py +384 -0
  137. gitpane/system/xdg_session.py +85 -0
  138. gitpane/text/__init__.py +0 -0
  139. gitpane/text/text_query.py +97 -0
  140. gitpane/text/typed_path.py +75 -0
  141. gitpane/text/typed_text.py +17 -0
  142. gitpane/thread/__init__.py +0 -0
  143. gitpane/thread/weak_signal.py +93 -0
  144. gitpane/tool/__init__.py +0 -0
  145. gitpane/tool/app_bundle.py +164 -0
  146. gitpane/tool/desktop_text_editor.py +156 -0
  147. gitpane/tool/difftool.py +429 -0
  148. gitpane/tool/editor.py +445 -0
  149. gitpane/tool/editor_bridge.py +193 -0
  150. gitpane/tool/editor_recipe.py +481 -0
  151. gitpane/tool/file_reveal.py +94 -0
  152. gitpane/tool/git_executable.py +264 -0
  153. gitpane/tool/git_version.py +62 -0
  154. gitpane/tool/kdiff3.py +41 -0
  155. gitpane/tool/kdiff3_install.py +162 -0
  156. gitpane/tool/launch_services_text_editor.py +104 -0
  157. gitpane/tool/mergetool_recipe.py +84 -0
  158. gitpane/tool/system_editor_default.py +80 -0
  159. gitpane/tool/system_text_editor.py +44 -0
  160. gitpane/tool/system_tool.py +816 -0
  161. gitpane/tool/terminal_recipe.py +390 -0
  162. gitpane/transport/__init__.py +0 -0
  163. gitpane/transport/branch_exchange.py +1690 -0
  164. gitpane/transport/clone.py +1273 -0
  165. gitpane/transport/credential_read_retry.py +35 -0
  166. gitpane/transport/git_refusal.py +207 -0
  167. gitpane/transport/push_tracking.py +121 -0
  168. gitpane/transport/refusal_notice.py +225 -0
  169. gitpane/transport/remote.py +747 -0
  170. gitpane/transport/remote_config.py +544 -0
  171. gitpane/ui/__init__.py +0 -0
  172. gitpane/ui/console/__init__.py +0 -0
  173. gitpane/ui/console/error_dialog.py +332 -0
  174. gitpane/ui/console/operation_dialog.py +408 -0
  175. gitpane/ui/console/operation_output.py +91 -0
  176. gitpane/ui/console/operation_output_view.py +373 -0
  177. gitpane/ui/emoji/__init__.py +0 -0
  178. gitpane/ui/emoji/emoji_font.py +257 -0
  179. gitpane/ui/emoji/emoji_grid.py +125 -0
  180. gitpane/ui/emoji/emoji_window.py +154 -0
  181. gitpane/ui/emoji/file_action_icon.py +131 -0
  182. gitpane/ui/emoji/opentype_family_name.py +182 -0
  183. gitpane/ui/history_rewrite/__init__.py +0 -0
  184. gitpane/ui/history_rewrite/details.py +78 -0
  185. gitpane/ui/history_rewrite/preview.py +565 -0
  186. gitpane/ui/history_rewrite/preview_model.py +291 -0
  187. gitpane/ui/history_rewrite/question.py +87 -0
  188. gitpane/ui/history_rewrite/recovery_dialog.py +262 -0
  189. gitpane/ui/look/__init__.py +0 -0
  190. gitpane/ui/look/action_icon.py +504 -0
  191. gitpane/ui/look/application_icon.py +78 -0
  192. gitpane/ui/look/application_style.py +317 -0
  193. gitpane/ui/look/avatar_label.py +53 -0
  194. gitpane/ui/look/color_contrast.py +43 -0
  195. gitpane/ui/look/desktop_appearance_follower.py +118 -0
  196. gitpane/ui/look/fixed_font.py +21 -0
  197. gitpane/ui/look/gtk_settings.py +116 -0
  198. gitpane/ui/look/icon_density.py +16 -0
  199. gitpane/ui/look/reference_font.py +19 -0
  200. gitpane/ui/look/theme_color.py +63 -0
  201. gitpane/ui/look/theme_look.py +95 -0
  202. gitpane/ui/look/theme_palette.py +173 -0
  203. gitpane/ui/look/warning_icon.py +86 -0
  204. gitpane/ui/look/windows_control_look.py +250 -0
  205. gitpane/ui/reader/__init__.py +0 -0
  206. gitpane/ui/reader/commit_message.py +129 -0
  207. gitpane/ui/reader/commit_panel.py +378 -0
  208. gitpane/ui/reader/diff_panel.py +626 -0
  209. gitpane/ui/reader/file_action.py +138 -0
  210. gitpane/ui/reader/file_tree_panel.py +235 -0
  211. gitpane/ui/reader/reader_toolbar.py +56 -0
  212. gitpane/ui/reader/revision_tree.py +80 -0
  213. gitpane/ui/reader/search_result.py +165 -0
  214. gitpane/ui/reader/source_text.py +134 -0
  215. gitpane/ui/reader/source_view.py +151 -0
  216. gitpane/ui/reader/syntax_span.py +67 -0
  217. gitpane/ui/reference/__init__.py +0 -0
  218. gitpane/ui/reference/branch_selector.py +106 -0
  219. gitpane/ui/reference/checkout_dialog.py +419 -0
  220. gitpane/ui/reference/checkout_revision_dialog.py +182 -0
  221. gitpane/ui/reference/commit_summary.py +339 -0
  222. gitpane/ui/reference/create_branch_dialog.py +346 -0
  223. gitpane/ui/reference/create_tag_dialog.py +283 -0
  224. gitpane/ui/reference/delete_local_branch_dialog.py +323 -0
  225. gitpane/ui/reference/delete_tag_dialog.py +394 -0
  226. gitpane/ui/reference/form_opening.py +53 -0
  227. gitpane/ui/reference/reference_dialog.py +417 -0
  228. gitpane/ui/reference/reference_feedback.py +62 -0
  229. gitpane/ui/reference/reference_form.py +139 -0
  230. gitpane/ui/reference/rename_branch_dialog.py +179 -0
  231. gitpane/ui/reference/reset_branch_dialog.py +430 -0
  232. gitpane/ui/reference/reset_dialog.py +263 -0
  233. gitpane/ui/reference/revision_dialog.py +121 -0
  234. gitpane/ui/reference/revision_picker.py +227 -0
  235. gitpane/ui/reference/select_branches_dialog.py +199 -0
  236. gitpane/ui/reference/stash_dialog.py +171 -0
  237. gitpane/ui/repository/__init__.py +0 -0
  238. gitpane/ui/repository/graph_delegate.py +298 -0
  239. gitpane/ui/repository/history_avatar.py +130 -0
  240. gitpane/ui/repository/history_model.py +179 -0
  241. gitpane/ui/repository/repository_list_popup.py +319 -0
  242. gitpane/ui/repository/repository_window.py +2580 -0
  243. gitpane/ui/resource/ArrowDown.png +0 -0
  244. gitpane/ui/resource/ArrowUp.png +0 -0
  245. gitpane/ui/resource/BranchCheckout.png +0 -0
  246. gitpane/ui/resource/BranchCreate.png +0 -0
  247. gitpane/ui/resource/BranchDelete.png +0 -0
  248. gitpane/ui/resource/CopyToClipboard.png +0 -0
  249. gitpane/ui/resource/GitPane-16.png +0 -0
  250. gitpane/ui/resource/GitPane-24.png +0 -0
  251. gitpane/ui/resource/GitPane-256.png +0 -0
  252. gitpane/ui/resource/GitPane-32.png +0 -0
  253. gitpane/ui/resource/GitPane-48.png +0 -0
  254. gitpane/ui/resource/GitPane-64.png +0 -0
  255. gitpane/ui/resource/GitPane.ico +0 -0
  256. gitpane/ui/resource/NotoColorEmoji-OFL.txt +92 -0
  257. gitpane/ui/resource/NotoColorEmoji_WindowsCompatible.ttf +0 -0
  258. gitpane/ui/resource/Renamed.png +0 -0
  259. gitpane/ui/resource/ResetAnotherBranchToHere.png +0 -0
  260. gitpane/ui/resource/ResetCurrentBranchToHere.png +0 -0
  261. gitpane/ui/resource/Stash.png +0 -0
  262. gitpane/ui/resource/Tag.png +0 -0
  263. gitpane/ui/resource/TagCreate.png +0 -0
  264. gitpane/ui/resource/TagDelete.png +0 -0
  265. gitpane/ui/resource/Unmerged.png +0 -0
  266. gitpane/ui/resource/checkout.png +0 -0
  267. gitpane/ui/sequencer/__init__.py +0 -0
  268. gitpane/ui/sequencer/conflict_dialog.py +532 -0
  269. gitpane/ui/sequencer/merge_recovery_dialog.py +67 -0
  270. gitpane/ui/sequencer/rebase_dialog.py +461 -0
  271. gitpane/ui/sequencer/rebase_editor_window.py +220 -0
  272. gitpane/ui/sequencer/replay_conflict_prompt.py +105 -0
  273. gitpane/ui/sequencer/replay_dialog.py +501 -0
  274. gitpane/ui/setting/__init__.py +0 -0
  275. gitpane/ui/setting/account_page.py +724 -0
  276. gitpane/ui/setting/add_account_dialog.py +1005 -0
  277. gitpane/ui/setting/credential_manager_dialog.py +151 -0
  278. gitpane/ui/setting/git_setting_widget.py +388 -0
  279. gitpane/ui/setting/setting_block.py +19 -0
  280. gitpane/ui/setting/setting_dialog.py +750 -0
  281. gitpane/ui/setting/ssh_widget.py +232 -0
  282. gitpane/ui/setting/utility_page.py +240 -0
  283. gitpane/ui/staging/__init__.py +0 -0
  284. gitpane/ui/staging/staging_action.py +356 -0
  285. gitpane/ui/staging/staging_window.py +745 -0
  286. gitpane/ui/transport/__init__.py +0 -0
  287. gitpane/ui/transport/clone_dialog.py +879 -0
  288. gitpane/ui/transport/push_dialog.py +580 -0
  289. gitpane/ui/transport/push_rejection_prompt.py +100 -0
  290. gitpane/ui/transport/push_tracking_question.py +88 -0
  291. gitpane/ui/transport/remote_dialog.py +943 -0
  292. gitpane/ui/welcome/__init__.py +0 -0
  293. gitpane/ui/welcome/invalid_repository_prompt.py +114 -0
  294. gitpane/ui/welcome/welcome_window.py +1137 -0
  295. gitpane/ui/widget/__init__.py +0 -0
  296. gitpane/ui/widget/busy_indicator.py +106 -0
  297. gitpane/ui/widget/commit_date_text.py +55 -0
  298. gitpane/ui/widget/content_growth.py +262 -0
  299. gitpane/ui/widget/content_measure.py +99 -0
  300. gitpane/ui/widget/dialog_layout.py +668 -0
  301. gitpane/ui/widget/drop_down_wheel_filter.py +32 -0
  302. gitpane/ui/widget/flow_layout.py +110 -0
  303. gitpane/ui/widget/long_text.py +542 -0
  304. gitpane/ui/widget/mnemonic.py +13 -0
  305. gitpane/ui/widget/numbered_text_edit.py +127 -0
  306. gitpane/ui/widget/plain_label.py +15 -0
  307. gitpane/ui/widget/plain_tooltip.py +19 -0
  308. gitpane/ui/widget/refresh_advice.py +25 -0
  309. gitpane/ui/widget/result_label.py +227 -0
  310. gitpane/ui/widget/result_status_bar.py +53 -0
  311. gitpane/ui/widget/scrolled_text.py +69 -0
  312. gitpane/ui/widget/shortcut_text.py +31 -0
  313. gitpane/ui/widget/standard_icon_label.py +55 -0
  314. gitpane/ui/widget/system_shortcut.py +75 -0
  315. gitpane/ui/widget/unavailable_reason.py +29 -0
  316. gitpane/ui/widget/wheel_safe_drop_down.py +24 -0
  317. gitpane/ui/window_control/__init__.py +0 -0
  318. gitpane/ui/window_control/button_focus.py +42 -0
  319. gitpane/ui/window_control/compositor_answer.py +152 -0
  320. gitpane/ui/window_control/compositor_growth_cap.py +73 -0
  321. gitpane/ui/window_control/desktop_activation.py +203 -0
  322. gitpane/ui/window_control/dialog_modality.py +122 -0
  323. gitpane/ui/window_control/item_activation.py +42 -0
  324. gitpane/ui/window_control/modal_focus_relay.py +124 -0
  325. gitpane/ui/window_control/wayland_activation.py +695 -0
  326. gitpane/ui/window_control/window_placement.py +305 -0
  327. gitpane/vendor/DISTLIB-LICENSE.txt +284 -0
  328. gitpane/vendor/w32.exe +0 -0
  329. gitpane/vendor/w64-arm.exe +0 -0
  330. gitpane/vendor/w64.exe +0 -0
  331. gitpane/worktree/__init__.py +0 -0
  332. gitpane/worktree/command_sequence.py +110 -0
  333. gitpane/worktree/file_action_target.py +61 -0
  334. gitpane/worktree/file_menu_action.py +52 -0
  335. gitpane/worktree/historical_restore.py +361 -0
  336. gitpane/worktree/ignore_rule.py +266 -0
  337. gitpane/worktree/mutation.py +1539 -0
  338. gitpane/worktree/partial_patch.py +435 -0
  339. gitpane/worktree/staged_reset.py +447 -0
  340. gitpane/worktree/staging.py +1418 -0
  341. gitpane/worktree/trash.py +302 -0
  342. gitpane/worktree/worktree_file.py +406 -0
  343. gitpane/worktree/worktree_service.py +584 -0
  344. gitpane/worktree/worktree_snapshot.py +630 -0
  345. gitpane-1.0.0.dist-info/METADATA +141 -0
  346. gitpane-1.0.0.dist-info/RECORD +353 -0
  347. gitpane-1.0.0.dist-info/WHEEL +5 -0
  348. gitpane-1.0.0.dist-info/entry_points.txt +2 -0
  349. gitpane-1.0.0.dist-info/licenses/LICENSE +21 -0
  350. gitpane-1.0.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +109 -0
  351. gitpane-1.0.0.dist-info/licenses/src/gitpane/ui/resource/NotoColorEmoji-OFL.txt +92 -0
  352. gitpane-1.0.0.dist-info/licenses/src/gitpane/vendor/DISTLIB-LICENSE.txt +284 -0
  353. gitpane-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,461 @@
1
+ """Display native rebase progress without owning any Git command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from typing import TYPE_CHECKING
7
+
8
+ from PySide6.QtCore import Qt, Signal
9
+ from PySide6.QtGui import QPalette
10
+ from PySide6.QtWidgets import QAbstractItemView, QApplication, QCheckBox, QDialog, QHBoxLayout, QHeaderView, QPlainTextEdit, QPushButton, QTableWidget, QTableWidgetItem, QToolButton, QVBoxLayout, QWidget
11
+
12
+ from gitpane.config import DARK_BACKGROUND_LIGHTNESS_LIMIT, ERROR_DARK_COLOR, ERROR_LIGHT_COLOR, OPERATION_HEIGHT, OPERATION_WIDTH, SHORT_OID_LENGTH, SPACING_MD, SPACING_SM
13
+ from gitpane.operation.console_line import COMPLETED_STATUS_SET
14
+ from gitpane.preference.preference_store import REBASE_PROMPT_PREFERENCE_KEY
15
+ from gitpane.sequencer.rebase_state import REBASE_CONTINUABLE_STATE_SET, REBASE_STOPPED_STATE_SET, RebaseSnapshot, can_abort_rebase
16
+ from gitpane.ui.look.theme_look import follow_theme
17
+ from gitpane.ui.widget.commit_date_text import format_commit_date
18
+ from gitpane.ui.widget.content_growth import ContentGrowthMixin, OpeningFit
19
+ from gitpane.ui.widget.content_measure import LayoutContentMeasure
20
+ from gitpane.ui.widget.dialog_layout import content_height, fit_form
21
+ from gitpane.ui.widget.flow_layout import FlowLayout
22
+ from gitpane.ui.widget.long_text import WrappingLabel
23
+ from gitpane.ui.widget.scrolled_text import ScrolledText
24
+
25
+ if TYPE_CHECKING:
26
+ from collections.abc import Callable
27
+
28
+ from PySide6.QtCore import QRect
29
+
30
+ from gitpane.operation.operation_status import OperationStatus
31
+ from gitpane.preference.preference_store import PreferenceStore
32
+ from gitpane.sequencer.rebase_state import RebaseCommit, RebaseStep
33
+
34
+ REBASE_COLUMN_TITLES = ("Status", "Action", "Subject", "Author", "Date", "Commit hash")
35
+ REBASE_STATUS_COLUMN, REBASE_ACTION_COLUMN, REBASE_MESSAGE_COLUMN, REBASE_AUTHOR_COLUMN, REBASE_DATE_COLUMN, REBASE_COMMIT_COLUMN = range(len(REBASE_COLUMN_TITLES))
36
+ REBASE_READY_LIGHT_COLOR = "#176b35"
37
+ REBASE_READY_DARK_COLOR = "#75d69a"
38
+ REBASE_EDIT_LIGHT_COLOR = "#b45309"
39
+ REBASE_EDIT_DARK_COLOR = "#ffb454"
40
+ CONFLICT_BUTTON_BACKGROUND = "#ffe59a"
41
+ CONFLICT_BUTTON_HOVER_BACKGROUND = "#ffd166"
42
+ CONFLICT_BUTTON_PRESSED_BACKGROUND = "#e7b53d"
43
+ CONFLICT_BUTTON_FOREGROUND = "#332500"
44
+ CONFLICT_BUTTON_BORDER = "#b47700"
45
+ CONTINUE_BUTTON_BACKGROUND = "#c8efd4"
46
+ CONTINUE_BUTTON_HOVER_BACKGROUND = "#98e0ad"
47
+ CONTINUE_BUTTON_PRESSED_BACKGROUND = "#66c884"
48
+ CONTINUE_BUTTON_FOREGROUND = "#153c24"
49
+ CONTINUE_BUTTON_BORDER = "#408355"
50
+ REBASE_ACTION_BORDER_WIDTH = 1
51
+ REBASE_ACTION_CORNER_RADIUS = 2
52
+ DIAGNOSTIC_VISIBLE_LINE_COUNT = 4
53
+ TABLE_MINIMUM_ROW_COUNT = 3
54
+ # Width the Rebase current HEAD? confirmation opens at, so its warning reads as a paragraph instead of a narrow column
55
+ # of short lines; its height follows the wrapped text.
56
+ REBASE_CONFIRMATION_WIDTH = 520
57
+ REBASE_STATUS_BY_STATE = {
58
+ "conflict": "There are unresolved merge conflicts.",
59
+ "edit": "Rebase paused for editing. Amend the commit if needed, then continue.",
60
+ "paused": "Conflicts are resolved and staged. Continue the rebase.",
61
+ "intervention_required": "Rebase needs your attention. Review the diagnostics before continuing.",
62
+ "unknown": "Rebase state is unavailable. Refresh to try again.",
63
+ "external-change": "Rebase changed externally. Refresh to inspect the current state.",
64
+ "starting": "Rebase in progress.",
65
+ "absent": "Rebase finished.",
66
+ }
67
+
68
+
69
+ def rebase_command_sentence(status: OperationStatus) -> str:
70
+ """Write the result of a rebase command as the window shows it: first line of its diagnostics and, once no rebase is left, its status."""
71
+ return f"Rebase {status.value}."
72
+
73
+
74
+ def step_rows_signature(state: RebaseSnapshot) -> tuple[object, ...]:
75
+ """Tell what the rows of the step table show of a state; two states with the same signature give the same rows."""
76
+ return (state.done_steps, state.current_step, state.remaining_steps, state.opaque_steps, state.fresh)
77
+
78
+
79
+ class RebaseDialog(ContentGrowthMixin, QDialog):
80
+ """Show native instructions in one ordered table with explicit recovery actions.
81
+
82
+ The window has no scroll area: its height follows its own layout through the shared growth rule, so the
83
+ diagnostics field grows it when shown and gives the height back when hidden, while the table takes all the room
84
+ left above its minimum of three rows.
85
+ """
86
+
87
+ refresh_requested = Signal()
88
+ edit_todo_requested = Signal()
89
+ amend_requested = Signal(object)
90
+ continue_requested = Signal(object)
91
+ abort_requested = Signal(object)
92
+ solve_conflicts_requested = Signal()
93
+ commit_details_requested = Signal(object)
94
+ message_column = REBASE_MESSAGE_COLUMN
95
+
96
+ def __init__(self, state: RebaseSnapshot, parent: QWidget | None = None, *, clock: Callable[[], datetime] | None = None) -> None:
97
+ """Keep progress prominent and optional diagnostics collapsed by default.
98
+
99
+ The clock is injected so the relative dates of the step table can be reproduced, as the window gallery needs.
100
+ """
101
+ super().__init__(parent)
102
+ self.clock = clock or (lambda: datetime.now().astimezone())
103
+ self.busy = False
104
+ self.command_message = ""
105
+ # Status of the last rebase command, None before any; forgotten once a rebase is in progress again.
106
+ self.command_status: OperationStatus | None = None
107
+ self.state = state
108
+ self.shown_step_signature: tuple[object, ...] | None = None
109
+ self.current_step_raw: str | None = None
110
+ self.row_keys: list[tuple[str, str, int]] = []
111
+ self.commit_by_oid: dict[str, RebaseCommit] = {}
112
+ self.setWindowTitle("Rebase")
113
+ self.resize(OPERATION_WIDTH, OPERATION_HEIGHT)
114
+ layout = QVBoxLayout(self)
115
+ self.repository_label = WrappingLabel(state.key.git_dir)
116
+ self.repository_label.setWordWrap(True)
117
+ layout.addWidget(self.repository_label)
118
+ self.step_table = QTableWidget()
119
+ self.step_table.setAccessibleName("Rebase steps")
120
+ self.step_table.setColumnCount(len(REBASE_COLUMN_TITLES))
121
+ self.step_table.setHorizontalHeaderLabels(REBASE_COLUMN_TITLES)
122
+ self.step_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
123
+ self.step_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
124
+ self.step_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
125
+ self.step_table.setAlternatingRowColors(True)
126
+ self.step_table.setWordWrap(False)
127
+ self.step_table.verticalHeader().hide()
128
+ self.step_table.horizontalHeader().setSectionResizeMode(REBASE_MESSAGE_COLUMN, QHeaderView.ResizeMode.Stretch)
129
+ layout.addWidget(self.step_table, 1)
130
+ self.status_label = WrappingLabel()
131
+ self.status_label.setWordWrap(True)
132
+ # A refusal written by Git or a hook can be longer than the screen: it scrolls past the window's growth cap, so
133
+ # the buttons under it stay inside the window.
134
+ self.status_area = ScrolledText(self.status_label)
135
+ layout.addWidget(self.status_area)
136
+ self.diagnostic_button = QToolButton()
137
+ self.diagnostic_button.setCheckable(True)
138
+ self.diagnostic_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
139
+ self.diagnostic_button.toggled.connect(self._toggle_diagnostic)
140
+ layout.addWidget(self.diagnostic_button)
141
+ self.diagnostic_edit = QPlainTextEdit()
142
+ self.diagnostic_edit.setReadOnly(True)
143
+ self.diagnostic_edit.setAccessibleName("Rebase diagnostics")
144
+ layout.addWidget(self.diagnostic_edit)
145
+ self._toggle_diagnostic(False)
146
+ self.button_flow = FlowLayout(spacing_px=SPACING_SM)
147
+ self.refresh_button = QPushButton("Refresh")
148
+ self.refresh_button.clicked.connect(self._refresh_clicked)
149
+ self.edit_todo_button = QPushButton("Edit rebase instructions…")
150
+ self.edit_todo_button.clicked.connect(self.edit_todo_requested)
151
+ self.amend_button = QPushButton("Amend commit…")
152
+ self.amend_button.clicked.connect(lambda: self.amend_requested.emit(self.state))
153
+ self.continue_button = QPushButton("Continue rebase")
154
+ self.continue_button.clicked.connect(lambda: self.continue_requested.emit(self.state))
155
+ self.abort_button = QPushButton("Abort")
156
+ self.abort_button.clicked.connect(lambda: self.abort_requested.emit(self.state))
157
+ self.solve_conflicts_button = QPushButton("Solve conflicts")
158
+ self.solve_conflicts_button.clicked.connect(self.solve_conflicts_requested)
159
+ self.close_button = QPushButton("Close")
160
+ self.close_button.clicked.connect(self.reject)
161
+ self.buttons = [self.refresh_button, self.edit_todo_button, self.amend_button, self.solve_conflicts_button, self.continue_button, self.abort_button, self.close_button]
162
+ for button in self.buttons:
163
+ # Enter must never trigger Abort or Continue from a table; each command stays an explicit click or Space on its focused button.
164
+ button.setAutoDefault(False)
165
+ self.button_flow.addWidget(button)
166
+ layout.addLayout(self.button_flow)
167
+ self.show_state(state)
168
+ self.refresh_metrics()
169
+ extra_row_count = max(0, self.step_table.rowCount() - TABLE_MINIMUM_ROW_COUNT)
170
+ initial_height = layout.totalMinimumHeightForWidth(self.width()) + extra_row_count * self.step_table.verticalHeader().defaultSectionSize()
171
+ self.resize(self.width(), min(OPERATION_HEIGHT, initial_height))
172
+ self.install_content_growth(LayoutContentMeasure(self), OpeningFit.GROW_ONLY, self.height())
173
+
174
+ def show_state(self, state: RebaseSnapshot) -> None:
175
+ """Retain the latest state, set labels and buttons again, and rebuild the rows only when what they show changed.
176
+
177
+ The selection is preserved unless the current instruction advances. With no rebase left, a last command that
178
+ did not succeed gives the status ("Rebase failed."), the sentence its diagnostics already start with: a rebase
179
+ that failed to start or stopped on an error leaves no state behind, and "Rebase finished." would contradict the
180
+ console, which reports the failure.
181
+ """
182
+ self.state = state
183
+ if state.state != "absent":
184
+ self.command_status = None
185
+ status = REBASE_STATUS_BY_STATE.get(state.state, "Rebase: " + state.state)
186
+ if state.state == "absent" and state.diagnostic:
187
+ status = state.diagnostic
188
+ elif state.state == "absent" and self.command_status is not None and self.command_status not in COMPLETED_STATUS_SET:
189
+ status = rebase_command_sentence(self.command_status)
190
+ if not state.fresh:
191
+ status = "State is not fresh. Refresh before taking action."
192
+ diagnostic_text = "\n".join(filter(None, (state.diagnostic, self.command_message)))
193
+ self.status_label.setText(status)
194
+ self.diagnostic_edit.setPlainText(diagnostic_text)
195
+ self.diagnostic_button.setVisible(bool(self.diagnostic_edit.toPlainText()))
196
+ self.diagnostic_edit.setVisible(self.diagnostic_button.isChecked() and bool(self.diagnostic_edit.toPlainText()))
197
+ # The state is published again at every observation. Rebuilding the table would spend time on the thread of the
198
+ # interface and ask the same commit details again, while the retained state must always be the last one
199
+ # received, because Continue and Abort send it.
200
+ signature = step_rows_signature(state)
201
+ rows_changed = signature != self.shown_step_signature
202
+ if rows_changed:
203
+ self._show_steps()
204
+ self.shown_step_signature = signature
205
+ self.set_busy(self.busy)
206
+ if rows_changed:
207
+ self.request_commit_details()
208
+
209
+ def request_commit_details(self) -> None:
210
+ """Ask for the author and date of every shown step.
211
+
212
+ The window shows its first state while it is built, before anybody listens to this request, so whoever
213
+ listens afterwards asks once here; an equal state shown later keeps its rows and does not ask again.
214
+ """
215
+ state = self.state
216
+ steps = state.done_steps + state.remaining_steps
217
+ if state.current_step is not None:
218
+ steps += (state.current_step,)
219
+ self.commit_details_requested.emit(steps)
220
+
221
+ def _refresh_clicked(self) -> None:
222
+ """Ask to read the rebase again, and show the rows and the commit details again even when Git gives the same steps.
223
+
224
+ Showing the details again retries a details read that failed.
225
+ """
226
+ self.shown_step_signature = None
227
+ self.refresh_requested.emit()
228
+
229
+ def show_commits(self, commits: tuple[RebaseCommit, ...]) -> None:
230
+ """Fill display-only details without changing the observed rebase generation or action state."""
231
+ for commit in commits:
232
+ self.commit_by_oid[commit.oid] = commit
233
+ self._show_steps()
234
+
235
+ def show_command_result(self, status: OperationStatus, cause: str = "") -> None:
236
+ """Retain command failures in expandable diagnostics and expose their cause immediately.
237
+
238
+ The status is kept too, so a failed command that left no rebase is not reported as finished.
239
+ """
240
+ self.command_status = status
241
+ self.command_message = rebase_command_sentence(status) + (" " + cause if cause else "")
242
+ self.show_state(self.state)
243
+ if cause and self.state.state != "conflict":
244
+ self.diagnostic_button.setChecked(True)
245
+
246
+ def set_busy(self, busy: bool) -> None:
247
+ """Keep all write actions disabled while another command or tool owns the worktree."""
248
+ self.busy = busy
249
+ state = self.state
250
+ available = not busy and state.fresh and state.identity is not None
251
+ self.amend_button.setEnabled(available and state.state == "edit")
252
+ self.continue_button.setEnabled(available and state.state in REBASE_CONTINUABLE_STATE_SET)
253
+ self.abort_button.setEnabled(not busy and can_abort_rebase(state))
254
+ self.solve_conflicts_button.setEnabled(state.fresh and state.identity is not None and state.state == "conflict")
255
+ self.edit_todo_button.setEnabled(available and state.identity.backend == "rebase-merge"
256
+ and state.state in REBASE_STOPPED_STATE_SET)
257
+ self._style_recovery()
258
+
259
+ def _show_steps(self) -> None:
260
+ """Keep native order and distinguish unsupported instructions without inventing their progress."""
261
+ table = self.step_table
262
+ selected_row = table.currentRow()
263
+ selected_key = self.row_keys[selected_row] if 0 <= selected_row < len(self.row_keys) else None
264
+ selected_column = max(REBASE_STATUS_COLUMN, table.currentColumn())
265
+ scroll_value = table.verticalScrollBar().value()
266
+ rows: list[tuple[str, RebaseStep]] = []
267
+ for step in self.state.done_steps:
268
+ rows.append(("Dropped" if step.action == "drop" else "Applied", step))
269
+ current_row = len(rows) if self.state.current_step is not None else None
270
+ current_raw = self.state.current_step.raw if self.state.current_step is not None else None
271
+ if self.state.current_step is not None:
272
+ rows.append(("Applying…", self.state.current_step))
273
+ for step in self.state.remaining_steps:
274
+ rows.append(("Remaining", step))
275
+ for step in self.state.opaque_steps:
276
+ rows.append(("Unknown", step))
277
+ commit_by_oid: dict[str, RebaseCommit] = {}
278
+ abbreviation_length_set = set()
279
+ for _status, step in rows:
280
+ if step.oid:
281
+ abbreviation_length_set.add(len(step.oid))
282
+ for commit in self.commit_by_oid.values():
283
+ for length in abbreviation_length_set:
284
+ commit_by_oid[commit.oid[:length]] = commit
285
+ now = self.clock()
286
+ table.blockSignals(True)
287
+ table.clearContents()
288
+ table.setRowCount(len(rows))
289
+ self.row_keys = []
290
+ occurrence_by_key: dict[tuple[str, str], int] = {}
291
+ restored_row = None
292
+ for row, (status, step) in enumerate(rows):
293
+ key = (status, step.raw)
294
+ occurrence = occurrence_by_key.get(key, 0)
295
+ occurrence_by_key[key] = occurrence + 1
296
+ row_key = (status, step.raw, occurrence)
297
+ self.row_keys.append(row_key)
298
+ commit = commit_by_oid.get(step.oid)
299
+ date = format_commit_date(commit.author_timestamp, now, relative_only=True) if commit is not None else "—"
300
+ author = commit.author_name if commit is not None else "—"
301
+ texts = (status, step.action, step.message, author, date, step.oid[:SHORT_OID_LENGTH])
302
+ for column, text in enumerate(texts):
303
+ item = QTableWidgetItem(text)
304
+ tooltip = text
305
+ if column == REBASE_DATE_COLUMN and commit is not None:
306
+ tooltip = format_commit_date(commit.author_timestamp, now)
307
+ elif column == REBASE_COMMIT_COLUMN:
308
+ tooltip = commit.oid if commit is not None else step.oid
309
+ elif column == REBASE_STATUS_COLUMN and (step.opaque or not self.state.fresh):
310
+ tooltip = "Execution state unknown." if step.opaque else "Last observed state; refresh to verify progress."
311
+ item.setToolTip(tooltip)
312
+ font = item.font()
313
+ font.setBold(row == current_row)
314
+ item.setFont(font)
315
+ table.setItem(row, column, item)
316
+ if row_key == selected_key:
317
+ restored_row = row
318
+ current_changed = current_raw != self.current_step_raw
319
+ self.current_step_raw = current_raw
320
+ target_row = current_row if current_changed or restored_row is None else restored_row
321
+ if target_row is not None:
322
+ table.setCurrentCell(target_row, selected_column)
323
+ elif rows:
324
+ first_row = 0
325
+ table.setCurrentCell(first_row, selected_column)
326
+ table.blockSignals(False)
327
+ if current_changed and current_row is not None:
328
+ table.scrollToItem(table.item(current_row, REBASE_STATUS_COLUMN), QAbstractItemView.ScrollHint.PositionAtCenter)
329
+ else:
330
+ table.verticalScrollBar().setValue(scroll_value)
331
+
332
+ def _toggle_diagnostic(self, expanded: bool) -> None:
333
+ """Reveal command diagnostics only when requested, without a second instruction display.
334
+
335
+ The window follows the field both ways through the shared growth rule: showing it grows the window until no
336
+ row covers its neighbour, hiding it brings back the height the window had before.
337
+ """
338
+ self.diagnostic_button.setText("Hide diagnostics" if expanded else "Show diagnostics")
339
+ self.diagnostic_button.setArrowType(Qt.ArrowType.DownArrow if expanded else Qt.ArrowType.RightArrow)
340
+ self.diagnostic_edit.setVisible(expanded)
341
+
342
+ def _style_action_button(self, button: QPushButton, *, active: bool, background: str, hover_background: str, pressed_background: str, foreground: str, border: str) -> None:
343
+ """Share normal button sizing and pointer feedback across highlighted recovery actions."""
344
+ button_style = (f"QPushButton {{ background-color: {background}; color: {foreground}; "
345
+ f"border: {REBASE_ACTION_BORDER_WIDTH}px solid {border}; border-radius: {REBASE_ACTION_CORNER_RADIUS}px; padding: 0 {SPACING_SM}px; }}"
346
+ f"QPushButton:hover {{ background-color: {hover_background}; }}"
347
+ f"QPushButton:pressed {{ background-color: {pressed_background}; }}"
348
+ "QPushButton:focus { border-style: dashed; }") if active else ""
349
+ if button.styleSheet() != button_style:
350
+ button.setStyleSheet(button_style)
351
+
352
+ def _style_recovery(self) -> None:
353
+ """Highlight edit pauses, conflict resolution and readiness to continue after staging."""
354
+ conflict = self.solve_conflicts_button.isEnabled()
355
+ ready = self.state.state == "paused" and self.continue_button.isEnabled()
356
+ self._style_action_button(self.solve_conflicts_button, active=conflict, background=CONFLICT_BUTTON_BACKGROUND, hover_background=CONFLICT_BUTTON_HOVER_BACKGROUND,
357
+ pressed_background=CONFLICT_BUTTON_PRESSED_BACKGROUND, foreground=CONFLICT_BUTTON_FOREGROUND, border=CONFLICT_BUTTON_BORDER)
358
+ self._style_action_button(self.continue_button, active=ready, background=CONTINUE_BUTTON_BACKGROUND, hover_background=CONTINUE_BUTTON_HOVER_BACKGROUND,
359
+ pressed_background=CONTINUE_BUTTON_PRESSED_BACKGROUND, foreground=CONTINUE_BUTTON_FOREGROUND, border=CONTINUE_BUTTON_BORDER)
360
+ dark = self.palette().color(QPalette.ColorRole.Window).lightnessF() < DARK_BACKGROUND_LIGHTNESS_LIMIT
361
+ status_style = ""
362
+ if conflict:
363
+ color = ERROR_DARK_COLOR if dark else ERROR_LIGHT_COLOR
364
+ status_style = f"color: {color}; font-weight: bold;"
365
+ elif ready:
366
+ color = REBASE_READY_DARK_COLOR if dark else REBASE_READY_LIGHT_COLOR
367
+ status_style = f"color: {color}; font-weight: bold;"
368
+ elif self.amend_button.isEnabled():
369
+ color = REBASE_EDIT_DARK_COLOR if dark else REBASE_EDIT_LIGHT_COLOR
370
+ status_style = f"color: {color}; font-weight: bold;"
371
+ if self.status_label.styleSheet() != status_style:
372
+ self.status_label.setStyleSheet(status_style)
373
+
374
+ def refresh_metrics(self, available: QRect | None = None) -> None:
375
+ """Give the table all remaining space and keep actions reachable at large font sizes."""
376
+ table = self.step_table
377
+ metrics = table.fontMetrics()
378
+ row_height = metrics.height() + SPACING_SM
379
+ table.verticalHeader().setDefaultSectionSize(row_height)
380
+ table.setMinimumHeight(row_height * TABLE_MINIMUM_ROW_COUNT + table.horizontalHeader().sizeHint().height() + table.frameWidth() * 2 + table.horizontalScrollBar().sizeHint().height())
381
+ for column, sample in ((REBASE_STATUS_COLUMN, "Applying…"), (REBASE_ACTION_COLUMN, "reword"), (REBASE_AUTHOR_COLUMN, "Author name here"), (REBASE_DATE_COLUMN, "12 months ago"), (REBASE_COMMIT_COLUMN, "a" * SHORT_OID_LENGTH)):
382
+ table.setColumnWidth(column, metrics.horizontalAdvance(sample) + SPACING_MD)
383
+ self.diagnostic_edit.setFixedHeight(metrics.height() * DIAGNOSTIC_VISIBLE_LINE_COUNT + SPACING_MD)
384
+ self._style_recovery()
385
+ fit_form(self, available)
386
+
387
+
388
+ class RebaseConfirmation(ContentGrowthMixin, QDialog):
389
+ """Confirm rewriting the captured HEAD onto the selected graph target once.
390
+
391
+ The window has no scroll area: its height follows its own layout through the shared growth rule, so an error line
392
+ grows it to show every line and gives the height back once the line is empty.
393
+ """
394
+
395
+ submit_requested = Signal()
396
+
397
+ def __init__(self, source: str, target: str, parent: QWidget | None = None, *, preference: PreferenceStore | None = None) -> None:
398
+ """Name both endpoints in plain text while keeping cancellation non-writing."""
399
+ super().__init__(parent)
400
+ self.preference = preference
401
+ self.submitting = False
402
+ self.cancelled = False
403
+ self.rejected.connect(self._cancel)
404
+ self.setWindowTitle("Rebase current HEAD?")
405
+ layout = QVBoxLayout(self)
406
+ # Wrapping labels break the long commit IDs and branch names too, and report the height their text needs at
407
+ # the width they get, so the window can grow to show every line.
408
+ self.message_label = WrappingLabel(f"Rebase {source} onto {target}? This rewrites commit history. Merge commits are flattened; merge-only resolutions may not be preserved.")
409
+ self.message_label.setWordWrap(True)
410
+ layout.addWidget(self.message_label)
411
+ self.dont_show_again = QCheckBox("Don't show again")
412
+ layout.addWidget(self.dont_show_again)
413
+ self.error_label = WrappingLabel()
414
+ self.error_label.setWordWrap(True)
415
+ layout.addWidget(self.error_label)
416
+ buttons = QHBoxLayout()
417
+ self.cancel_button = QPushButton("Cancel rebase")
418
+ self.cancel_button.clicked.connect(self.reject)
419
+ self.confirm_button = QPushButton("Rebase")
420
+ self.confirm_button.clicked.connect(self.confirm)
421
+ buttons.addWidget(self.cancel_button)
422
+ buttons.addWidget(self.confirm_button)
423
+ layout.addLayout(buttons)
424
+ self.refresh_metrics()
425
+ self.resize(REBASE_CONFIRMATION_WIDTH, self.height())
426
+ self.resize(REBASE_CONFIRMATION_WIDTH, content_height(self))
427
+ self.install_content_growth(LayoutContentMeasure(self), OpeningFit.GROW_ONLY, self.height())
428
+ follow_theme(self, self.refresh_metrics)
429
+
430
+ def confirm(self) -> None:
431
+ """Publish the suppression choice before requesting the captured rebase.
432
+
433
+ A choice that cannot be saved keeps the confirmation open with an error line, which the shared growth rule gives
434
+ room to; a new try that succeeds starts the rebase.
435
+ """
436
+ if self.submitting:
437
+ return
438
+ try:
439
+ if self.preference is not None:
440
+ self.preference.update({REBASE_PROMPT_PREFERENCE_KEY: self.dont_show_again.isChecked()})
441
+ except (OSError, ValueError, TypeError):
442
+ self.error_label.setText("Could not save this choice. The confirmation remains enabled. Fix the settings error and try again.")
443
+ return
444
+ self.submit_requested.emit()
445
+
446
+ def _cancel(self) -> None:
447
+ """Invalidate asynchronous validation even when confirmation was suppressed."""
448
+ self.cancelled = True
449
+
450
+ def refresh_metrics(self, available: QRect | None = None) -> None:
451
+ """Keep the confirmation readable and its errors prominent on light and dark backgrounds, again after each appearance switch.
452
+
453
+ The error color is decided on the application palette: when the palette changes, the window's own palette can
454
+ still be the old one.
455
+ """
456
+ dark = QApplication.palette().color(QPalette.ColorRole.Window).lightnessF() < DARK_BACKGROUND_LIGHTNESS_LIMIT
457
+ color = ERROR_DARK_COLOR if dark else ERROR_LIGHT_COLOR
458
+ error_style = f"color: {color}; font-weight: bold;"
459
+ if self.error_label.styleSheet() != error_style:
460
+ self.error_label.setStyleSheet(error_style)
461
+ fit_form(self, available)