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,345 @@
1
+ """Close GitPane only once no write or process it started still runs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from PySide6.QtCore import QEvent, QObject, Qt
8
+ from PySide6.QtWidgets import QApplication, QDialog
9
+ from shiboken6 import isValid
10
+
11
+ from gitpane.controller.session_registry import NavigationKind
12
+ from gitpane.operation.operation_status import OperationStatus
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Callable
16
+
17
+ from PySide6.QtWidgets import QWidget
18
+
19
+ from gitpane.account.authentication import AuthenticationService
20
+ from gitpane.account.ssh import SshService
21
+ from gitpane.controller.account_controller import AccountController
22
+ from gitpane.controller.close_readiness import CloseReadiness
23
+ from gitpane.controller.conflict_controller import ConflictController
24
+ from gitpane.controller.emoji_window_controller import EmojiWindowController
25
+ from gitpane.controller.history_read_controller import HistoryReadController
26
+ from gitpane.controller.navigation_controller import NavigationController
27
+ from gitpane.controller.rebase_controller import RebaseController
28
+ from gitpane.controller.reference_form_controller import ReferenceFormController
29
+ from gitpane.controller.reference_read_controller import ReferenceReadController
30
+ from gitpane.controller.refresh_controller import RefreshController
31
+ from gitpane.controller.replay_controller import ReplayController
32
+ from gitpane.controller.session_registry import SessionRegistry
33
+ from gitpane.controller.staging_session_registry import StagingSessionRegistry
34
+ from gitpane.controller.stash_controller import StashController
35
+ from gitpane.controller.transport_controller import TransportController
36
+ from gitpane.controller.window_activation import WindowActivationFilter
37
+ from gitpane.history.repository import GitRepository
38
+ from gitpane.operation.repository_operation import RepositoryOperationService
39
+ from gitpane.sequencer.mergetool import MergetoolService
40
+ from gitpane.sequencer.rebase import RebaseService
41
+ from gitpane.sequencer.replay import ReplayService
42
+ from gitpane.submodule.submodule_discovery import SubmoduleService
43
+ from gitpane.tool.difftool import DifftoolService
44
+ from gitpane.tool.editor import EditorService
45
+ from gitpane.tool.system_tool import SystemToolService
46
+ from gitpane.transport.branch_exchange import TransportService
47
+ from gitpane.transport.clone import CloneService
48
+ from gitpane.worktree.mutation import MutationService
49
+ from gitpane.worktree.worktree_service import WorktreeService
50
+
51
+
52
+ def holds_running_work(widget: QWidget) -> bool:
53
+ """Tell whether a window is making Git work right now, so quitting waits for it instead of closing it.
54
+
55
+ Operation consoles say so with ``running`` and forms with ``busy``; closing one of them would stop a write halfway
56
+ (a running console treats a close as Abort) instead of letting it finish. Any other window has neither attribute.
57
+ """
58
+ return bool(getattr(widget, "running", False) or getattr(widget, "busy", False))
59
+
60
+
61
+ class ShutdownController(QObject):
62
+ """The end of GitPane and of each repository window: it keeps hold of Quit and of the close of a repository window while Git works, closes the windows of every domain in their order, and leaves the event loop once everything has drained."""
63
+
64
+ def __init__(self, *, session_registry_controller: SessionRegistry, staging_session_registry_controller: StagingSessionRegistry, window_activation_controller: WindowActivationFilter, reference_read_controller: ReferenceReadController,
65
+ refresh_controller: RefreshController, history_read_controller: HistoryReadController, account_controller: AccountController, emoji_window_controller: EmojiWindowController,
66
+ transport_controller: TransportController, reference_form_controller: ReferenceFormController, replay_controller: ReplayController, rebase_controller: RebaseController, stash_controller: StashController,
67
+ conflict_controller: ConflictController, close_readiness_controller: CloseReadiness, navigation_controller: NavigationController, application: QApplication, leave_event_loop: Callable[[], None],
68
+ repository: GitRepository, difftool: DifftoolService, worktree_service: WorktreeService, editor: EditorService, repository_operation: RepositoryOperationService, rebase: RebaseService, replay: ReplayService,
69
+ mutation: MutationService, authentication: AuthenticationService, ssh: SshService, transport: TransportService, clone: CloneService, mergetool: MergetoolService, system_tool: SystemToolService,
70
+ submodule: SubmoduleService, parent: QObject | None = None) -> None:
71
+ """Receive the services and the lower controllers whose work and windows a close waits for; the wiring builds it last and installs it on the application before the placement filter."""
72
+ super().__init__(parent)
73
+ self.session_registry_controller = session_registry_controller
74
+ self.staging_session_registry_controller = staging_session_registry_controller
75
+ self.window_activation_controller = window_activation_controller
76
+ self.reference_read_controller = reference_read_controller
77
+ self.refresh_controller = refresh_controller
78
+ self.history_read_controller = history_read_controller
79
+ self.account_controller = account_controller
80
+ self.emoji_window_controller = emoji_window_controller
81
+ self.transport_controller = transport_controller
82
+ self.reference_form_controller = reference_form_controller
83
+ self.replay_controller = replay_controller
84
+ self.rebase_controller = rebase_controller
85
+ self.stash_controller = stash_controller
86
+ self.conflict_controller = conflict_controller
87
+ self.close_readiness_controller = close_readiness_controller
88
+ self.navigation_controller = navigation_controller
89
+ self.application = application
90
+ self.leave_event_loop = leave_event_loop
91
+ self.repository = repository
92
+ self.difftool = difftool
93
+ self.worktree_service = worktree_service
94
+ self.editor = editor
95
+ self.repository_operation = repository_operation
96
+ self.rebase = rebase
97
+ self.replay = replay
98
+ self.mutation = mutation
99
+ self.authentication = authentication
100
+ self.ssh = ssh
101
+ self.transport = transport
102
+ self.clone = clone
103
+ self.mergetool = mergetool
104
+ self.system_tool = system_tool
105
+ self.submodule = submodule
106
+ self.application_quit_requested = False
107
+
108
+ def eventFilter(self, watched: QObject, event: QEvent) -> bool:
109
+ """Keep hold of the Quit of the application and of the Close of a repository window while Git work still runs.
110
+
111
+ Quit becomes a shutdown request that waits for every write and process; the Close of a repository window waits the
112
+ same way when its repository is still busy.
113
+ """
114
+ if watched is self.application and event.type() == QEvent.Type.Quit and not self.session_registry_controller.shutdown_requested:
115
+ self.request_shutdown()
116
+ event.ignore()
117
+ return True
118
+ if event.type() == QEvent.Type.Close:
119
+ for session_id, window in tuple(self.session_registry_controller.window_by_session.items()):
120
+ if watched is window and self.close_readiness_controller.defer_repository_close(session_id):
121
+ event.ignore()
122
+ return True
123
+ return super().eventFilter(watched, event)
124
+
125
+ def request_shutdown(self) -> None:
126
+ """Keep application ownership until all writes, descendants and file work finish."""
127
+ if self.session_registry_controller.shutdown_requested:
128
+ return
129
+ self._reject_idle_modal_windows()
130
+ self.session_registry_controller.quit_pending = True
131
+ if not self.close_readiness_controller.close_ready():
132
+ self.close_readiness_controller.show_close_wait()
133
+ self.close_readiness_controller.close_timer.start()
134
+ return
135
+ self.session_registry_controller.quit_pending = False
136
+ self.session_registry_controller.shutdown_requested = True
137
+ self.close_readiness_controller.close_timer.stop()
138
+ self.rebase.close()
139
+ self.replay.close()
140
+ self.editor.close()
141
+ # A window that refuses to close keeps Qt from quitting, so Settings closes here without asking about unsaved changes.
142
+ if self.session_registry_controller.setting_dialog is not None and isValid(self.session_registry_controller.setting_dialog):
143
+ self.session_registry_controller.setting_dialog.discard_and_close()
144
+ # A window modal for the whole application left open would keep the other windows from closing.
145
+ if self.account_controller.credential_manager_dialog is not None and isValid(self.account_controller.credential_manager_dialog):
146
+ self.account_controller.credential_manager_dialog.close()
147
+ # The keyboard belongs to no repository window, so closing those would leave it open; closing it saves its geometry.
148
+ if self.emoji_window_controller.emoji_window is not None and isValid(self.emoji_window_controller.emoji_window) and self.emoji_window_controller.emoji_window.isVisible():
149
+ self.emoji_window_controller.emoji_window.close()
150
+ for window in tuple(self.session_registry_controller.window_by_session.values()):
151
+ window.close()
152
+ self.transport.close()
153
+ self.clone.close()
154
+ for run in tuple(self.submodule.run_by_id.values()):
155
+ self.submodule.cancel(run.operation_id)
156
+ self.ssh.close()
157
+ self.repository_operation.close()
158
+ self.authentication.close()
159
+ self.difftool.close()
160
+ self.mergetool.close()
161
+ self.system_tool.close()
162
+ for staging in self.staging_session_registry_controller.staging_by_key.values():
163
+ staging.timer.stop()
164
+ staging.refresh_queued = False
165
+ staging.queued_refresh_priority = None
166
+ self.maybe_quit()
167
+
168
+ def _reject_idle_modal_windows(self) -> None:
169
+ """Answer Cancel in every open modal window that makes no Git work, so a window left open never keeps GitPane from quitting.
170
+
171
+ On macOS every modal window of GitPane blocks the whole application, and a question asked with exec() runs its
172
+ own event loop until answered. A console or form still making Git work is left open: quitting waits for it, and
173
+ the next retry of the pending quit closes it once it is idle. Settings and the Git Credential Manager window keep
174
+ their own closing in request_shutdown.
175
+ """
176
+ kept_window_set = {self.session_registry_controller.setting_dialog, self.account_controller.credential_manager_dialog}
177
+ for widget in tuple(QApplication.topLevelWidgets()):
178
+ if not isinstance(widget, QDialog) or not isValid(widget) or widget in kept_window_set:
179
+ continue
180
+ if not widget.isVisible() or widget.windowModality() == Qt.WindowModality.NonModal or holds_running_work(widget):
181
+ continue
182
+ widget.reject()
183
+
184
+ def resume_pending_close(self) -> None:
185
+ """Honor original close requests only after owners have published native results."""
186
+ if self.session_registry_controller.quit_pending:
187
+ self.request_shutdown()
188
+ return
189
+ for session_id in tuple(self.session_registry_controller.close_session_set):
190
+ if not self.close_readiness_controller.close_ready(session_id):
191
+ self.close_readiness_controller.show_close_wait(session_id)
192
+ continue
193
+ self.session_registry_controller.close_session_set.discard(session_id)
194
+ window = self.session_registry_controller.window_by_session.get(session_id)
195
+ if window is not None and isValid(window):
196
+ window.close()
197
+ for session_id, (session, repository_dir, kind) in tuple(self.session_registry_controller.navigation_request_by_session.items()):
198
+ if self.session_registry_controller.session_by_id.get(session_id) is not session:
199
+ self.session_registry_controller.navigation_request_by_session.pop(session_id, None)
200
+ continue
201
+ if not self.close_readiness_controller.navigation_ready(session_id):
202
+ continue
203
+ self.session_registry_controller.navigation_request_by_session.pop(session_id, None)
204
+ # A repository chosen in the switch menu is outside the family, which the submodule path refuses.
205
+ if kind is NavigationKind.SWITCH:
206
+ self.navigation_controller.switch_repository(session_id, repository_dir)
207
+ else:
208
+ self.navigation_controller.navigate_repository(session_id, repository_dir)
209
+ # The resumed change checks everything again and may stop before reading anything (folder gone, window
210
+ # opened meanwhile): its wait message goes with it. A change that waits again or started reading keeps it.
211
+ if session_id not in self.session_registry_controller.navigation_request_by_session and session_id not in self.navigation_controller.navigation_target_by_session:
212
+ self.close_readiness_controller.clear_navigation_wait(session_id)
213
+ for target_id, snapshot in tuple(self.navigation_controller.navigation_resume_by_target.items()):
214
+ owner_id = self.session_registry_controller.navigation_owner_by_id.get(target_id)
215
+ if owner_id is not None and not self.close_readiness_controller.navigation_ready(owner_id):
216
+ continue
217
+ self.navigation_controller.navigation_resume_by_target.pop(target_id, None)
218
+ self.navigation_controller.publish_repository(target_id, snapshot)
219
+ if not self.session_registry_controller.close_session_set and not self.session_registry_controller.navigation_request_by_session and not self.navigation_controller.navigation_resume_by_target:
220
+ self.close_readiness_controller.close_timer.stop()
221
+
222
+ def maybe_quit(self, *arguments: object) -> None:
223
+ """Exit only after results have been retained and every native child has drained."""
224
+ if (self.application_quit_requested or not self.session_registry_controller.shutdown_requested
225
+ or self.repository_operation.active_by_id or self.mutation.active_key_set or self.mutation.configuration_busy or self.authentication.active_by_id
226
+ or self.editor.active_by_id or self.mergetool.active_by_id or self.mergetool.series_by_id or self.worktree_service.file_task_by_id or self.worktree_service.continuation_by_id):
227
+ return
228
+ if not self.repository.runner.closed:
229
+ self.repository.close()
230
+ if (not self.application_quit_requested and not self.system_tool.export_by_id and not self.difftool.active_by_id and not self.mergetool.active_by_id and not self.repository.runner.active_by_id
231
+ and not self.repository.runner.pending_requests and not self.worktree_service.file_task_by_id and not self.worktree_service.continuation_by_id):
232
+ self.application_quit_requested = True
233
+ self.leave_event_loop()
234
+
235
+ def close_repository(self, session_id: str, *, keep_navigation: bool = False) -> None:
236
+ """Close one session and preserve all other windows and pending requests."""
237
+ if not keep_navigation and self.close_readiness_controller.defer_repository_close(session_id):
238
+ return
239
+ self.session_registry_controller.navigation_request_by_session.pop(session_id, None)
240
+ if not keep_navigation:
241
+ for run in tuple(self.submodule.run_by_id.values()):
242
+ if run.session_id == session_id:
243
+ self.submodule.cancel(run.operation_id)
244
+ for target_id, owner_id in tuple(self.session_registry_controller.navigation_owner_by_id.items()):
245
+ if owner_id != session_id:
246
+ continue
247
+ self.session_registry_controller.navigation_owner_by_id.pop(target_id, None)
248
+ self.navigation_controller.navigation_kind_by_id.pop(target_id, None)
249
+ self.navigation_controller.navigation_family_by_id.pop(target_id, None)
250
+ self.navigation_controller.navigation_resume_by_target.pop(target_id, None)
251
+ run = self.navigation_controller.submodule_run_by_navigation.pop(target_id, None)
252
+ if run is not None:
253
+ run.open_status = OperationStatus.CANCELLED.value
254
+ self.navigation_controller.navigation_context_by_id.pop(target_id, None)
255
+ self.navigation_controller.navigation_snapshot_by_id.pop(target_id, None)
256
+ self.navigation_controller.navigation_commits_by_id.pop(target_id, None)
257
+ self.navigation_controller.navigation_streamed_history_by_id.pop(target_id, None)
258
+ self.repository.cancel(target_id)
259
+ self.repository.context_by_session.pop(target_id, None)
260
+ self.navigation_controller.navigation_target_by_session.pop(session_id, None)
261
+ self.refresh_controller.submodule_snapshot_by_session.pop(session_id, None)
262
+ self.navigation_controller.anchor_by_window.pop(session_id, None)
263
+ self.mergetool.invalidate_session(session_id)
264
+ self.refresh_controller.leave_full_refresh_read(session_id)
265
+ self.replay_controller.replay_refresh_by_session.pop(session_id, None)
266
+ self.session_registry_controller.replay_pending_resolve_set.discard(session_id)
267
+ for key, request in tuple(self.replay_controller.replay_auto_continue_by_key.items()):
268
+ if request.session_id == session_id:
269
+ self.replay_controller.replay_auto_continue_by_key.pop(key, None)
270
+ for key, resume_session_id in tuple(self.replay_controller.replay_resume_session_by_key.items()):
271
+ if resume_session_id == session_id:
272
+ self.replay_controller.replay_resume_session_by_key.pop(key, None)
273
+ for dialog_by_session in (self.replay_controller.replay_dialog_by_session, self.replay_controller.replay_prompt_by_session,
274
+ self.rebase_controller.rebase_dialog_by_session, self.rebase_controller.rebase_confirmation_by_session, self.stash_controller.stash_dialog_by_session,
275
+ self.conflict_controller.conflict_dialog_by_session, self.conflict_controller.conflict_confirmation_by_session, self.conflict_controller.conflict_prompt_by_session,
276
+ self.navigation_controller.invalid_repository_prompt_by_session):
277
+ dialog = dialog_by_session.pop(session_id, None)
278
+ if dialog is not None and isValid(dialog):
279
+ dialog.close()
280
+ recovery_dialog = self.conflict_controller.merge_recovery_dialog_by_session.pop(session_id, None)
281
+ if recovery_dialog is not None and isValid(recovery_dialog):
282
+ recovery_dialog.close()
283
+ for operation_id, run in tuple(self.transport.run_by_id.items()):
284
+ if run.request.operation.session_id == session_id:
285
+ self.transport.cancel(operation_id)
286
+ if operation_id in self.transport.outcome_by_id:
287
+ run.trace.close()
288
+ checkout_dialog = self.reference_form_controller.checkout_dialog_by_session.pop(session_id, None)
289
+ checkout_revision_dialog = self.reference_form_controller.checkout_revision_dialog_by_session.pop(session_id, None)
290
+ branch_dialog = self.reference_form_controller.branch_dialog_by_session.pop(session_id, None)
291
+ delete_dialog = self.reference_form_controller.delete_dialog_by_session.pop(session_id, None)
292
+ if delete_dialog is not None and isValid(delete_dialog):
293
+ delete_dialog.reject()
294
+ local_branch_delete_dialog = self.reference_form_controller.local_branch_delete_dialog_by_session.pop(session_id, None)
295
+ if local_branch_delete_dialog is not None and isValid(local_branch_delete_dialog):
296
+ local_branch_delete_dialog.reject()
297
+ remote_dialog = self.transport_controller.remote_dialog_by_session.pop(session_id, None)
298
+ push_dialog = self.transport_controller.push_dialog_by_session.pop(session_id, None)
299
+ if push_dialog is not None and isValid(push_dialog):
300
+ push_dialog.reject()
301
+ push_rejection_prompt = self.transport_controller.push_rejection_prompt_by_session.pop(session_id, None)
302
+ if push_rejection_prompt is not None and isValid(push_rejection_prompt):
303
+ push_rejection_prompt.reject()
304
+ # A question parented on the Push window outlives its rejection, since Qt hides no child window then.
305
+ push_tracking_question = self.transport_controller.push_tracking_question_by_session.pop(session_id, None)
306
+ if push_tracking_question is not None and isValid(push_tracking_question):
307
+ push_tracking_question.reject()
308
+ if remote_dialog is not None and isValid(remote_dialog):
309
+ remote_dialog.reject()
310
+ tag_dialog = self.reference_form_controller.tag_dialog_by_session.pop(session_id, None)
311
+ if tag_dialog is not None and isValid(tag_dialog):
312
+ tag_dialog.reject()
313
+ reset_dialog = self.reference_form_controller.reset_dialog_by_session.pop(session_id, None)
314
+ # A form may still wait for its opening check and never have been shown, and close() then emits no finished.
315
+ for form in (reset_dialog, branch_dialog, checkout_dialog, checkout_revision_dialog):
316
+ if form is not None and isValid(form):
317
+ form.done(QDialog.DialogCode.Rejected)
318
+ self.reference_read_controller.reference_snapshot_by_session.pop(session_id, None)
319
+ self.reference_read_controller.reference_read_generation_by_session.pop(session_id, None)
320
+ self.reference_read_controller.logged_stash_cause_by_session.pop(session_id, None)
321
+ self.reference_read_controller.reference_continuation_by_session.pop(session_id, None)
322
+ self.refresh_controller.checkout_refresh_session_set.discard(session_id)
323
+ # The window id outlives its session when the window moves to another repository: what the previous repository
324
+ # still had to do (scroll to the top, offer to resolve its conflicts, read its remotes for Fetch) must never
325
+ # happen in the repository the same window shows next.
326
+ self.history_read_controller.history_top_session_set.discard(session_id)
327
+ self.session_registry_controller.conflict_pending_session_set.discard(session_id)
328
+ self.transport_controller.fetch_read_by_session.pop(session_id, None)
329
+ self.rebase_controller.rebase_final_oid_by_session.pop(session_id, None)
330
+ self.rebase_controller.rebase_end_notice_session_set.discard(session_id)
331
+ for identity, owner_id in tuple(self.session_registry_controller.export_owner_by_identity.items()):
332
+ if owner_id == session_id:
333
+ self.session_registry_controller.export_owner_by_identity.pop(identity, None)
334
+ self.system_tool.cancel_export(identity)
335
+ export_id = identity.request_identity.session_id
336
+ self.repository.cancel(export_id)
337
+ self.repository.context_by_session.pop(export_id, None)
338
+ self.session_registry_controller.window_by_session.pop(session_id, None)
339
+ self.session_registry_controller.identity_by_session.pop(session_id, None)
340
+ session = self.session_registry_controller.session_by_id.pop(session_id, None)
341
+ if session is not None:
342
+ session.refresh_queued = None
343
+ session.close()
344
+ # A worktree that has just lost its last window must stop refreshing itself, and no activation event says so.
345
+ self.window_activation_controller.schedule_activation_update()
@@ -0,0 +1,199 @@
1
+ """Coordinate the Prepare changes window of each worktree with the rest of GitPane."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from typing import TYPE_CHECKING
7
+
8
+ from shiboken6 import isValid
9
+
10
+ from gitpane.history.file_history import WindowCapability
11
+ from gitpane.history.repository_value import RequestIdentity
12
+ from gitpane.operation.diagnostic import Diagnostic
13
+ from gitpane.operation.operation_status import OperationStatus
14
+ from gitpane.system.path_identity import canonical_path
15
+ from gitpane.tool.system_tool import text_editor_unavailable_reason
16
+ from gitpane.transport.remote_config import GitConfigScope
17
+ from gitpane.ui.repository.repository_window import COMMIT_NOTICE_KIND
18
+ from gitpane.worktree.worktree_snapshot import WorktreeKey
19
+
20
+ if TYPE_CHECKING:
21
+ from gitpane.commit.commit_write import CommitOutcome
22
+ from gitpane.controller.emoji_window_controller import EmojiWindowController
23
+ from gitpane.controller.file_action_controller import FileActionController
24
+ from gitpane.controller.history_read_controller import HistoryReadController
25
+ from gitpane.controller.opening_controller import OpeningController
26
+ from gitpane.controller.operation_policy import OperationPolicy
27
+ from gitpane.controller.refresh_controller import RefreshController
28
+ from gitpane.controller.session_registry import SessionRegistry
29
+ from gitpane.controller.staging_session_registry import StagingSessionRegistry
30
+ from gitpane.controller.window_activation import WindowActivationFilter
31
+ from gitpane.controller.worktree_state_controller import WorktreeStateController
32
+ from gitpane.history.file_history import FileHistoryAnchor
33
+ from gitpane.history.repository import GitRepository
34
+ from gitpane.history.repository_value import RepositoryContext
35
+ from gitpane.operation.diagnostic import DiagnosticLog
36
+ from gitpane.preference.preference_store import PreferenceStore
37
+ from gitpane.ui.repository.repository_window import RepositoryWindow
38
+ from gitpane.ui.staging.staging_window import StagingWindow
39
+ from gitpane.worktree.mutation import MutationOutcome
40
+ from gitpane.worktree.staging import StagingSession
41
+
42
+
43
+ class StagingController:
44
+ """Prepare changes of every worktree: its opening from a repository window, its window bound to the shared preferences, and the reads that its commits, its ignore changes and its index writes ask from the other worktrees and windows."""
45
+
46
+ def __init__(self, *, session_registry_controller: SessionRegistry, operation_policy_controller: OperationPolicy, staging_session_registry_controller: StagingSessionRegistry, window_activation_controller: WindowActivationFilter,
47
+ refresh_controller: RefreshController, worktree_state_controller: WorktreeStateController, history_read_controller: HistoryReadController, opening_controller: OpeningController, emoji_window_controller: EmojiWindowController,
48
+ file_action_controller: FileActionController, preference: PreferenceStore, repository: GitRepository, diagnostic_log: DiagnosticLog | None) -> None:
49
+ """Receive the services and the lower controllers Prepare changes works with; the wiring builds it after the file action controller, and the repository windows and the preparation sessions reach it through the wiring."""
50
+ self.session_registry_controller = session_registry_controller
51
+ self.operation_policy_controller = operation_policy_controller
52
+ self.staging_session_registry_controller = staging_session_registry_controller
53
+ self.window_activation_controller = window_activation_controller
54
+ self.refresh_controller = refresh_controller
55
+ self.worktree_state_controller = worktree_state_controller
56
+ self.history_read_controller = history_read_controller
57
+ self.opening_controller = opening_controller
58
+ self.emoji_window_controller = emoji_window_controller
59
+ self.file_action_controller = file_action_controller
60
+ self.preference = preference
61
+ self.repository = repository
62
+ self.diagnostic_log = diagnostic_log
63
+
64
+ def open_staging(self, session_id: str) -> StagingWindow | None:
65
+ """Open shared preparation only from a live management window with a working tree."""
66
+ if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
67
+ return None
68
+ session = self.session_registry_controller.session_by_id[session_id]
69
+ if session.repository_context.is_bare or session.repository_context.worktree_dir is None:
70
+ return None
71
+ staging = self.staging_session_registry_controller.staging_session(session.repository_context)
72
+ staging.commit_push_session_id = session_id
73
+ return staging.open()
74
+
75
+ def log_mutation_result(self, outcome: MutationOutcome) -> None:
76
+ """Include prepared write failures in the optional application diagnostic file."""
77
+ if outcome.diagnostic is not None and self.diagnostic_log is not None:
78
+ self.diagnostic_log.write(outcome.diagnostic)
79
+ staging = self.staging_session_registry_controller.staging_by_key.get(outcome.key)
80
+ if staging is not None:
81
+ self.refresh_submodule_family(staging.context)
82
+
83
+ def refresh_submodule_family(self, context: RepositoryContext) -> None:
84
+ """Refresh declared relatives across separate Git directories after a local write."""
85
+ directory = canonical_path(context.repository_dir)
86
+ related_dir_set: set[str] = set()
87
+ for session_id, family in tuple(self.refresh_controller.submodule_snapshot_by_session.items()):
88
+ family_dir_set = {canonical_path(entry.repository_dir) for entry in family.entries if entry.repository_dir}
89
+ if directory not in family_dir_set:
90
+ continue
91
+ session = self.session_registry_controller.session_by_id.get(session_id)
92
+ if session is None or session.is_closed or canonical_path(session.repository_context.repository_dir) not in family_dir_set:
93
+ continue
94
+ related_dir_set.update(family_dir_set)
95
+ self.refresh_controller.read_submodules(session_id)
96
+ for staging in tuple(self.staging_session_registry_controller.staging_by_key.values()):
97
+ if canonical_path(staging.context.repository_dir) in related_dir_set and WorktreeKey.from_context(staging.context) != WorktreeKey.from_context(context):
98
+ staging.refresh()
99
+
100
+ def git_setting_changed(self, result: object) -> None:
101
+ """Refresh every affected worktree after conversion changes shared across windows."""
102
+ context, results = result
103
+ changed_scope_set: set[GitConfigScope] = set()
104
+ for outcome in results:
105
+ if outcome.saved and outcome.change.key == "core.autocrlf":
106
+ changed_scope_set.add(outcome.change.scope)
107
+ if not changed_scope_set:
108
+ return
109
+ for staging in self.staging_session_registry_controller.staging_by_key.values():
110
+ if GitConfigScope.GLOBAL in changed_scope_set or (context is not None and canonical_path(staging.context.common_git_dir) == canonical_path(context.common_git_dir)):
111
+ staging.invalidate_conversion()
112
+
113
+ def staging_window_opened(self, window: StagingWindow) -> None:
114
+ """Bind a new preparation view to shared persisted proportions and its current screen, and open it over the repository window that asked for it."""
115
+ window.installEventFilter(self.window_activation_controller)
116
+ window.closed.connect(self.window_activation_controller.schedule_activation_update)
117
+ for staging in self.staging_session_registry_controller.staging_by_key.values():
118
+ if staging.window is not window:
119
+ continue
120
+ if staging.commit_push_session_id is not None:
121
+ window.placement_anchor = self.session_registry_controller.window_by_session.get(staging.commit_push_session_id)
122
+ context = staging.context
123
+ window.text_editor_reason = lambda entry, context=context: text_editor_unavailable_reason(context, entry.path, self.file_action_controller.text_editor_command(context))
124
+ if context.repository_dir not in self.file_action_controller.git_editor_setting_by_repository_dir:
125
+ self.file_action_controller.read_git_editor_setting(context)
126
+ window.apply_preference(self.preference.value_by_key)
127
+ window.preference_requested.connect(lambda values: self._save_staging_preference(window, values))
128
+ self.emoji_window_controller.report_emoji_font()
129
+
130
+ def _save_staging_preference(self, window: StagingWindow, value_by_key: dict[str, object]) -> None:
131
+ """Restore accepted pane weights if storage refuses a change, preserving the message."""
132
+ if not isValid(window):
133
+ return
134
+ try:
135
+ self.preference.update(value_by_key)
136
+ except (OSError, ValueError, TypeError) as error:
137
+ window.apply_preference(self.preference.value_by_key)
138
+ diagnostic = Diagnostic("Save staging preferences", window.context.repository_dir, "", [], str(error)).prepare()
139
+ window.status_label.setText(diagnostic.cause)
140
+ if self.diagnostic_log is not None:
141
+ self.diagnostic_log.write(diagnostic)
142
+
143
+ def open_staging_history(self, staging: StagingSession, anchor: FileHistoryAnchor) -> RepositoryWindow | None:
144
+ """Open the existing limited history view for a locally resolved committed blob."""
145
+ if self.staging_session_registry_controller.staging_by_key.get(staging.key) is not staging or staging.window is None or staging.window.close_requested or staging.busy:
146
+ return None
147
+ if staging.snapshot is None or not staging.snapshot.fresh or staging.snapshot.head_oid != anchor.commit_oid:
148
+ return None
149
+ identity = RequestIdentity(uuid.uuid4().hex, 1)
150
+ self.session_registry_controller.identity_by_session[identity.session_id] = identity
151
+ self.repository.context_by_session[identity.session_id] = staging.context
152
+ self.opening_controller.opened(identity, staging.context, None, anchor, staging.window)
153
+ return self.session_registry_controller.window_by_session[identity.session_id]
154
+
155
+ def ignore_changed(self, context: RepositoryContext, *, refreshed_key: WorktreeKey | None = None) -> None:
156
+ """Reread every worktree sharing the repository's exclude file, except the one named as already reading.
157
+
158
+ That key names the session whose staging asked for the read itself; it is not derived from the context, which
159
+ can belong to a linked worktree that has not read yet. Without a key, every sharing worktree reads.
160
+ """
161
+ common_dir = canonical_path(context.common_git_dir)
162
+ for staging in self.staging_session_registry_controller.staging_by_key.values():
163
+ if staging.key == refreshed_key:
164
+ continue
165
+ if canonical_path(staging.context.common_git_dir) == common_dir:
166
+ staging.refresh()
167
+
168
+ def commit_finished(self, staging: StagingSession, outcome: CommitOutcome) -> None:
169
+ """Refresh shared history after native writes without sharing linked-worktree drafts.
170
+
171
+ The committing worktree shows the complete read the commit made after writing; its preparation session reads
172
+ again on its own when that read cannot serve. Every other worktree of the repository reads once for all its windows.
173
+
174
+ A successful Commit or Amend puts its commit at the top of the graph, so every repository window of the committing
175
+ worktree goes back to its top once that read is shown. Other worktrees, whose branch did not move, and a refused
176
+ or failed commit leave the graph where it was.
177
+
178
+ A successful commit around which a hook or another program changed files writes those details to the --log
179
+ file. The neutral notice of the commit, or its absence, replaces the previous commit's notice in every
180
+ repository window of the worktree, next to any other notice they show.
181
+ """
182
+ self.log_mutation_result(outcome)
183
+ if outcome.log_diagnostic is not None and self.diagnostic_log is not None:
184
+ self.diagnostic_log.write(outcome.log_diagnostic)
185
+ for _session_id, window in self.worktree_state_controller.managed_windows_of_worktree(staging.key):
186
+ window.show_status_notice(staging.key, COMMIT_NOTICE_KIND, outcome.notice)
187
+ common_dir = canonical_path(staging.context.common_git_dir)
188
+ if outcome.status == OperationStatus.SUCCESS:
189
+ for session_id, session in self.session_registry_controller.session_by_id.items():
190
+ if not session.is_closed and session.mode == "repository" and WorktreeKey.from_context(session.repository_context) == staging.key:
191
+ self.history_read_controller.history_top_session_set.add(session_id)
192
+ shared_by_key = {staging.key: self.refresh_controller.begin_full_refresh_read(staging.key, published_staging=staging)}
193
+ for session_id, session in tuple(self.session_registry_controller.session_by_id.items()):
194
+ if not session.is_closed and canonical_path(session.repository_context.common_git_dir) == common_dir:
195
+ self.refresh_controller.refresh(session_id, full_refresh_read=self.refresh_controller.full_refresh_read_for(session, shared_by_key))
196
+ refreshed_key_set = self.refresh_controller.observe_full_refresh_pauses(shared_by_key)
197
+ for other in self.staging_session_registry_controller.staging_by_key.values():
198
+ if other is not staging and other.key not in refreshed_key_set and canonical_path(other.context.common_git_dir) == common_dir:
199
+ other.refresh()
@@ -0,0 +1,47 @@
1
+ """Reuse one preparation session per worktree across the windows that show it."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from PySide6.QtCore import QObject
8
+
9
+ from gitpane.worktree.staging import StagingSession
10
+ from gitpane.worktree.worktree_snapshot import WorktreeKey
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Callable
14
+
15
+ from PySide6.QtWidgets import QWidget
16
+
17
+ from gitpane.history.repository import GitRepository
18
+ from gitpane.history.repository_value import RepositoryContext
19
+ from gitpane.sequencer.rebase import RebaseService
20
+ from gitpane.worktree.mutation import MutationService
21
+ from gitpane.worktree.worktree_service import WorktreeService
22
+
23
+
24
+ class StagingSessionRegistry(QObject):
25
+ """The preparation session of each worktree, created once, owned here and shared by every window of that worktree."""
26
+
27
+ def __init__(self, *, repository: GitRepository, worktree_service: WorktreeService, rebase: RebaseService, mutation: MutationService,
28
+ wire_staging_session: Callable[[WorktreeKey, StagingSession], None], window_factory: Callable[[RepositoryContext], QWidget], parent: QObject | None = None) -> None:
29
+ """Receive the services a session works with, the function of the wiring that connects a new session to the controllers above this registry, and the
30
+ factory every session builds its Prepare changes window with."""
31
+ super().__init__(parent)
32
+ self.repository = repository
33
+ self.worktree_service = worktree_service
34
+ self.rebase = rebase
35
+ self.mutation = mutation
36
+ self.wire_staging_session = wire_staging_session
37
+ self.window_factory = window_factory
38
+ self.staging_by_key: dict[WorktreeKey, StagingSession] = {}
39
+
40
+ def staging_session(self, context: RepositoryContext) -> StagingSession:
41
+ """Reuse one index owner and in-memory draft across repository windows."""
42
+ key = WorktreeKey.from_context(context)
43
+ if key not in self.staging_by_key:
44
+ staging = StagingSession(context, self.repository.git_filename, self.worktree_service, self.mutation, self, rebase_service=self.rebase, window_factory=self.window_factory)
45
+ self.staging_by_key[key] = staging
46
+ self.wire_staging_session(key, staging)
47
+ return self.staging_by_key[key]