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,179 @@
1
+ """Reproduce the Git Extensions "Rename branch" window: one row with the new name, then a Rename button."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from PySide6.QtCore import QMargins, QRect, QSize, Qt, Signal
8
+ from PySide6.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QVBoxLayout, QWidget
9
+
10
+ from gitpane.config import LOCAL_PREFIX
11
+ from gitpane.operation.console_line import UNCHANGED_OUTCOME_STATUS_SET
12
+ from gitpane.operation.operation_status import OperationStatus
13
+ from gitpane.reference.reference_edit import BranchIntent, BranchMode
14
+ from gitpane.text.typed_text import typed_value
15
+ from gitpane.ui.look.theme_look import follow_theme
16
+ from gitpane.ui.look.windows_control_look import apply_windows_button_look, apply_windows_line_edit_look
17
+ from gitpane.ui.reference.reference_form import ReferenceFormDialog
18
+ from gitpane.ui.widget.dialog_layout import fit_caption_column, fit_capture_control, install_plain_form_palette
19
+
20
+ if TYPE_CHECKING:
21
+ from gitpane.operation.repository_operation import OperationOutcome
22
+ from gitpane.reference.reference_snapshot import ReferenceSnapshot
23
+
24
+ RENAME_BRANCH_TITLE = "Rename branch"
25
+ NEW_NAME_CAPTION = "New name"
26
+ RENAME_BUTTON_TEXT = "Rename"
27
+ # Client area of the Git Extensions capture, 484 x 42 inside the window borders and title bar.
28
+ RENAME_BRANCH_CLIENT_SIZE = QSize(484, 42)
29
+ # Measured on the capture in client pixels at 100 %, visible borders included: "New name" ink x 6-60, y 14-22; name
30
+ # field (72, 9, 344, 23), widened with the window; Rename button (423, 8, 57, 26), 4 px from the right edge.
31
+ NAME_FIELD_RECT = QRect(72, 9, 344, 23)
32
+ RENAME_BUTTON_RECT = QRect(423, 8, 57, 26)
33
+ RENAME_BUTTON_SIZE = RENAME_BUTTON_RECT.size()
34
+ NEW_NAME_CAPTION_LEFT = 6
35
+ # Width of the caption, from its left edge to the name field; a wider caption text widens it.
36
+ NEW_NAME_CAPTION_WIDTH = NAME_FIELD_RECT.left() - NEW_NAME_CAPTION_LEFT
37
+ # The row's margins: the caption's left edge, the button's top edge and its distance to the right and bottom edges.
38
+ RENAME_BRANCH_MARGINS = QMargins(NEW_NAME_CAPTION_LEFT, RENAME_BUTTON_RECT.top(), RENAME_BRANCH_CLIENT_SIZE.width() - RENAME_BUTTON_RECT.right() - 1,
39
+ RENAME_BRANCH_CLIENT_SIZE.height() - RENAME_BUTTON_RECT.bottom() - 1)
40
+ # The field sits 1 px lower than the button in the capture.
41
+ NAME_FIELD_DROP = NAME_FIELD_RECT.top() - RENAME_BUTTON_RECT.top()
42
+ # The caption's top below the button's top, fitted so that Qt's text lands on the captured ink rows (y 14-22) in the
43
+ # Fusion style; the capture's text sits a little above the field's middle.
44
+ CAPTION_DROP = 2
45
+ NAME_FIELD_TO_BUTTON_SPACE = RENAME_BUTTON_RECT.left() - NAME_FIELD_RECT.right() - 1
46
+
47
+
48
+ class RenameBranchDialog(ReferenceFormDialog):
49
+ """Rename one local branch, like the Git Extensions Rename branch window.
50
+
51
+ The window opens on the branch's current name, selected so typing replaces it, and only freezes a request once
52
+ the name differs. It needs no commit details, so opening it reads nothing beyond the references. There is no
53
+ Cancel button: Escape and the title bar close the window. Git's refusal or failure appears in a message box, as
54
+ Git Extensions shows the error of ``git branch -m``.
55
+ """
56
+
57
+ branch_requested = Signal(object)
58
+
59
+ def __init__(self, session_id: str, full_ref: str, target_oid: str, parent: QWidget | None = None) -> None:
60
+ """Build the single row of the capture, with the branch's current name selected in the field."""
61
+ super().__init__(session_id, parent)
62
+ self.intent: BranchIntent | None = None
63
+ self.setWindowTitle(RENAME_BRANCH_TITLE)
64
+ self.full_ref = full_ref
65
+ self.target_oid = target_oid
66
+ self.current_name = full_ref.removeprefix(LOCAL_PREFIX)
67
+ install_plain_form_palette(self)
68
+ layout = QHBoxLayout(self)
69
+ layout.setContentsMargins(RENAME_BRANCH_MARGINS)
70
+ layout.setSpacing(0)
71
+ self.caption_label = QLabel(NEW_NAME_CAPTION)
72
+ self.name_edit = QLineEdit(self.current_name)
73
+ self.name_edit.setAccessibleName(NEW_NAME_CAPTION)
74
+ self.caption_label.setBuddy(self.name_edit)
75
+ caption_column = QVBoxLayout()
76
+ caption_column.setContentsMargins(0, CAPTION_DROP, 0, 0)
77
+ caption_column.setSpacing(0)
78
+ caption_column.addWidget(self.caption_label)
79
+ caption_column.addStretch(1)
80
+ name_column = QVBoxLayout()
81
+ name_column.setContentsMargins(0, NAME_FIELD_DROP, 0, 0)
82
+ name_column.setSpacing(0)
83
+ name_column.addWidget(self.name_edit)
84
+ name_column.addStretch(1)
85
+ self.submit_button.setText(RENAME_BUTTON_TEXT)
86
+ self.submit_button.setEnabled(False)
87
+ self.apply_theme_look()
88
+ follow_theme(self, self.apply_theme_look)
89
+ layout.addLayout(caption_column)
90
+ layout.addLayout(name_column, 1)
91
+ layout.addSpacing(NAME_FIELD_TO_BUTTON_SPACE)
92
+ layout.addWidget(self.submit_button, 0, Qt.AlignmentFlag.AlignTop)
93
+ # Enter runs Rename from anywhere in the form. The role is set only once the button belongs to the dialog, so Qt
94
+ # records it as the dialog's main default.
95
+ self.submit_button.setDefault(True)
96
+ self.submit_button.clicked.connect(self._submit)
97
+ self.name_edit.textChanged.connect(self._update_intent)
98
+ self.resize(RENAME_BRANCH_CLIENT_SIZE)
99
+ # Git Extensions gives this window the same minimum and maximum height: it only grows in width.
100
+ self.single_row_height = RENAME_BRANCH_CLIENT_SIZE.height()
101
+ self.refresh_metrics()
102
+ self.name_edit.selectAll()
103
+ self.name_edit.setFocus()
104
+
105
+ def apply_theme_look(self) -> None:
106
+ """Lay the light or dark look of the name field and Rename, decided on the application palette; called again at each switch.
107
+
108
+ Only style sheets and palettes change: the typed name and its selection stay as they are.
109
+ """
110
+ apply_windows_line_edit_look(self.name_edit)
111
+ apply_windows_button_look(self.submit_button)
112
+
113
+ def fit_capture_controls(self) -> None:
114
+ """Give the caption and the Rename button the capture's sizes, or more when their text needs it; a wider caption moves the field right."""
115
+ fit_caption_column((self.caption_label,), NEW_NAME_CAPTION_WIDTH)
116
+ fit_capture_control(self.submit_button, RENAME_BUTTON_SIZE)
117
+
118
+ def populate(self, snapshot: ReferenceSnapshot, full_ref: str, target_oid: str) -> None:
119
+ """Bind the window to the references it was opened on; a name the user already typed is kept."""
120
+ if self.closed or self.busy or self.completed:
121
+ return
122
+ self.snapshot = snapshot
123
+ self.full_ref = full_ref
124
+ self.target_oid = target_oid
125
+ self._update_intent()
126
+
127
+ def _update_intent(self) -> None:
128
+ """Freeze the typed name into a rename request once it is not empty and differs from the current name.
129
+
130
+ The name is taken without the blanks around it; the field keeps what was typed.
131
+ """
132
+ self.intent = None
133
+ name = typed_value(self.name_edit.text())
134
+ if self.snapshot is not None and name and name != self.current_name and not self.busy and not self.completed:
135
+ try:
136
+ self.intent = BranchIntent.capture(self.snapshot, self.target_oid, session_id=self.session_id, name=name, mode=BranchMode.RENAME, full_ref=self.full_ref)
137
+ except ValueError:
138
+ self.intent = None
139
+ self.submit_button.setToolTip("")
140
+ self.submit_button.setEnabled(self.intent is not None and not self.closed)
141
+
142
+ def _submit(self) -> None:
143
+ """Freeze the form and hand its request to the reference form controller once."""
144
+ if self.closed or self.busy or self.completed or self.intent is None or not self.submit_button.isEnabled():
145
+ return
146
+ self.busy = True
147
+ self.name_edit.setEnabled(False)
148
+ self.submit_button.setEnabled(False)
149
+ self.branch_requested.emit(self.intent)
150
+
151
+ def show_outcome(self, outcome: OperationOutcome) -> None:
152
+ """Close after a successful rename; otherwise explain Git's answer in a message box.
153
+
154
+ A refused or failed rename left the branch as it was, so the typed name stays editable for another try. After
155
+ a partial or unknown result the branch may already carry the new name, so the form only shows what was asked.
156
+ """
157
+ self.busy = False
158
+ if outcome.status == OperationStatus.SUCCESS:
159
+ self.accept()
160
+ return
161
+ if outcome.status in UNCHANGED_OUTCOME_STATUS_SET:
162
+ self.name_edit.setEnabled(True)
163
+ self._update_intent()
164
+ else:
165
+ self.completed = True
166
+ self.name_edit.setEnabled(False)
167
+ self.show_error(outcome_text(outcome))
168
+
169
+
170
+ def outcome_text(outcome: OperationOutcome) -> str:
171
+ """Write each step's result and Git's diagnostic, one per line, for the message box of an unsuccessful rename."""
172
+ lines: list[str] = []
173
+ for step in outcome.steps:
174
+ lines.append(step.operation + ": " + step.status.value + " — " + step.effect)
175
+ if outcome.diagnostic is not None:
176
+ lines.append(outcome.diagnostic.detail)
177
+ if not lines:
178
+ lines.append(RENAME_BRANCH_TITLE + ": " + outcome.status.value)
179
+ return "\n".join(lines)
@@ -0,0 +1,430 @@
1
+ """Reproduce the Git Extensions "Reset branch" window (Reset another branch to here): a local branch, the commit box, two options."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from PySide6.QtCore import QMargins, QRect, QSignalBlocker, QSize, Qt, Signal
8
+ from PySide6.QtWidgets import QApplication, QComboBox, QHBoxLayout, QLabel, QPushButton, QStyle, QVBoxLayout, QWidget
9
+
10
+ from gitpane.config import COMMIT_METADATA_UNAVAILABLE, OK_BUTTON_TEXT, fit_window
11
+ from gitpane.operation.console_line import UNCHANGED_OUTCOME_STATUS_SET
12
+ from gitpane.operation.operation_status import OperationStatus
13
+ from gitpane.reference.reference_edit import BranchIntent, BranchMode
14
+ from gitpane.reference.reference_snapshot import short_reference_name
15
+ from gitpane.ui.look.theme_color import is_dark_palette
16
+ from gitpane.ui.look.theme_look import follow_theme
17
+ from gitpane.ui.look.windows_control_look import WINDOWS_BUTTON_BORDER_WIDTH, WINDOWS_BUTTON_DISABLED_BORDER_COLOR, apply_windows_button_look, apply_windows_combo_look, windows_button_style_sheet
18
+ from gitpane.ui.reference.commit_summary import GIT_EXTENSIONS_COMPACT_LAST_ROW_HEIGHT, CommitSummary
19
+ from gitpane.ui.reference.reference_form import ReferenceFormDialog
20
+ from gitpane.ui.widget.dialog_layout import install_plain_form_palette
21
+ from gitpane.ui.widget.long_text import ElidedCheckBox
22
+ from gitpane.ui.widget.standard_icon_label import StandardIconLabel
23
+
24
+ if TYPE_CHECKING:
25
+ from collections.abc import Callable
26
+ from datetime import datetime
27
+
28
+ from gitpane.history.repository_value import Commit
29
+ from gitpane.operation.repository_operation import OperationOutcome
30
+ from gitpane.reference.reference_snapshot import ReferenceSnapshot
31
+
32
+ RESET_BRANCH_TITLE = "Reset branch"
33
+ WARNING_FIRST_LINE = "You can only reset a branch safely if there is a direct path from it to selected revision."
34
+ WARNING_SECOND_LINE = "Forcing a branch to reset if it has not been merged might leave some commits unreachable."
35
+ RESET_LOCAL_BRANCH_CAPTION = "Reset local branch:"
36
+ CHECKOUT_AFTER_RESET_LABEL = "Checkout branch after reset"
37
+ FORCE_RESET_LABEL = "Force reset for a non-fast-forward reset"
38
+ CANCEL_BUTTON_TEXT = "Cancel"
39
+ NO_AVAILABLE_BRANCH_CAUSE = "No other available local branches"
40
+ NON_FAST_FORWARD_CAUSE = "This reset is not a fast-forward. Check Force reset to continue."
41
+ # A disabled OK turns this red while the chosen reset is not a fast-forward and Force is unchecked, so the user sees
42
+ # why OK cannot be pressed. GitPane shows this state since its first Reset branch window; the capture, taken without a
43
+ # chosen branch, does not show it.
44
+ NON_FAST_FORWARD_BUTTON_COLOR = "#ff8080"
45
+ NON_FAST_FORWARD_BUTTON_TEXT_COLOR = "#000000"
46
+ # The border makes Qt fill the whole face with the red in a dark theme too, where the native button would only tint it.
47
+ NON_FAST_FORWARD_BUTTON_STYLE = (f" QPushButton:disabled {{ background-color: {NON_FAST_FORWARD_BUTTON_COLOR}; color: {NON_FAST_FORWARD_BUTTON_TEXT_COLOR};"
48
+ f" border: {WINDOWS_BUTTON_BORDER_WIDTH}px solid {WINDOWS_BUTTON_DISABLED_BORDER_COLOR}; }}")
49
+ # Client area of the Git Extensions capture, 545 x 347 inside the window borders and title bar.
50
+ RESET_BRANCH_CLIENT_SIZE = QSize(545, 347)
51
+ # Measured on the capture in client pixels at 100 %, visible borders included: warning icon 16 x 16 at (16, 16) and
52
+ # its two text lines from x 41, ink y 16-42; "Reset local branch:" ink x 17-114, y 58-66; branch list
53
+ # (19, 73, 507, 23); commit frame (12, 108, 521, 141) with its title ink from x 21; check box squares at (14, 256) and
54
+ # (14, 281); OK (342, 307, 89, 23) and Cancel (439, 307, 89, 23), 8 px apart, 17 px from the right and bottom edges.
55
+ WARNING_ICON_RECT = QRect(16, 16, 16, 16)
56
+ WARNING_TEXT_LEFT = 40
57
+ RESET_BRANCH_LIST_RECT = QRect(19, 73, 507, 23)
58
+ COMMIT_FRAME_RECT = QRect(12, 108, 521, 141)
59
+ CHECK_BOX_LEFT = 14
60
+ OK_BUTTON_RECT = QRect(342, 307, 89, 23)
61
+ CANCEL_BUTTON_RECT = QRect(439, 307, 89, 23)
62
+ RESET_BUTTON_SIZE = OK_BUTTON_RECT.size()
63
+ BUTTON_SPACING = CANCEL_BUTTON_RECT.left() - OK_BUTTON_RECT.right() - 1
64
+ # The body runs from the commit frame's left edge to its right edge; the buttons end 17 px above the bottom edge.
65
+ RESET_BRANCH_MARGINS = QMargins(COMMIT_FRAME_RECT.left(), 0, RESET_BRANCH_CLIENT_SIZE.width() - COMMIT_FRAME_RECT.right() - 1,
66
+ RESET_BRANCH_CLIENT_SIZE.height() - OK_BUTTON_RECT.bottom() - 1)
67
+ # Rows measured from the body's left edge.
68
+ WARNING_ICON_INDENT = WARNING_ICON_RECT.left() - COMMIT_FRAME_RECT.left()
69
+ WARNING_ICON_TO_TEXT_SPACE = WARNING_TEXT_LEFT - WARNING_ICON_RECT.right() - 1
70
+ # A label writes its text about 1 px after its own left edge, so the caption starts 1 px left of its captured ink at x 17.
71
+ CAPTION_LEFT = 16
72
+ RESET_BRANCH_CAPTION_INDENT = CAPTION_LEFT - COMMIT_FRAME_RECT.left()
73
+ BRANCH_LIST_INDENT = RESET_BRANCH_LIST_RECT.left() - COMMIT_FRAME_RECT.left()
74
+ CHECK_BOX_INDENT = CHECK_BOX_LEFT - COMMIT_FRAME_RECT.left()
75
+ BUTTON_ROW_RIGHT_INDENT = RESET_BRANCH_CLIENT_SIZE.width() - CANCEL_BUTTON_RECT.right() - 1 - RESET_BRANCH_MARGINS.right()
76
+ # Vertical positions, fitted so that Qt's text, list, frame and squares land where the capture has them in the Fusion
77
+ # style: the warning text's top row, then the spaces between the rows below it.
78
+ WARNING_TEXT_TOP = 12
79
+ WARNING_ICON_DROP = WARNING_ICON_RECT.top() - WARNING_TEXT_TOP
80
+ WARNING_TO_CAPTION_SPACE = 10
81
+ CAPTION_TO_BRANCH_LIST_SPACE = 3
82
+ BRANCH_LIST_TO_FRAME_SPACE = 3
83
+ FRAME_TO_CHECKOUT_SPACE = 4
84
+ CHECKOUT_TO_FORCE_SPACE = 4
85
+
86
+
87
+ class ResetOtherBranchDialog(ReferenceFormDialog):
88
+ """Move another local branch to a commit, and optionally check it out, like the Git Extensions Reset branch window.
89
+
90
+ The window freezes a request once a branch is chosen. Whether that reset is a fast-forward is asked through
91
+ ``ancestry_requested`` the first time each branch is chosen; the reference form controller answers from the graph in memory
92
+ when it can, and from Git otherwise. A reset that is not a fast-forward needs Force, and the disabled OK turns red
93
+ until Force is checked. Checkout branch after reset starts checked at every opening. Git still checks the
94
+ fast-forward itself when the reset starts, so the answer shown here only drives the form.
95
+ """
96
+
97
+ branch_requested = Signal(object)
98
+ ancestry_requested = Signal(str, str)
99
+
100
+ def __init__(self, session_id: str, target_oid: str, parent: QWidget | None = None, *, clock: Callable[[], datetime] | None = None) -> None:
101
+ """Build the warning, the branch list, the commit box, the two check boxes and the OK and Cancel buttons of the capture.
102
+
103
+ ``clock`` dates the commit box, which reads the system time when it is absent.
104
+ """
105
+ super().__init__(session_id, parent)
106
+ self.intent: BranchIntent | None = None
107
+ self.setWindowTitle(RESET_BRANCH_TITLE)
108
+ self.target_oid = target_oid
109
+ self.full_ref = ""
110
+ # The local branches the reference form controller offered, in the list's order.
111
+ self.candidate_refs: tuple[str, ...] = ()
112
+ # Whether resetting each branch already checked is a fast-forward, and the branches still waiting for the answer.
113
+ self.ancestry_by_ref: dict[str, bool] = {}
114
+ self.pending_ancestry_set: set[str] = set()
115
+ # True once the commit box shows the target's subject, author and date; OK waits for it.
116
+ self.metadata_ready = False
117
+ # Set when Git could not read the target commit: a branch is never reset to a commit nobody could show.
118
+ self.metadata_failed = False
119
+ # Whether OK currently carries the red non-fast-forward look, so its style sheet is only rewritten on a change.
120
+ self.blocked_look = False
121
+ install_plain_form_palette(self)
122
+ layout = QVBoxLayout(self)
123
+ layout.setContentsMargins(RESET_BRANCH_MARGINS)
124
+ layout.setSpacing(0)
125
+ layout.addSpacing(WARNING_TEXT_TOP)
126
+ layout.addLayout(self._warning_row())
127
+ layout.addSpacing(WARNING_TO_CAPTION_SPACE)
128
+ self.caption_label = QLabel(RESET_LOCAL_BRANCH_CAPTION)
129
+ layout.addLayout(self._indented_row(self.caption_label, RESET_BRANCH_CAPTION_INDENT))
130
+ layout.addSpacing(CAPTION_TO_BRANCH_LIST_SPACE)
131
+ self.branch_selector = QComboBox()
132
+ self.branch_selector.setAccessibleName(RESET_LOCAL_BRANCH_CAPTION.removesuffix(":"))
133
+ # The list takes the width the window gives it, whatever the length of the branch names.
134
+ self.branch_selector.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
135
+ self.branch_selector.setMinimumContentsLength(1)
136
+ self.caption_label.setBuddy(self.branch_selector)
137
+ branch_row = QHBoxLayout()
138
+ branch_row.setContentsMargins(BRANCH_LIST_INDENT, 0, RESET_BRANCH_CLIENT_SIZE.width() - RESET_BRANCH_LIST_RECT.right() - 1 - RESET_BRANCH_MARGINS.right(), 0)
139
+ branch_row.addWidget(self.branch_selector)
140
+ layout.addLayout(branch_row)
141
+ layout.addSpacing(BRANCH_LIST_TO_FRAME_SPACE)
142
+ # The v6.0 commit box: its last row is as high as the others and long values are cut instead of wrapped.
143
+ self.commit_summary = CommitSummary(target_oid, git_extensions_look=True, last_row_height=GIT_EXTENSIONS_COMPACT_LAST_ROW_HEIGHT, single_line=True, clock=clock)
144
+ layout.addWidget(self.commit_summary)
145
+ layout.addSpacing(FRAME_TO_CHECKOUT_SPACE)
146
+ # Checked at every opening and never remembered, unlike Git Extensions, whose capture shows it unchecked.
147
+ self.checkout_checkbox = ElidedCheckBox(CHECKOUT_AFTER_RESET_LABEL, open_whole=True)
148
+ self.checkout_checkbox.setChecked(True)
149
+ layout.addLayout(self._indented_row(self.checkout_checkbox, CHECK_BOX_INDENT, stretch=True))
150
+ layout.addSpacing(CHECKOUT_TO_FORCE_SPACE)
151
+ self.force_checkbox = ElidedCheckBox(FORCE_RESET_LABEL, open_whole=True)
152
+ layout.addLayout(self._indented_row(self.force_checkbox, CHECK_BOX_INDENT, stretch=True))
153
+ layout.addStretch(1)
154
+ self.submit_button.setText(OK_BUTTON_TEXT)
155
+ self.submit_button.setEnabled(False)
156
+ self.cancel_button = QPushButton(CANCEL_BUTTON_TEXT)
157
+ # Only OK may be the default button; see the Enter handling of the reference forms.
158
+ self.cancel_button.setAutoDefault(False)
159
+ button_row = QHBoxLayout()
160
+ button_row.setContentsMargins(0, 0, BUTTON_ROW_RIGHT_INDENT, 0)
161
+ button_row.setSpacing(BUTTON_SPACING)
162
+ button_row.addStretch(1)
163
+ button_row.addWidget(self.submit_button)
164
+ button_row.addWidget(self.cancel_button)
165
+ layout.addLayout(button_row)
166
+ self.apply_theme_look()
167
+ follow_theme(self, self.apply_theme_look)
168
+ # Enter runs OK from anywhere in the form. The role is set only once the button belongs to the dialog, so Qt
169
+ # records it as the dialog's main default.
170
+ self.submit_button.setDefault(True)
171
+ self.submit_button.clicked.connect(self._submit)
172
+ self.cancel_button.clicked.connect(self.reject)
173
+ self.branch_selector.currentIndexChanged.connect(self._branch_changed)
174
+ self.checkout_checkbox.toggled.connect(self._update_intent)
175
+ self.force_checkbox.toggled.connect(self._update_intent)
176
+ self.resize(RESET_BRANCH_CLIENT_SIZE)
177
+ self.refresh_metrics()
178
+ self.branch_selector.setFocus()
179
+
180
+ def apply_theme_look(self) -> None:
181
+ """Lay the light or dark look of the branch list, OK and Cancel, decided on the application palette; called again at each switch.
182
+
183
+ Git Extensions draws the branch list like a field one can type into; GitPane keeps it a plain choice among the
184
+ offered branches, so no typed name can reach the reset. OK keeps its red look while a reset needs Force. Only
185
+ style sheets and palettes change: the branch, both check boxes and whether OK can be used stay as they are.
186
+ """
187
+ apply_windows_combo_look(self.branch_selector, editable=True)
188
+ apply_windows_button_look(self.cancel_button)
189
+ self._lay_submit_button_look()
190
+
191
+ def fit_capture_controls(self) -> None:
192
+ """Give OK and Cancel the capture's size, or more when the system text needs it; both keep the same size.
193
+
194
+ The capture's 89 x 23 holds Windows' Segoe UI 9 pt; a larger font would cut the button text at that height.
195
+ """
196
+ size = QSize(RESET_BUTTON_SIZE)
197
+ for button in (self.submit_button, self.cancel_button):
198
+ size = size.expandedTo(button.sizeHint())
199
+ for button in (self.submit_button, self.cancel_button):
200
+ button.setFixedSize(size)
201
+
202
+ def refresh_metrics(self, available: QRect | None = None) -> None:
203
+ """Size the capture's controls, then keep a minimum size at which both warning sentences stay whole.
204
+
205
+ Unlike the other reference forms, Reset branch keeps a minimum size, so the user can make it neither narrow nor
206
+ short enough to cut a sentence of the warning shown before a forced reset, as long as the usable screen area
207
+ holds that minimum. The minimum is laid here, when the font, palette, density or screen changes, never while the
208
+ window is resized, and fit_window brings it back to the usable screen area: with a large font on a small screen
209
+ the minimum is relaxed, and the sentences can lose their last lines again.
210
+ """
211
+ super().refresh_metrics(available)
212
+ minimum = self._whole_warning_minimum_size()
213
+ fit_window(self, minimum.width(), minimum.height(), available)
214
+
215
+ def _whole_warning_minimum_size(self) -> QSize:
216
+ """Return the minimum size, taken at the capture's width, at which every row, wrapped warning sentences included, gets the height it needs.
217
+
218
+ A top-level window keeps as its minimum height the one of its layout, where each wrapped sentence counts for a
219
+ single line, so a narrower window would leave the sentences less height than their extra lines need. The width
220
+ is the capture's, or the layout's minimum when that is wider: the capture's width keeps the opening height with
221
+ the captures' font, and at any larger width the sentences take fewer lines, so the height asked here is enough.
222
+ """
223
+ layout = self.layout()
224
+ layout.activate()
225
+ layout_minimum = layout.totalMinimumSize()
226
+ width = max(RESET_BRANCH_CLIENT_SIZE.width(), layout_minimum.width())
227
+ height = layout_minimum.height()
228
+ if layout.hasHeightForWidth():
229
+ height = max(height, layout.totalMinimumHeightForWidth(width))
230
+ return QSize(width, height)
231
+
232
+ def _warning_row(self) -> QHBoxLayout:
233
+ """Build the warning icon and its two sentences, each wrapped onto as many lines as the window's width asks, never cut.
234
+
235
+ The sentences are prose, like the other captions of the reference forms: plain text, neither selectable nor broken
236
+ in the middle of a word. GitPane fits every new window to its content at its first show, wrapped texts included,
237
+ so the window opens tall enough for the lines the sentences take; with the captures' font each sentence holds on
238
+ one line, as in the capture. After the opening, the window's minimum size keeps the sentences whole however small
239
+ the user makes the window, as long as the usable screen area holds that minimum.
240
+ """
241
+ self.warning_icon = StandardIconLabel(WARNING_ICON_RECT.size(), QStyle.StandardPixmap.SP_MessageBoxWarning)
242
+ icon_column = QVBoxLayout()
243
+ icon_column.setContentsMargins(0, WARNING_ICON_DROP, 0, 0)
244
+ icon_column.addWidget(self.warning_icon)
245
+ icon_column.addStretch(1)
246
+ self.warning_labels = [QLabel(WARNING_FIRST_LINE), QLabel(WARNING_SECOND_LINE)]
247
+ for label in self.warning_labels:
248
+ label.setTextFormat(Qt.TextFormat.PlainText)
249
+ label.setWordWrap(True)
250
+ text_column = QVBoxLayout()
251
+ text_column.setContentsMargins(0, 0, 0, 0)
252
+ text_column.setSpacing(0)
253
+ for label in self.warning_labels:
254
+ text_column.addWidget(label)
255
+ row = QHBoxLayout()
256
+ row.setContentsMargins(WARNING_ICON_INDENT, 0, 0, 0)
257
+ row.setSpacing(WARNING_ICON_TO_TEXT_SPACE)
258
+ row.addLayout(icon_column)
259
+ row.addLayout(text_column, 1)
260
+ return row
261
+
262
+ def _indented_row(self, widget: QWidget, indent: int, *, stretch: bool = False) -> QHBoxLayout:
263
+ """Place one control this far right of the body's left edge; ``stretch`` lets it take the rest of the width."""
264
+ row = QHBoxLayout()
265
+ row.setContentsMargins(indent, 0, 0, 0)
266
+ row.setSpacing(0)
267
+ row.addWidget(widget, 1 if stretch else 0)
268
+ if not stretch:
269
+ row.addStretch(1)
270
+ return row
271
+
272
+ def populate(self, snapshot: ReferenceSnapshot, target_oid: str, candidate_refs: tuple[str, ...]) -> None:
273
+ """Offer the resettable branches of the references the window was opened on, keeping a branch the user already chose.
274
+
275
+ No branch is chosen at first, as in Git Extensions. Each new capture drops the fast-forward answers and the Force
276
+ consent, which were given for the previous one.
277
+ """
278
+ if self.closed or self.busy or self.completed:
279
+ return
280
+ self.snapshot = snapshot
281
+ self.target_oid = target_oid
282
+ self.candidate_refs = candidate_refs
283
+ self.ancestry_by_ref.clear()
284
+ self.pending_ancestry_set.clear()
285
+ with QSignalBlocker(self.force_checkbox):
286
+ self.force_checkbox.setChecked(False)
287
+ self.commit_summary.show_references(snapshot)
288
+ selected_ref = self.branch_selector.currentData()
289
+ with QSignalBlocker(self.branch_selector):
290
+ self.branch_selector.clear()
291
+ for full_ref in candidate_refs:
292
+ self.branch_selector.addItem(short_reference_name(full_ref), full_ref)
293
+ self.branch_selector.setItemData(self.branch_selector.count() - 1, full_ref, Qt.ItemDataRole.ToolTipRole)
294
+ self.branch_selector.setCurrentIndex(self.branch_selector.findData(selected_ref) if selected_ref else -1)
295
+ self.branch_selector.setEnabled(bool(candidate_refs))
296
+ self._show_branch_tooltip()
297
+ self._update_intent()
298
+
299
+ def _show_branch_tooltip(self) -> None:
300
+ """Show the chosen branch's full name as the list's tooltip, or why nothing can be chosen."""
301
+ if not self.branch_selector.count():
302
+ self.branch_selector.setToolTip(NO_AVAILABLE_BRANCH_CAUSE)
303
+ return
304
+ self.branch_selector.setToolTip(self.branch_selector.currentData() or "")
305
+
306
+ def _branch_changed(self) -> None:
307
+ """Ask for fresh Force consent after choosing another branch, then freeze the new request."""
308
+ with QSignalBlocker(self.force_checkbox):
309
+ self.force_checkbox.setChecked(False)
310
+ self._show_branch_tooltip()
311
+ self._update_intent()
312
+
313
+ def show_ancestry(self, full_ref: str, fast_forward: bool | None, cause: str = "") -> None:
314
+ """Record whether resetting a branch is a fast-forward, or explain why it could not be told for the chosen branch."""
315
+ self.pending_ancestry_set.discard(full_ref)
316
+ if self.closed or self.busy or self.completed:
317
+ return
318
+ if fast_forward is not None:
319
+ self.ancestry_by_ref[full_ref] = fast_forward
320
+ self._update_intent()
321
+ return
322
+ if full_ref == self.full_ref:
323
+ self.show_error(cause)
324
+
325
+ def show_loaded_commit(self, commit: Commit) -> None:
326
+ """Fill the commit box from a commit the graph already holds, without asking Git."""
327
+ if self.closed or commit.oid != self.commit_summary.target_oid:
328
+ return
329
+ self.commit_summary.show_loaded_commit(commit)
330
+ self.metadata_ready = True
331
+ self._update_intent()
332
+
333
+ def show_metadata(self, fields: tuple[str, ...] | None, cause: str = "") -> None:
334
+ """Fill the commit box from Git's answer, or refuse the target Git could not read and explain why."""
335
+ if self.closed:
336
+ return
337
+ if fields is None:
338
+ self.metadata_ready = False
339
+ self.metadata_failed = True
340
+ self.commit_summary.subject_label.setText(COMMIT_METADATA_UNAVAILABLE)
341
+ self._update_intent()
342
+ self.show_error(cause)
343
+ return
344
+ oid_field_index = 0
345
+ if fields[oid_field_index] != self.commit_summary.target_oid:
346
+ return
347
+ self.commit_summary.show_commit(fields)
348
+ self.metadata_ready = True
349
+ self._update_intent()
350
+
351
+ def _update_intent(self) -> None:
352
+ """Freeze the chosen branch and both options into the request OK would send, and ask whether it is a fast-forward.
353
+
354
+ The request exists as soon as a branch is chosen, so a reset that needs Force is still refused by the business
355
+ rules if it is sent without it; OK only becomes usable once the commit is shown and the reset is a fast-forward
356
+ or forced.
357
+ """
358
+ if self.busy or self.completed or self.closed:
359
+ return
360
+ self.intent = None
361
+ self.full_ref = self.branch_selector.currentData() or ""
362
+ fast_forward = None
363
+ if self.snapshot is not None and self.full_ref:
364
+ old_oid = self.snapshot.object_oid_by_name.get(self.full_ref)
365
+ fast_forward = self.ancestry_by_ref.get(self.full_ref)
366
+ if fast_forward is None and old_oid and self.full_ref not in self.pending_ancestry_set:
367
+ self.pending_ancestry_set.add(self.full_ref)
368
+ self.ancestry_requested.emit(self.full_ref, old_oid)
369
+ # The graph in memory answers at once, while the signal is still being delivered.
370
+ fast_forward = self.ancestry_by_ref.get(self.full_ref)
371
+ blocked = fast_forward is False and not self.force_checkbox.isChecked()
372
+ self._paint_submit_button(blocked)
373
+ self.submit_button.setToolTip(NON_FAST_FORWARD_CAUSE if blocked else "")
374
+ if self.snapshot is not None and self.full_ref and not self.metadata_failed:
375
+ try:
376
+ self.intent = BranchIntent.capture(self.snapshot, self.target_oid, session_id=self.session_id, checkout=self.checkout_checkbox.isChecked(),
377
+ mode=BranchMode.MOVE, full_ref=self.full_ref, force_reset=self.force_checkbox.isChecked())
378
+ except ValueError as error:
379
+ self.submit_button.setToolTip(self.text_filter.redact(str(error)))
380
+ self.submit_button.setEnabled(self.intent is not None and self.metadata_ready and fast_forward is not None and not blocked)
381
+
382
+ def _paint_submit_button(self, blocked: bool) -> None:
383
+ """Turn OK red while it is disabled for a missing Force, or back to its usual look; nothing is rewritten when that does not change."""
384
+ if blocked == self.blocked_look:
385
+ return
386
+ self.blocked_look = blocked
387
+ self._lay_submit_button_look()
388
+
389
+ def _lay_submit_button_look(self) -> None:
390
+ """Give OK the Windows look of the light theme, or the palette's in a dark one, red while disabled for a missing Force.
391
+
392
+ The theme is the application palette's: at a switch this runs before Qt hands the new palette to the button.
393
+ """
394
+ base_style = "" if is_dark_palette(QApplication.palette()) else windows_button_style_sheet()
395
+ self.submit_button.setStyleSheet(base_style + (NON_FAST_FORWARD_BUTTON_STYLE if self.blocked_look else ""))
396
+
397
+ def _submit(self) -> None:
398
+ """Freeze the form and hand its request to the reference form controller once; Cancel stays disabled until the result."""
399
+ if self.closed or self.busy or self.completed or self.intent is None or not self.submit_button.isEnabled():
400
+ return
401
+ self.busy = True
402
+ self._set_choices_enabled(False)
403
+ self.submit_button.setEnabled(False)
404
+ self.cancel_button.setEnabled(False)
405
+ self.branch_requested.emit(self.intent)
406
+
407
+ def show_outcome(self, outcome: OperationOutcome) -> None:
408
+ """Close after a successful reset; keep the form after any other result while the console explains it.
409
+
410
+ A refused or failed reset moved nothing, so every choice stays as it was and editable for another try. After a
411
+ partial or unknown result the branch may have moved, so the form only shows what was asked and can be closed.
412
+ """
413
+ self.busy = False
414
+ self.cancel_button.setEnabled(True)
415
+ if outcome.status == OperationStatus.SUCCESS:
416
+ self.accept()
417
+ return
418
+ if outcome.status in UNCHANGED_OUTCOME_STATUS_SET:
419
+ self._set_choices_enabled(True)
420
+ self._update_intent()
421
+ return
422
+ self.completed = True
423
+ self._set_choices_enabled(False)
424
+ self.submit_button.setEnabled(False)
425
+
426
+ def _set_choices_enabled(self, enabled: bool) -> None:
427
+ """Enable or disable the branch list and both options together; an empty list always stays disabled."""
428
+ self.branch_selector.setEnabled(enabled and self.branch_selector.count() > 0)
429
+ self.checkout_checkbox.setEnabled(enabled)
430
+ self.force_checkbox.setEnabled(enabled)