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,337 @@
1
+ """Hold the open repository sessions of GitPane with what every controller reads about them."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import weakref
6
+ from dataclasses import dataclass, field
7
+ from enum import Enum
8
+ from typing import TYPE_CHECKING, Self
9
+
10
+ from PySide6.QtCore import QEvent, QObject, QTimer, Signal
11
+ from shiboken6 import isValid
12
+
13
+ from gitpane.history.file_history import WINDOW_CAPABILITY_BY_MODE
14
+ from gitpane.history.repository_value import FilePanel, validate_oid
15
+
16
+ if TYPE_CHECKING:
17
+ from PySide6.QtCore import QMetaObject
18
+ from PySide6.QtGui import QScreen
19
+ from PySide6.QtWidgets import QApplication, QMessageBox
20
+
21
+ from gitpane.controller.refresh_controller import RefreshRequest
22
+ from gitpane.history.file_history import FileHistoryAnchor, FileHistoryEngine, FileHistoryResult
23
+ from gitpane.history.repository import GitRepository
24
+ from gitpane.history.repository_value import Commit, ComparisonGroup, FileReadIdentity, HistorySnapshot, RepositoryContext, RequestIdentity, RevisionEntry, TreeEntry
25
+ from gitpane.ui.console.error_dialog import ErrorDialog
26
+ from gitpane.ui.repository.repository_window import RepositoryWindow, ViewAnchor
27
+ from gitpane.ui.setting.setting_dialog import SettingDialog
28
+ from gitpane.ui.staging.staging_window import StagingWindow
29
+ from gitpane.ui.transport.clone_dialog import CloneDialog
30
+ from gitpane.ui.welcome.welcome_window import WelcomeWindow
31
+ from gitpane.worktree.worktree_snapshot import WorktreeKey
32
+
33
+
34
+ class WindowDisplayBinding(QObject):
35
+ """Follow one window's screen and system appearance without altering its session.
36
+
37
+ The binding never keeps a QScreen object. PySide has a single Python object per screen and makes it a child of the
38
+ last widget or native window whose screen() returned it; once that window is destroyed, PySide marks the object
39
+ deleted for everyone who holds it, although the screen is still there. GitPane centres each repository window on the
40
+ Open repository window, reading that window's screen, then deletes the Open window: a kept object was dead from the
41
+ start and the repository window stopped following font, density and usable-area changes. Any other window that read
42
+ the screen last and is then closed does the same. The screen is therefore asked from the window at each refresh, and
43
+ the screen's notifications are held as connections, which can be cut without that object.
44
+ """
45
+
46
+ def __init__(self, window: WelcomeWindow | RepositoryWindow | StagingWindow | SettingDialog | ErrorDialog, application: QApplication) -> None:
47
+ """Connect once per native window and coalesce appearance events into one update."""
48
+ super().__init__(window)
49
+ self.window_ref = weakref.ref(window)
50
+ self.application = application
51
+ self.screen_connections: list[QMetaObject.Connection] = []
52
+ self.handle = None
53
+ self.ratio: float | None = None
54
+ self.refreshing = False
55
+ self.timer = QTimer(self)
56
+ self.timer.setSingleShot(True)
57
+ self.timer.timeout.connect(self.refresh)
58
+ window.installEventFilter(self)
59
+ application.screenRemoved.connect(self.screen_removed)
60
+ self.attach_handle()
61
+
62
+ def attach_handle(self) -> None:
63
+ """Attach to a newly created native handle without duplicating its screen callback."""
64
+ window = self.window_ref()
65
+ if window is None or not isValid(window):
66
+ return
67
+ handle = window.windowHandle()
68
+ if handle is None or handle is self.handle:
69
+ return
70
+ if self.handle is not None and isValid(self.handle):
71
+ self.handle.screenChanged.disconnect(self.set_screen)
72
+ self.handle = handle
73
+ handle.screenChanged.connect(self.set_screen)
74
+ self.set_screen(handle.screen())
75
+
76
+ def set_screen(self, screen: QScreen | None) -> None:
77
+ """Listen to the usable-area and density changes of the window's new screen only, then refresh."""
78
+ screen = screen if screen is not None else self.application.primaryScreen()
79
+ for connection in self.screen_connections:
80
+ QObject.disconnect(connection)
81
+ self.screen_connections = []
82
+ if screen is not None:
83
+ self.screen_connections = [screen.availableGeometryChanged.connect(self.schedule_refresh),
84
+ screen.logicalDotsPerInchChanged.connect(self.schedule_refresh)]
85
+ self.schedule_refresh()
86
+
87
+ def screen_removed(self, screen: QScreen) -> None:
88
+ """Follow the primary screen when the window's screen is unplugged, until Qt moves the window to a remaining one."""
89
+ if self.handle is not None and isValid(self.handle) and self.handle.screen() is screen:
90
+ self.set_screen(self.application.primaryScreen())
91
+
92
+ def schedule_refresh(self, *arguments: object) -> None:
93
+ """Collapse bursts of native events while ignoring our own layout changes."""
94
+ if not self.refreshing and not self.timer.isActive():
95
+ self.timer.start(0)
96
+
97
+ def eventFilter(self, watched: QObject, event: QEvent) -> bool:
98
+ """React to window appearance changes while preserving the native event behavior."""
99
+ if event.type() in (QEvent.Type.Show, QEvent.Type.WinIdChange):
100
+ self.attach_handle()
101
+ if event.type() in (QEvent.Type.Show, QEvent.Type.ApplicationFontChange, QEvent.Type.FontChange, QEvent.Type.ApplicationPaletteChange,
102
+ QEvent.Type.PaletteChange, QEvent.Type.DevicePixelRatioChange, QEvent.Type.ScreenChangeInternal):
103
+ self.schedule_refresh()
104
+ return super().eventFilter(watched, event)
105
+
106
+ def refresh(self) -> None:
107
+ """Rebuild logical metrics and density-specific photos without resetting data or focus, for the screen the window is on now."""
108
+ window = self.window_ref()
109
+ if window is None or not isValid(window) or self.refreshing or not window.isVisible():
110
+ return
111
+ screen = window.screen()
112
+ if screen is None:
113
+ return
114
+ self.refreshing = True
115
+ try:
116
+ window.refresh_metrics(screen.availableGeometry())
117
+ ratio = window.devicePixelRatioF()
118
+ panel = getattr(window, "commit_panel", None)
119
+ if panel is not None and ratio != self.ratio:
120
+ panel.refresh_avatar(ratio)
121
+ self.ratio = ratio
122
+ window.update()
123
+ finally:
124
+ self.refreshing = False
125
+
126
+
127
+ class NavigationKind(Enum):
128
+ """The kind of one change of repository inside the same window.
129
+
130
+ It decides what the window carries over: a walk into a submodule or back to its parent keeps the family, the
131
+ position and the filter of the repository it returns to; a repository chosen in the switch menu keeps nothing.
132
+ Each kind carries its own texts: the operation named in a failure's details, the status line of a failure, and the
133
+ cause given when the target turns out to be another repository.
134
+ """
135
+
136
+ SUBMODULE = ("submodule", "Open submodule",
137
+ "Could not open submodule '{directory}'. The current repository is still open. Review the details, fix the reported problem, then open the submodule again.",
138
+ "The target does not resolve to its declared repository.")
139
+ SWITCH = ("switch", "Open repository",
140
+ "Could not open repository '{directory}'. The current repository is still open. Review the details, fix the reported problem, then choose it again.",
141
+ "The folder is no longer the top folder of a Git repository: Git opens another repository from it.")
142
+
143
+ def __new__(cls, value: str, operation_label: str, failure_status: str, target_mismatch_text: str) -> Self:
144
+ """Keep the kind's name as its value, and its three texts beside it."""
145
+ member = object.__new__(cls)
146
+ member._value_ = value
147
+ member.operation_label = operation_label
148
+ member.failure_status = failure_status
149
+ member.target_mismatch_text = target_mismatch_text
150
+ return member
151
+
152
+
153
+ @dataclass
154
+ class StreamedHistory:
155
+ """The rows the history read delivered for one history, in the order they came.
156
+
157
+ They are kept so that a search with an empty query, which shows every commit, can publish them again without
158
+ reading Git: a refresh, a walk into another repository, a file history without a filter, or a cleared filter.
159
+ """
160
+
161
+ snapshot: HistorySnapshot
162
+ commits: list[Commit] = field(default_factory=list)
163
+
164
+ def complete_commits(self, snapshot: HistorySnapshot) -> list[Commit] | None:
165
+ """Answer the kept rows only when they are exactly every commit of this complete history, in its order."""
166
+ if self.snapshot is not snapshot or not snapshot.complete or len(self.commits) != len(snapshot.ordered_oids):
167
+ return None
168
+ for commit, oid in zip(self.commits, snapshot.ordered_oids, strict=True):
169
+ if commit.oid != oid:
170
+ return None
171
+ return self.commits
172
+
173
+
174
+ def keep_streamed_commits(streamed_history: StreamedHistory | None, snapshot: HistorySnapshot, commits: list[Commit]) -> StreamedHistory:
175
+ """Add delivered rows to the kept rows of their history, starting over when they belong to a new history."""
176
+ if streamed_history is None or streamed_history.snapshot is not snapshot:
177
+ streamed_history = StreamedHistory(snapshot)
178
+ streamed_history.commits.extend(commits)
179
+ return streamed_history
180
+
181
+
182
+ class RepositorySession(QObject):
183
+ """Own navigation and request generations for one repository window."""
184
+
185
+ selection_changed = Signal(object)
186
+ navigation_requested = Signal(str)
187
+ closed = Signal()
188
+
189
+ def __init__(self, session_id: str, repository_context: RepositoryContext, repository: GitRepository, file_history_anchor: FileHistoryAnchor | None = None) -> None:
190
+ """Associate independent navigation state with an injected Git service."""
191
+ super().__init__()
192
+ self.session_id = session_id
193
+ self.repository_context = repository_context
194
+ self.repository = repository
195
+ self.file_history_anchor = file_history_anchor
196
+ self.file_history_generation = 0
197
+ self.file_history_result: FileHistoryResult | None = None
198
+ self.file_history_engine: FileHistoryEngine | None = None
199
+ self.mode = "file_history" if file_history_anchor is not None else "repository"
200
+ self.capabilities = WINDOW_CAPABILITY_BY_MODE[self.mode]
201
+ self.visible_oid_set: set[str] = set()
202
+ self.generation = 0
203
+ self.selection_generation = 0
204
+ self.selected_oid: str | None = None
205
+ self.filter_text = ""
206
+ self.anchor_oid: str | None = None
207
+ self.task_id_set: set[str] = set()
208
+ self.is_closed = False
209
+ self.snapshot: HistorySnapshot | None = None
210
+ self.search_generation = 0
211
+ self.search_identity: RequestIdentity | None = None
212
+ self.search_pending = False
213
+ self.matching_commits: list[Commit] = []
214
+ self.streamed_history: StreamedHistory | None = None
215
+ self.refresh_active = False
216
+ # The refresh asked while another one runs; it keeps its scope, its capture and its generation until it starts.
217
+ self.refresh_queued: RefreshRequest | None = None
218
+ self.view_anchor: ViewAnchor | None = None
219
+ self.relation_generation = 0
220
+ self.relation_identity: RequestIdentity | None = None
221
+ self.navigation_oid: str | None = None
222
+ self.navigation_commit: Commit | None = None
223
+ self.navigation_filter_cleared = False
224
+ self.panel_generation = 0
225
+ self.file_identity_by_kind: dict[FilePanel, FileReadIdentity] = {}
226
+ self.diff_groups: list[ComparisonGroup] = []
227
+ self.selected_entry: RevisionEntry | None = None
228
+ self.tree_entries: list[TreeEntry] = []
229
+ # The tree read that answered while its tab was hidden; its entries are built when the File tree tab comes back.
230
+ self.pending_tree_identity: FileReadIdentity | None = None
231
+ self.selected_tree_entry: TreeEntry | None = None
232
+ # The request whose commit details are on screen; the file panels read for that very request.
233
+ self.commit_identity: RequestIdentity | None = None
234
+
235
+ def search(self, text: str) -> None:
236
+ """Keep the exact query and invalidate every older message scan immediately."""
237
+ if self.is_closed:
238
+ return
239
+ self.filter_text = text
240
+ self.search_generation += 1
241
+ self.search_identity = None
242
+ self.search_pending = True
243
+ self.matching_commits.clear()
244
+ self.repository.cancel_search(self.session_id)
245
+
246
+ def select_commit(self, oid: str) -> None:
247
+ """Change this window's selection without affecting another open repository."""
248
+ if self.is_closed or self.selected_oid == oid:
249
+ return
250
+ if self.mode == "file_history" and (self.search_pending or oid not in self.visible_oid_set):
251
+ return
252
+ self.selected_oid = oid
253
+ self.navigation_oid = None
254
+ self.navigation_commit = None
255
+ self.navigation_filter_cleared = False
256
+ self.selection_generation += 1
257
+ self.invalidate_file_selection()
258
+ self.selection_changed.emit(oid)
259
+
260
+ def invalidate_file_selection(self) -> None:
261
+ """Forget every old file comparison before a new commit can be displayed."""
262
+ self.panel_generation += 1
263
+ self.file_identity_by_kind.clear()
264
+ self.diff_groups.clear()
265
+ self.selected_entry = None
266
+ self.tree_entries.clear()
267
+ self.pending_tree_identity = None
268
+ self.selected_tree_entry = None
269
+ for panel_kind in (FilePanel.DIFF, FilePanel.TREE, FilePanel.BLOB):
270
+ self.repository.cancel_file(self.session_id, panel_kind)
271
+
272
+ def close(self) -> None:
273
+ """Invalidate outstanding work once and release this session's navigation."""
274
+ if self.is_closed:
275
+ return
276
+ self.is_closed = True
277
+ if self.file_history_engine is not None:
278
+ self.file_history_engine.finished.disconnect()
279
+ self.file_history_engine.progressed.disconnect()
280
+ self.file_history_engine.close()
281
+ self.generation += 1
282
+ self.selection_generation += 1
283
+ self.search_generation += 1
284
+ self.file_history_generation += 1
285
+ self.relation_generation += 1
286
+ self.search_identity = None
287
+ self.relation_identity = None
288
+ self.search_pending = False
289
+ self.matching_commits.clear()
290
+ self.streamed_history = None
291
+ self.navigation_oid = None
292
+ self.navigation_commit = None
293
+ self.invalidate_file_selection()
294
+ self.task_id_set.clear()
295
+ self.repository.cancel(self.session_id)
296
+ self.repository.context_by_session.pop(self.session_id, None)
297
+ self.closed.emit()
298
+
299
+ def navigate_to_commit(self, oid: str) -> None:
300
+ """Validate an explicit target and invalidate any older navigation before reading it."""
301
+ if self.is_closed or (self.mode == "file_history" and self.search_pending):
302
+ return
303
+ validate_oid(oid)
304
+ if self.mode == "file_history" and oid not in self.visible_oid_set:
305
+ return
306
+ self.select_commit(oid)
307
+ self.selection_generation += 1
308
+ self.navigation_oid = oid
309
+ self.navigation_requested.emit(oid)
310
+
311
+
312
+ class SessionRegistry:
313
+ """The sessions, windows, dialogs and flags that the controllers of GitPane share."""
314
+
315
+ def __init__(self) -> None:
316
+ """Start with no session open; the wiring builds the registry before any service, so every controller can receive it."""
317
+ self.dialogs: list[ErrorDialog | QMessageBox] = []
318
+ self.clone_dialog_by_session: dict[str, CloneDialog] = {}
319
+ self.clone_generation_by_session: dict[str, int] = {}
320
+ self.export_owner_by_identity: dict[FileReadIdentity, str] = {}
321
+ self.setting_dialog: SettingDialog | None = None
322
+ self.welcome_by_session: dict[str, WelcomeWindow] = {}
323
+ self.window_by_session: dict[str, RepositoryWindow] = {}
324
+ self.session_by_id: dict[str, RepositorySession] = {}
325
+ self.generation_by_session: dict[str, int] = {}
326
+ self.opening_dir_by_session: dict[str, str] = {}
327
+ self.identity_by_session: dict[str, RequestIdentity] = {}
328
+ self.navigation_owner_by_id: dict[str, str] = {}
329
+ self.shutdown_requested = False
330
+ self.quit_pending = False
331
+ self.close_session_set: set[str] = set()
332
+ self.close_reread_count_by_key: dict[WorktreeKey, int] = {}
333
+ self.navigation_request_by_session: dict[str, tuple[RepositorySession, str, NavigationKind]] = {}
334
+ # Sessions whose conflict list opens, for a paused replay, or whose conflict resolution is offered, after a Stash
335
+ # pop left conflicts, once the next fresh read of their worktree arrives.
336
+ self.replay_pending_resolve_set: set[str] = set()
337
+ self.conflict_pending_session_set: set[str] = set()