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,323 @@
1
+ """Reproduce the Git Extensions "Delete branch" window: a list of local branches to type or pick, then one Delete button."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import TYPE_CHECKING
7
+
8
+ from PySide6.QtCore import QMargins, QSize, Qt, Signal
9
+ from PySide6.QtWidgets import QComboBox, QHBoxLayout, QLabel, QMessageBox, QPushButton, QVBoxLayout, QWidget
10
+ from shiboken6 import isValid
11
+
12
+ from gitpane.config import DELETE_BRANCH_TITLE, DELETE_BUTTON_TEXT, ICON_SIDE, LOCAL_PREFIX
13
+ from gitpane.operation.operation_status import OperationStatus
14
+ from gitpane.reference.reference_snapshot import short_reference_name
15
+ from gitpane.text.typed_text import typed_value
16
+ from gitpane.ui.look.action_icon import action_icon
17
+ from gitpane.ui.look.theme_look import follow_theme
18
+ from gitpane.ui.look.windows_control_look import apply_windows_button_look, apply_windows_combo_look
19
+ from gitpane.ui.reference.reference_form import ReferenceFormDialog
20
+ from gitpane.ui.reference.select_branches_dialog import SELECT_BRANCHES_CAPTION, SelectBranchesDialog
21
+ from gitpane.ui.widget.dialog_layout import BandButtonSizing, fit_caption_column, fit_capture_control, install_banded_layout
22
+ from gitpane.ui.window_control.dialog_modality import open_window_modal, prepare_message_box
23
+
24
+ if TYPE_CHECKING:
25
+ from collections.abc import Callable
26
+
27
+ from gitpane.operation.repository_operation import OperationOutcome
28
+ from gitpane.reference.reference_edit import BranchDeletionIntent
29
+ from gitpane.reference.reference_snapshot import ReferenceSnapshot
30
+
31
+ MULTIPLE_BRANCHES_NAME = "Select multiple branches"
32
+ MULTIPLE_BRANCHES_HINT = "Type several branch names separated by spaces"
33
+ UNMERGED_QUESTION_HEADING = "The following branches are not fully merged:"
34
+ UNMERGED_QUESTION_ENDING = "Delete them anyway?"
35
+ NO_BRANCH_CAUSE = "Select at least one branch to delete"
36
+ CURRENT_BRANCH_CAUSE = "is the current branch"
37
+ OCCUPIED_BRANCH_CAUSE = "is checked out in a worktree or held by a rebase or bisect"
38
+ UNKNOWN_BRANCH_CAUSE = "is not a local branch"
39
+ # Typed branch names are separated by ASCII whitespace only: Git refuses the ASCII space and every ASCII control
40
+ # character in a branch name, but accepts other Unicode spaces such as the non-breaking space, which therefore
41
+ # belong to the name and never separate two of them.
42
+ BRANCH_NAME_SEPARATOR_PATTERN = re.compile(r"\s+", re.ASCII)
43
+ # Client area of the Git Extensions capture (412 x 91 inside the window borders and title bar).
44
+ DELETE_BRANCH_CLIENT_SIZE = QSize(412, 91)
45
+ # Measured on the capture in client pixels at 100 %: white body y 0-49, band line y 50, band y 51-90; "Select branches"
46
+ # ink from x 15; branch list (104, 12, 272, 23); multiple selection button (381, 11, 21, 23); Delete button
47
+ # (325, 59, 73, 23) in the band. The body starts 1 px left of the caption ink and at the multiple button's top.
48
+ DELETE_BRANCH_BODY_MARGINS = QMargins(14, 11, 10, 0)
49
+ # Caption width from its left edge to the branch list (104 - 14); a wider caption text widens it.
50
+ DELETE_BRANCH_CAPTION_WIDTH = 90
51
+ # Qt's first anti-aliased ink pixel sits on the text origin, one pixel left of the capture's ink.
52
+ DELETE_BRANCH_CAPTION_INDENT = 1
53
+ BRANCH_LIST_TO_BUTTON_SPACE = 5
54
+ MULTIPLE_BRANCHES_BUTTON_SIZE = QSize(21, 23)
55
+ DELETE_BUTTON_SIZE = QSize(73, 23)
56
+ # The list sits 1 px lower than the multiple selection button in the capture.
57
+ BRANCH_LIST_DROP = 1
58
+
59
+
60
+ class DeleteLocalBranchDialog(ReferenceFormDialog):
61
+ """Delete one or several local branches, like the Git Extensions Delete branch window.
62
+
63
+ The list takes branch names separated by spaces, as the Git Extensions list does; Git never allows an ASCII space
64
+ or control character in a branch name, and only those separate names, so the text reads back without ambiguity
65
+ even for a name holding another Unicode space. The window only checks the typed names against the
66
+ branches the reference form controller offered and hands their full names over; that controller decides which ones are merged,
67
+ asks about the others and runs Git.
68
+ """
69
+
70
+ delete_requested = Signal(object)
71
+
72
+ def __init__(self, session_id: str, parent: QWidget | None = None) -> None:
73
+ """Build the caption, the editable branch list, the multiple selection button and the button band of the capture."""
74
+ super().__init__(session_id, parent)
75
+ self.setWindowTitle(DELETE_BRANCH_TITLE)
76
+ # The request the reference form controller froze once every branch was classified and every question answered.
77
+ self.intent: BranchDeletionIntent | None = None
78
+ # The full names of the local branches this window may delete, as the reference form controller computed them at opening.
79
+ self.candidate_refs: tuple[str, ...] = ()
80
+ # The Select multiple branches window while it is open.
81
+ self.select_dialog: SelectBranchesDialog | None = None
82
+ self.unmerged_question: QMessageBox | None = None
83
+ # Raised for each request the reference form controller has to ask Git about, so a late answer for an older one is dropped.
84
+ self.classification_generation = 0
85
+ layout = QVBoxLayout()
86
+ layout.setSpacing(0)
87
+ row = QHBoxLayout()
88
+ row.setContentsMargins(0, 0, 0, 0)
89
+ row.setSpacing(0)
90
+ self.caption_label = QLabel(SELECT_BRANCHES_CAPTION)
91
+ self.caption_label.setContentsMargins(DELETE_BRANCH_CAPTION_INDENT, 0, 0, 0)
92
+ self.branch_combo = QComboBox()
93
+ self.branch_combo.setEditable(True)
94
+ # Typed names stay in the text; the list itself only ever holds the offered branches.
95
+ self.branch_combo.setInsertPolicy(QComboBox.InsertPolicy.NoInsert)
96
+ self.branch_combo.setAccessibleName(SELECT_BRANCHES_CAPTION)
97
+ self.caption_label.setBuddy(self.branch_combo)
98
+ branch_column = QVBoxLayout()
99
+ branch_column.setContentsMargins(0, BRANCH_LIST_DROP, 0, 0)
100
+ branch_column.setSpacing(0)
101
+ branch_column.addWidget(self.branch_combo)
102
+ self.multiple_button = QPushButton()
103
+ self.multiple_button.setIcon(action_icon("choose_revision", self.devicePixelRatioF()))
104
+ self.multiple_button.setIconSize(QSize(ICON_SIDE, ICON_SIDE))
105
+ self.multiple_button.setFixedSize(MULTIPLE_BRANCHES_BUTTON_SIZE)
106
+ self.multiple_button.setAccessibleName(MULTIPLE_BRANCHES_NAME)
107
+ self.multiple_button.setToolTip(MULTIPLE_BRANCHES_HINT)
108
+ # Only the Delete button may be the default one; see the Enter handling of the reference forms.
109
+ self.multiple_button.setAutoDefault(False)
110
+ # Offered once the window knows its branches.
111
+ self.multiple_button.setEnabled(False)
112
+ row.addWidget(self.caption_label, 0, Qt.AlignmentFlag.AlignVCenter)
113
+ row.addLayout(branch_column, 1)
114
+ row.addSpacing(BRANCH_LIST_TO_BUTTON_SPACE)
115
+ row.addWidget(self.multiple_button, 0, Qt.AlignmentFlag.AlignTop)
116
+ layout.addLayout(row)
117
+ layout.addStretch(1)
118
+ self.submit_button.setText(DELETE_BUTTON_TEXT)
119
+ self.submit_button.setEnabled(False)
120
+ # Git Extensions shows this form as a white body over a grey band holding its single button; there is no
121
+ # Cancel button, Escape and the title bar close it.
122
+ self.apply_theme_look()
123
+ self.button_band = install_banded_layout(self, layout, [self.submit_button], body_margins=DELETE_BRANCH_BODY_MARGINS, button_sizing=BandButtonSizing.WINDOW)
124
+ follow_theme(self, self.apply_theme_look)
125
+ # Enter runs Delete from anywhere in the form. The role is set only now, once the button belongs to the dialog:
126
+ # Qt then records it as the dialog's main default and gives it back whenever the focus moves.
127
+ self.submit_button.setDefault(True)
128
+ self.submit_button.clicked.connect(self._submit)
129
+ self.multiple_button.clicked.connect(self._open_select_dialog)
130
+ self.branch_combo.editTextChanged.connect(self._text_changed)
131
+ self.resize(DELETE_BRANCH_CLIENT_SIZE)
132
+ # Git Extensions gives this window the same minimum and maximum height: it only grows in width.
133
+ self.single_row_height = DELETE_BRANCH_CLIENT_SIZE.height()
134
+ self.refresh_metrics()
135
+ self.branch_combo.setFocus()
136
+
137
+ def apply_theme_look(self) -> None:
138
+ """Lay the light or dark look of the branch list and the multiple selection button, decided on the application palette.
139
+
140
+ Called by the constructor and again at each switch; the body and the band follow the switch on their own. Only
141
+ style sheets and palettes change: the branches typed or picked stay as they are.
142
+ """
143
+ apply_windows_combo_look(self.branch_combo, editable=True)
144
+ apply_windows_button_look(self.multiple_button)
145
+
146
+ def fit_capture_controls(self) -> None:
147
+ """Give the caption and the Delete button the capture's sizes, or more when their text needs it; a wider caption moves the list right."""
148
+ fit_caption_column((self.caption_label,), DELETE_BRANCH_CAPTION_WIDTH)
149
+ fit_capture_control(self.submit_button, DELETE_BUTTON_SIZE)
150
+
151
+ def populate(self, snapshot: ReferenceSnapshot, candidate_refs: tuple[str, ...], initial_ref: str) -> None:
152
+ """Offer the deletable branches of the references the window was opened on, starting with the clicked branch.
153
+
154
+ The clicked branch is written in the list even when it cannot be deleted, so pressing Delete explains why.
155
+ """
156
+ if self.closed or self.busy or self.completed:
157
+ return
158
+ self.snapshot = snapshot
159
+ self.candidate_refs = candidate_refs
160
+ self.branch_combo.blockSignals(True)
161
+ self.branch_combo.clear()
162
+ for full_ref in candidate_refs:
163
+ self.branch_combo.addItem(short_reference_name(full_ref), full_ref)
164
+ initial_index = self.branch_combo.findData(initial_ref)
165
+ self.branch_combo.setCurrentIndex(initial_index)
166
+ if initial_index < 0:
167
+ self.branch_combo.setEditText(short_reference_name(initial_ref))
168
+ self.branch_combo.blockSignals(False)
169
+ self.branch_combo.lineEdit().selectAll()
170
+ self.branch_combo.setFocus()
171
+ self.multiple_button.setEnabled(True)
172
+ self._text_changed()
173
+
174
+ def selected_refs(self) -> tuple[str, ...]:
175
+ """Read the typed names as full local branch names, in the typed order and each only once."""
176
+ full_refs: list[str] = []
177
+ for name in BRANCH_NAME_SEPARATOR_PATTERN.split(self.branch_combo.currentText()):
178
+ if not name:
179
+ continue
180
+ full_ref = LOCAL_PREFIX + name
181
+ if full_ref not in full_refs:
182
+ full_refs.append(full_ref)
183
+ return tuple(full_refs)
184
+
185
+ def known_selected_refs(self) -> tuple[str, ...]:
186
+ """Read the typed names that are offered branches, silently leaving the others aside, to pre-check them in the list."""
187
+ full_refs: list[str] = []
188
+ for full_ref in self.selected_refs():
189
+ if full_ref in self.candidate_refs:
190
+ full_refs.append(full_ref)
191
+ return tuple(full_refs)
192
+
193
+ def _open_select_dialog(self) -> None:
194
+ """Open Select multiple branches on the offered branches, the typed ones checked, without starting Git."""
195
+ if self.closed or self.busy or self.completed or self.snapshot is None:
196
+ return
197
+ if self.select_dialog is not None and isValid(self.select_dialog) and self.select_dialog.isVisible():
198
+ self.select_dialog.raise_()
199
+ return
200
+ dialog = SelectBranchesDialog(self.candidate_refs, set(self.known_selected_refs()), self)
201
+ self.select_dialog = dialog
202
+ dialog.selection_applied.connect(self._apply_selection)
203
+ dialog.finished.connect(dialog.deleteLater)
204
+ open_window_modal(dialog)
205
+
206
+ def _apply_selection(self, full_refs: tuple[str, ...]) -> None:
207
+ """Write the branches checked in Select multiple branches as the list text, their names separated by spaces.
208
+
209
+ Git Extensions replaces the text with the checked branches however its window was closed, so an empty
210
+ selection empties the text.
211
+ """
212
+ self.select_dialog = None
213
+ if self.closed or self.busy or self.completed:
214
+ return
215
+ names: list[str] = []
216
+ for full_ref in full_refs:
217
+ names.append(short_reference_name(full_ref))
218
+ self.branch_combo.setEditText(" ".join(names))
219
+ self.branch_combo.setFocus()
220
+
221
+ def _refusal_cause(self, full_refs: tuple[str, ...]) -> str:
222
+ """Explain which typed names cannot be deleted and why, or answer an empty text when all of them can."""
223
+ if not full_refs:
224
+ return NO_BRANCH_CAUSE
225
+ causes: list[str] = []
226
+ for full_ref in full_refs:
227
+ if full_ref in self.candidate_refs:
228
+ continue
229
+ name = "'" + short_reference_name(full_ref) + "' "
230
+ if self.snapshot is not None and full_ref == self.snapshot.head_ref:
231
+ causes.append(name + CURRENT_BRANCH_CAUSE)
232
+ elif self.snapshot is not None and full_ref in self.snapshot.object_oid_by_name:
233
+ causes.append(name + OCCUPIED_BRANCH_CAUSE)
234
+ else:
235
+ causes.append(name + UNKNOWN_BRANCH_CAUSE)
236
+ if not causes:
237
+ return ""
238
+ return "Cannot delete: " + "; ".join(causes) + "."
239
+
240
+ def _text_changed(self) -> None:
241
+ """Offer Delete again once the typed names change, dropping the explanation of an earlier refusal."""
242
+ self.submit_button.setToolTip("")
243
+ self.submit_button.setEnabled(self.snapshot is not None and bool(typed_value(self.branch_combo.currentText())) and not self.busy
244
+ and not self.completed and not self.closed)
245
+
246
+ def _submit(self) -> None:
247
+ """Hand the typed branches to the reference form controller once, or explain before any process why some cannot be deleted."""
248
+ if self.closed or self.busy or self.completed or self.snapshot is None or not self.submit_button.isEnabled():
249
+ return
250
+ full_refs = self.selected_refs()
251
+ cause = self._refusal_cause(full_refs)
252
+ if cause:
253
+ self.show_error(cause)
254
+ return
255
+ self.busy = True
256
+ self._set_controls_enabled(False)
257
+ self.delete_requested.emit(full_refs)
258
+
259
+ def show_unmerged_question(self, unmerged_refs: tuple[str, ...], answer: Callable[[bool], None]) -> None:
260
+ """Ask once whether the listed branches may lose their commits, without blocking the application.
261
+
262
+ No is the default and the answer of Escape. An answer given after the window closed is dropped.
263
+ """
264
+ if self.closed:
265
+ return
266
+ lines = [UNMERGED_QUESTION_HEADING]
267
+ for full_ref in unmerged_refs:
268
+ lines.append(short_reference_name(full_ref))
269
+ lines.append(UNMERGED_QUESTION_ENDING)
270
+ question = QMessageBox(QMessageBox.Icon.Warning, DELETE_BRANCH_TITLE, "\n".join(lines),
271
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, self)
272
+ prepare_message_box(question, DELETE_BRANCH_TITLE)
273
+ question.setTextFormat(Qt.TextFormat.PlainText)
274
+ no_button = question.button(QMessageBox.StandardButton.No)
275
+ yes_button = question.button(QMessageBox.StandardButton.Yes)
276
+ question.setDefaultButton(no_button)
277
+ question.setEscapeButton(no_button)
278
+ self.unmerged_question = question
279
+
280
+ def finished() -> None:
281
+ """Pass the answer on once, and only while the window that asked is still open."""
282
+ self.unmerged_question = None
283
+ accepted = question.clickedButton() is yes_button
284
+ question.deleteLater()
285
+ if self.closed:
286
+ return
287
+ answer(accepted)
288
+
289
+ question.finished.connect(finished)
290
+ open_window_modal(question)
291
+
292
+ def release(self) -> None:
293
+ """Give the form back to the user after a request that wrote nothing, such as a No to the question."""
294
+ if self.closed:
295
+ return
296
+ self.busy = False
297
+ self.intent = None
298
+ self._set_controls_enabled(True)
299
+ self._text_changed()
300
+
301
+ def show_outcome(self, outcome: OperationOutcome) -> None:
302
+ """Close once every branch was deleted; otherwise keep the form, frozen, while the console explains each branch."""
303
+ self.busy = False
304
+ if outcome.status == OperationStatus.SUCCESS:
305
+ self.accept()
306
+ return
307
+ self.completed = True
308
+ self._set_controls_enabled(False)
309
+
310
+ def _set_controls_enabled(self, enabled: bool) -> None:
311
+ """Enable or disable the branch list, the multiple selection button and the Delete button together."""
312
+ self.branch_combo.setEnabled(enabled)
313
+ self.multiple_button.setEnabled(enabled)
314
+ self.submit_button.setEnabled(enabled)
315
+
316
+ def _closed(self) -> None:
317
+ """Close a pending question or branch selection with the window, so its late answer can never start a deletion."""
318
+ super()._closed()
319
+ self.classification_generation += 1
320
+ if self.unmerged_question is not None and isValid(self.unmerged_question):
321
+ self.unmerged_question.reject()
322
+ if self.select_dialog is not None and isValid(self.select_dialog):
323
+ self.select_dialog.reject()