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,671 @@
1
+ """Read from Git what a repository window shows of its history."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import replace
6
+ from typing import TYPE_CHECKING
7
+
8
+ from PySide6.QtCore import QObject, QSignalBlocker, QTimer, Signal
9
+ from shiboken6 import isValid
10
+
11
+ from gitpane.config import SharedText
12
+ from gitpane.controller.session_registry import keep_streamed_commits
13
+ from gitpane.history.file_history import FileHistoryEngine, WindowCapability, visible_parent_map
14
+ from gitpane.history.history_filter import HistoryFilterMode
15
+ from gitpane.history.history_parse import summarize_commit
16
+ from gitpane.history.repository_value import FilePanel, FileReadIdentity, RequestIdentity
17
+ from gitpane.operation.diagnostic import Diagnostic
18
+ from gitpane.operation.operation_status import OperationStatus
19
+ from gitpane.text.text_query import TextQuery
20
+ from gitpane.transport.refusal_notice import failure_summary
21
+ from gitpane.ui.console.error_dialog import ErrorDialog
22
+ from gitpane.ui.welcome.welcome_window import WelcomeWindow
23
+ from gitpane.ui.window_control.dialog_modality import open_window_modal
24
+
25
+ if TYPE_CHECKING:
26
+ from gitpane.controller.operation_policy import OperationPolicy
27
+ from gitpane.controller.reference_read_controller import ReferenceReadController
28
+ from gitpane.controller.refresh_controller import RefreshController
29
+ from gitpane.controller.session_registry import RepositorySession, SessionRegistry
30
+ from gitpane.history.file_history import FileHistoryResult
31
+ from gitpane.history.repository import GitRepository
32
+ from gitpane.history.repository_value import BlobContent, Commit, CommitRelation, ComparisonGroup, HistorySnapshot, RepositoryFailure, RevisionEntry, TreeEntry
33
+ from gitpane.operation.diagnostic import DiagnosticLog
34
+ from gitpane.preference.preference_store import PreferenceStore
35
+ from gitpane.tool.system_tool import SystemToolService
36
+ from gitpane.transport.clone import CloneService
37
+
38
+
39
+ class HistoryReadController(QObject):
40
+ """The history reads of every repository window: the graph and its search, the selected commit with its relatives, its files, its tree and the diff of one file."""
41
+
42
+ # session id, generation of the history just shown in the window
43
+ history_published = Signal(str, int)
44
+ # session id, reference capture the refresh read (or None), once the published history wrote its status line
45
+ history_status_written = Signal(str, object)
46
+ # session id, prepared diagnostic of the read that ended a refresh
47
+ refresh_failed = Signal(str, object)
48
+
49
+ def __init__(self, *, session_registry_controller: SessionRegistry, operation_policy_controller: OperationPolicy, reference_read_controller: ReferenceReadController, refresh_controller: RefreshController, preference: PreferenceStore,
50
+ repository: GitRepository, clone: CloneService, diagnostic_log: DiagnosticLog | None, system_tool: SystemToolService, parent: QObject | None = None) -> None:
51
+ """Receive the services and the lower controllers the reads go through; the wiring builds it after the worktree state controller, and the repository service, the windows and the operations ask it to read."""
52
+ super().__init__(parent)
53
+ self.session_registry_controller = session_registry_controller
54
+ self.repository = repository
55
+ self.operation_policy_controller = operation_policy_controller
56
+ self.preference = preference
57
+ self.diagnostic_log = diagnostic_log
58
+ self.system_tool = system_tool
59
+ self.reference_read_controller = reference_read_controller
60
+ self.refresh_controller = refresh_controller
61
+ self.clone = clone
62
+ # Windows whose graph goes back to its top once the history read after their operation is shown: an operation
63
+ # that adds commits or moves a branch to a new commit puts its result at the top of the graph. An operation that
64
+ # names a commit the user clicked, a tag or a reset of a branch, leaves the graph where it was.
65
+ self.history_top_session_set: set[str] = set()
66
+ self.reported_request_set: set[RequestIdentity] = set()
67
+
68
+ def read_selection(self, session_id: str, oid: str) -> None:
69
+ """Read full text separately from lightweight table rows and traversal pages."""
70
+ session = self.session_registry_controller.session_by_id.get(session_id)
71
+ if session is None:
72
+ return
73
+ if session.mode == "file_history" and (session.search_pending or oid not in session.visible_oid_set):
74
+ return
75
+ identity = RequestIdentity(session_id, session.generation, session.selection_generation, oid)
76
+ window = self.session_registry_controller.window_by_session[session_id]
77
+ window.commit_panel.show_loading(oid, window.commit.short_oid if window.commit is not None and window.commit.oid == oid else "")
78
+ if session.refresh_active and session.view_anchor is not None:
79
+ session.view_anchor = replace(session.view_anchor, selected_oid=oid, selected_visible_index=window.commit_list.currentIndex().row())
80
+ if session.navigation_oid == oid:
81
+ self.repository.read_commit(oid, identity, verify_exists=True)
82
+ else:
83
+ self.repository.read_commit(oid, identity)
84
+
85
+ def file_is_current(self, identity: FileReadIdentity) -> bool:
86
+ """Reject replies whose window, commit, selection, or panel has changed."""
87
+ request = identity.request_identity
88
+ session = self.session_registry_controller.session_by_id.get(request.session_id)
89
+ return (session is not None and not session.is_closed and session.generation == request.generation
90
+ and session.selection_generation == request.selection_generation and session.selected_oid == request.oid
91
+ and session.file_identity_by_kind.get(identity.panel_kind) == identity)
92
+
93
+ def read_diff(self, session_id: str, entry: RevisionEntry) -> None:
94
+ """Start a new comparison only for an entry in the current commit's file list."""
95
+ session = self.session_registry_controller.session_by_id.get(session_id)
96
+ if session is None or entry.commit_oid != session.selected_oid or not any(entry in group.entries for group in session.diff_groups):
97
+ return
98
+ preserve_selection = session.selected_entry == entry
99
+ session.panel_generation += 1
100
+ session.selected_entry = entry
101
+ request = RequestIdentity(session_id, session.generation, session.selection_generation, session.selected_oid)
102
+ panel = self.session_registry_controller.window_by_session[session_id].diff_panel
103
+ identity = FileReadIdentity(request, FilePanel.DIFF, session.panel_generation, entry.parent_oid, entry.old_path, entry.new_path, panel.full_context_checkbox.isChecked())
104
+ session.file_identity_by_kind[FilePanel.DIFF] = identity
105
+ panel.selected_entry = entry
106
+ panel.full_context_checkbox.setEnabled(True)
107
+ panel.diff_view.clear(preserve_selection=preserve_selection)
108
+ panel.reader_toolbar.start_loading("Loading diff…")
109
+ self.repository.read_diff(entry, identity)
110
+
111
+ def change_diff_context(self, session_id: str, checked: bool) -> None:
112
+ """Reload only the selected comparison while keeping graph and commit detail intact."""
113
+ session = self.session_registry_controller.session_by_id.get(session_id)
114
+ if session is not None and session.selected_entry is not None:
115
+ self.read_diff(session_id, session.selected_entry)
116
+
117
+ def diff_read(self, identity: FileReadIdentity, entry: RevisionEntry, patch: bytes) -> None:
118
+ """Publish patch bytes only for the selected entry and panel generation."""
119
+ if not self.file_is_current(identity):
120
+ return
121
+ session_id = identity.request_identity.session_id
122
+ if self.session_registry_controller.session_by_id[session_id].selected_entry != entry:
123
+ return
124
+ panel = self.session_registry_controller.window_by_session[session_id].diff_panel
125
+ try:
126
+ panel.show_patch(entry, patch)
127
+ except ValueError as error:
128
+ panel.show_error(str(error))
129
+
130
+ def retry_diff(self, session_id: str) -> None:
131
+ """Retry failed file or file-list reads without replacing commit details."""
132
+ session = self.session_registry_controller.session_by_id.get(session_id)
133
+ if session is None:
134
+ return
135
+ if session.selected_entry is not None:
136
+ self.read_diff(session_id, session.selected_entry)
137
+ return
138
+ session.invalidate_file_selection()
139
+ if session.selected_oid is not None:
140
+ self.read_selection(session_id, session.selected_oid)
141
+
142
+ def save_reading_preference(self, session_id: str, changed_value_by_key: dict[str, object]) -> None:
143
+ """Publish only synchronized choices and restore the accepted view on refusal."""
144
+ window = self.session_registry_controller.window_by_session.get(session_id)
145
+ if window is None or window.is_closed:
146
+ return
147
+ try:
148
+ self.preference.update(changed_value_by_key)
149
+ except (OSError, ValueError, TypeError) as error:
150
+ window.apply_reading_preference(self.preference.value_by_key)
151
+ diagnostic = Diagnostic("Save reading preferences", window.context.repository_dir, "", [], str(error)).prepare()
152
+ window.statusBar().showMessage(diagnostic.cause)
153
+ if self.diagnostic_log is not None:
154
+ self.diagnostic_log.write(diagnostic)
155
+
156
+ def resume_file_history(self, session_id: str) -> None:
157
+ """Resume only the current limited window's immutable file-history calculation."""
158
+ session = self.session_registry_controller.session_by_id.get(session_id)
159
+ if session is None or session.is_closed or session.file_history_engine is None:
160
+ return
161
+ self.session_registry_controller.window_by_session[session_id].file_history_resume_action.setEnabled(False)
162
+ session.file_history_engine.resume()
163
+
164
+ def change_filter(self, session_id: str, text: str) -> None:
165
+ """Invalidate on every keystroke, before the delayed replacement scan starts."""
166
+ session = self.session_registry_controller.session_by_id.get(session_id)
167
+ if session is None:
168
+ return
169
+ session.search(text)
170
+ self.session_registry_controller.window_by_session[session_id].begin_search()
171
+
172
+ def read_navigation(self, session_id: str, oid: str) -> None:
173
+ """Clear the previous row's active detail while verifying a requested relative."""
174
+ if not self.operation_policy_controller.allows(session_id, WindowCapability.CONSULT):
175
+ return
176
+ window = self.session_registry_controller.window_by_session[session_id]
177
+ session = self.session_registry_controller.session_by_id[session_id]
178
+ if session.mode == "file_history" and (session.search_pending or oid not in session.visible_oid_set):
179
+ return
180
+ if not window.search_pending and oid in window.history_model.row_by_oid and window.commit is not None and window.commit.oid != oid:
181
+ window.reveal_commit(oid)
182
+ return
183
+ window.commit_list.clearSelection()
184
+ self.read_selection(session_id, oid)
185
+
186
+ def _finish_navigation(self, session_id: str, commit: Commit) -> None:
187
+ """Reveal the real row and announce only a filter that hid this target."""
188
+ if not self.operation_policy_controller.allows(session_id, WindowCapability.CONSULT):
189
+ return
190
+ session = self.session_registry_controller.session_by_id[session_id]
191
+ window = self.session_registry_controller.window_by_session[session_id]
192
+ if session.mode == "file_history" and (session.search_pending or commit.oid not in session.visible_oid_set):
193
+ return
194
+ session.navigation_oid = None
195
+ session.navigation_commit = None
196
+ if (session.snapshot is not None and commit.oid not in session.snapshot.parent_oids_by_oid
197
+ and not any(explicit.oid == commit.oid for explicit in session.snapshot.explicit_commits)):
198
+ session.snapshot.explicit_commits.append(summarize_commit(commit))
199
+ window.reveal_commit(commit.oid)
200
+ window.commit = commit
201
+ window.commit_panel.show_commit(commit)
202
+ if session.navigation_filter_cleared:
203
+ window.statusBar().showMessage(f"Filter cleared to show commit {commit.short_oid}.")
204
+ session.navigation_filter_cleared = False
205
+
206
+ def _read_relation(self, session_id: str) -> None:
207
+ """Associate each relative read with the active detail and index completeness."""
208
+ if not self.operation_policy_controller.allows(session_id, WindowCapability.CONSULT):
209
+ return
210
+ session = self.session_registry_controller.session_by_id[session_id]
211
+ commit = self.session_registry_controller.window_by_session[session_id].commit_panel.commit
212
+ if commit is None or commit.oid != session.selected_oid:
213
+ return
214
+ session.relation_generation += 1
215
+ identity = RequestIdentity(session_id, session.generation, session.selection_generation, commit.oid, relation_generation=session.relation_generation)
216
+ session.relation_identity = identity
217
+ self.session_registry_controller.window_by_session[session_id].commit_panel.child_label.setText(SharedText.CHILD_LOADING.value)
218
+ self.repository.read_relation(commit, session.snapshot, identity)
219
+
220
+ def relation_read(self, identity: RequestIdentity, relation: CommitRelation) -> None:
221
+ """Reject late relatives from another selection, capture or index read."""
222
+ session = self.session_registry_controller.session_by_id.get(identity.session_id)
223
+ if (session is None or session.relation_identity != identity or session.generation != identity.generation
224
+ or session.selection_generation != identity.selection_generation or session.selected_oid != relation.oid):
225
+ return
226
+ if session.mode == "file_history":
227
+ parents = []
228
+ child_links = []
229
+ for link in relation.parents:
230
+ parents.append(replace(link, available=link.available and link.oid in session.visible_oid_set))
231
+ for link in relation.child_links:
232
+ child_links.append(replace(link, available=link.available and link.oid in session.visible_oid_set))
233
+ relation = replace(relation, parents=parents, child_links=child_links)
234
+ self.session_registry_controller.window_by_session[identity.session_id].show_commit_relation(relation)
235
+
236
+ def retry_selection(self, session_id: str, oid: str) -> None:
237
+ """Give an explicit retry a fresh identity so another diagnostic can be shown."""
238
+ session = self.session_registry_controller.session_by_id.get(session_id)
239
+ if session is None or session.selected_oid != oid:
240
+ return
241
+ session.selection_generation += 1
242
+ self.read_selection(session_id, oid)
243
+
244
+ def read_visible_tree(self, session_id: str) -> None:
245
+ """Read the historical tree of the selected commit once the File tree tab is on screen.
246
+
247
+ The tree of a commit nobody looks at is never read. The read belongs to the current selection and panel
248
+ generation, so the answer for a commit the user has left is dropped, and showing the tab again for the same
249
+ selection, an empty tree or a failed read included, asks for nothing more; Retry is the way to read again.
250
+ An answer that arrived while the tab was hidden is built now from the entries already received, without a new
251
+ Git read, provided it still belongs to the current selection.
252
+ """
253
+ session = self.session_registry_controller.session_by_id.get(session_id)
254
+ window = self.session_registry_controller.window_by_session.get(session_id)
255
+ if session is None or session.is_closed or window is None or not isValid(window):
256
+ return
257
+ if window.detail_tabs.currentWidget() is not window.file_tree_panel:
258
+ return
259
+ # Only the commit read for the current selection and generation can own the read: a commit still shown from
260
+ # before a refresh or a new selection waits for its own detail read, which asks again.
261
+ request = session.commit_identity
262
+ commit = window.commit
263
+ if (request is None or commit is None or commit.detail_error or request.oid != commit.oid or request.oid != session.selected_oid
264
+ or request.generation != session.generation or request.selection_generation != session.selection_generation):
265
+ return
266
+ pending = session.pending_tree_identity
267
+ session.pending_tree_identity = None
268
+ if pending is not None and self.file_is_current(pending):
269
+ window.file_tree_panel.show_tree(session.tree_entries)
270
+ return
271
+ if session.file_identity_by_kind.get(FilePanel.TREE) is not None:
272
+ return
273
+ tree_identity = FileReadIdentity(request, FilePanel.TREE, session.panel_generation)
274
+ session.file_identity_by_kind[FilePanel.TREE] = tree_identity
275
+ self.repository.read_tree(commit.oid, tree_identity)
276
+
277
+ def tree_read(self, identity: FileReadIdentity, entries: list[TreeEntry]) -> None:
278
+ """Present paths only for the active commit and repository window, and only while the File tree tab is shown.
279
+
280
+ The entries are kept in every case because choosing a file of the tree relies on them. Building thousands of
281
+ tree rows for a tab the user already left would only block the window, so a hidden tab keeps the answer as
282
+ pending and the tree is built when the tab comes back.
283
+ """
284
+ if not self.file_is_current(identity):
285
+ return
286
+ session_id = identity.request_identity.session_id
287
+ session = self.session_registry_controller.session_by_id[session_id]
288
+ session.tree_entries = entries
289
+ window = self.session_registry_controller.window_by_session[session_id]
290
+ if window.detail_tabs.currentWidget() is not window.file_tree_panel:
291
+ session.pending_tree_identity = identity
292
+ return
293
+ window.file_tree_panel.show_tree(entries)
294
+
295
+ def read_blob(self, session_id: str, entry: TreeEntry) -> None:
296
+ """Resolve the selected tree object independently of the diff selection."""
297
+ session = self.session_registry_controller.session_by_id.get(session_id)
298
+ if session is None or entry not in session.tree_entries:
299
+ return
300
+ session.panel_generation += 1
301
+ session.selected_tree_entry = entry
302
+ request = RequestIdentity(session_id, session.generation, session.selection_generation, session.selected_oid)
303
+ identity = FileReadIdentity(request, FilePanel.BLOB, session.panel_generation, new_path=entry.path)
304
+ session.file_identity_by_kind[FilePanel.BLOB] = identity
305
+ panel = self.session_registry_controller.window_by_session[session_id].file_tree_panel
306
+ panel.selected_entry = entry
307
+ panel.begin_loading()
308
+ self.repository.cancel_file(session_id, FilePanel.BLOB)
309
+ if entry.mode == "160000":
310
+ panel.show_submodule(entry.oid)
311
+ return
312
+ self.repository.read_blob(entry.oid, identity)
313
+
314
+ def blob_read(self, identity: FileReadIdentity, content: BlobContent) -> None:
315
+ """Ignore late file bytes even when they belong to another file in the same tree."""
316
+ if not self.file_is_current(identity):
317
+ return
318
+ session_id = identity.request_identity.session_id
319
+ entry = self.session_registry_controller.session_by_id[session_id].selected_tree_entry
320
+ if entry is None or entry.oid != content.oid:
321
+ return
322
+ self.session_registry_controller.window_by_session[session_id].file_tree_panel.show_blob(content)
323
+
324
+ def changes_read(self, identity: FileReadIdentity, groups: list[ComparisonGroup]) -> None:
325
+ """Publish file groups only for the active comparison request."""
326
+ if not self.file_is_current(identity):
327
+ return
328
+ session = self.session_registry_controller.session_by_id[identity.request_identity.session_id]
329
+ session.diff_groups = groups
330
+ window = self.session_registry_controller.window_by_session[session.session_id]
331
+ panel = window.diff_panel
332
+ selected_entry = panel.selected_entry
333
+ panel.show_changes(groups)
334
+ if selected_entry not in panel.item_by_entry and session.file_history_result is not None and window.commit is not None:
335
+ selected_entry = session.file_history_result.select_entry(window.commit, groups)
336
+ if selected_entry not in panel.item_by_entry:
337
+ selected_entry = panel.first_entry()
338
+ if selected_entry in panel.item_by_entry:
339
+ panel.file_tree.setCurrentItem(panel.item_by_entry[selected_entry])
340
+
341
+ def retry_file_tree(self, session_id: str, kind: FilePanel | None) -> None:
342
+ """Retry only the failed tree or blob with fresh ownership in either window mode; any other panel, or none, is not retried here."""
343
+ session = self.session_registry_controller.session_by_id.get(session_id)
344
+ if session is None or session.is_closed or session.selected_oid is None:
345
+ return
346
+ if kind == FilePanel.BLOB and session.selected_tree_entry is not None:
347
+ self.read_blob(session_id, session.selected_tree_entry)
348
+ return
349
+ if kind != FilePanel.TREE:
350
+ return
351
+ session.panel_generation += 1
352
+ session.pending_tree_identity = None
353
+ request = RequestIdentity(session_id, session.generation, session.selection_generation, session.selected_oid)
354
+ identity = FileReadIdentity(request, FilePanel.TREE, session.panel_generation)
355
+ session.file_identity_by_kind[FilePanel.TREE] = identity
356
+ self.session_registry_controller.window_by_session[session_id].file_tree_panel.show_error("Loading files…")
357
+ self.repository.read_tree(session.selected_oid, identity)
358
+
359
+ def file_failed(self, identity: FileReadIdentity, failure: RepositoryFailure) -> None:
360
+ """Present a file-specific diagnostic without marking commit metadata unavailable."""
361
+ if identity in self.session_registry_controller.export_owner_by_identity:
362
+ self.system_tool.fail_export(identity, failure.cause)
363
+ return
364
+ if not self.file_is_current(identity):
365
+ return
366
+ diagnostic = Diagnostic(failure.operation, failure.repository_dir, failure.program_filename, failure.arguments, failure.cause, failure.stderr, failure.return_code).prepare()
367
+ window = self.session_registry_controller.window_by_session[identity.request_identity.session_id]
368
+ if identity.panel_kind == FilePanel.DIFF:
369
+ window.diff_panel.show_error(diagnostic.detail)
370
+ else:
371
+ window.file_tree_panel.show_error(diagnostic.detail, retry_kind=identity.panel_kind)
372
+ if self.diagnostic_log is not None:
373
+ self.diagnostic_log.write(diagnostic)
374
+
375
+ def snapshot_read(self, identity: RequestIdentity, snapshot: HistorySnapshot) -> None:
376
+ """Route one capture through its owning session to the native model."""
377
+ if identity.session_id in self.session_registry_controller.navigation_owner_by_id:
378
+ return
379
+ if self.session_registry_controller.identity_by_session.get(identity.session_id) != identity:
380
+ return
381
+ session = self.session_registry_controller.session_by_id[identity.session_id]
382
+ if session.file_history_engine is not None and session.snapshot is not snapshot:
383
+ return
384
+ session.snapshot = snapshot
385
+ if session.file_history_anchor is not None:
386
+ if snapshot.complete and session.file_history_engine is None:
387
+ self._calculate_file_history(session)
388
+ return
389
+ if session.search_pending or session.filter_text or session.refresh_active:
390
+ if snapshot.complete and session.search_identity is None:
391
+ self.start_search(identity.session_id)
392
+ return
393
+ self.session_registry_controller.window_by_session[identity.session_id].show_snapshot(snapshot)
394
+ if snapshot.complete:
395
+ self._read_relation(identity.session_id)
396
+
397
+ def _calculate_file_history(self, session: RepositorySession) -> None:
398
+ """Publish a captured file membership only to the still-open owning session."""
399
+ session.file_history_generation += 1
400
+ generation = session.file_history_generation
401
+ engine = FileHistoryEngine(self.repository)
402
+ session.file_history_engine = engine
403
+
404
+ def receive(result: FileHistoryResult) -> None:
405
+ """Start message filtering only after the file membership is known."""
406
+ if session.is_closed or session.file_history_generation != generation:
407
+ return
408
+ session.file_history_result = result
409
+ self.session_registry_controller.window_by_session[session.session_id].file_history_result = result
410
+ session.search(session.filter_text)
411
+ self.session_registry_controller.window_by_session[session.session_id].begin_search()
412
+ self.start_search(session.session_id)
413
+
414
+ def progress(count: int, total: int) -> None:
415
+ """Show progress without publishing an unfinished file membership."""
416
+ if not session.is_closed and session.file_history_generation == generation:
417
+ self.session_registry_controller.window_by_session[session.session_id].statusBar().showMessage(f"Reading file history… {count}/{total} commits")
418
+
419
+ engine.finished.connect(receive)
420
+ engine.progressed.connect(progress)
421
+ engine.calculate(session.snapshot, session.file_history_anchor, self.session_registry_controller.identity_by_session[session.session_id])
422
+
423
+ def commit_batch_read(self, identity: RequestIdentity, commits: list[Commit]) -> None:
424
+ """Keep the rows the history read just parsed, and append them, in Git's order, to the live generation of the whole history.
425
+
426
+ The rows are pushed as the history is read, and kept with their history so that an empty query can show them
427
+ again without reading Git. A file history, a search, a filter or a refresh shows its own rows instead, so it
428
+ does not append these; neither does a walk into another repository, whose window is not replaced yet.
429
+ """
430
+ if identity.session_id in self.session_registry_controller.navigation_owner_by_id:
431
+ return
432
+ snapshot = self.repository.snapshot_by_session.get(identity.session_id)
433
+ if self.session_registry_controller.identity_by_session.get(identity.session_id) != identity:
434
+ return
435
+ session = self.session_registry_controller.session_by_id[identity.session_id]
436
+ if snapshot is not None:
437
+ session.streamed_history = keep_streamed_commits(session.streamed_history, snapshot, commits)
438
+ if session.mode == "file_history" or session.snapshot is None:
439
+ return
440
+ if session.search_pending or session.filter_text or session.refresh_active:
441
+ return
442
+ self.session_registry_controller.window_by_session[identity.session_id].append_history(commits)
443
+
444
+ def start_search(self, session_id: str) -> None:
445
+ """Start one scan only once the captured ancestry is complete.
446
+
447
+ An empty query answers on a later turn with the rows already read; that answer is dropped if the window's history
448
+ list is destroyed first.
449
+ """
450
+ session = self.session_registry_controller.session_by_id.get(session_id)
451
+ if session is None or session.snapshot is None or not session.snapshot.complete or session.search_identity is not None:
452
+ return
453
+ if self.session_registry_controller.window_by_session[session_id].search_timer.isActive():
454
+ return
455
+ if session.mode == "file_history" and session.file_history_result is None:
456
+ return
457
+ if not session.search_pending:
458
+ session.search(session.filter_text)
459
+ identity = RequestIdentity(session_id, session.generation, search_generation=session.search_generation)
460
+ session.search_identity = identity
461
+ # An empty query shows every commit: the rows the history read already delivered are the answer. They go
462
+ # through the same path as a search's answer, on a later turn of the event loop as a search's would.
463
+ streamed_history = session.streamed_history
464
+ commits = streamed_history.complete_commits(session.snapshot) if streamed_history is not None and not session.filter_text else None
465
+ if commits is not None:
466
+ # Tied to the history list: a window closed before the next turn takes the answer with it, instead of
467
+ # publishing into a list that no longer exists.
468
+ QTimer.singleShot(0, self.session_registry_controller.window_by_session[session_id].commit_list, lambda: self.matching_read(identity, commits, len(commits), True))
469
+ return
470
+ window = self.session_registry_controller.window_by_session[session_id]
471
+ self.repository.read_filter_matches(session.snapshot, session.filter_text, window.filter_mode, identity)
472
+
473
+ def matching_read(self, identity: RequestIdentity, commits: list[Commit], scanned_count: int, complete: bool) -> None:
474
+ """Publish a coherent graph only after the current query has checked every OID."""
475
+ if identity.session_id in self.session_registry_controller.navigation_owner_by_id:
476
+ return
477
+ session = self.session_registry_controller.session_by_id.get(identity.session_id)
478
+ if session is None or session.search_identity != identity or not session.search_pending:
479
+ return
480
+ for commit in commits:
481
+ if session.file_history_result is None or commit.oid in session.file_history_result.touching_oid_set:
482
+ session.matching_commits.append(commit)
483
+ window = self.session_registry_controller.window_by_session[identity.session_id]
484
+ if not complete:
485
+ if session.refresh_active:
486
+ window.statusBar().showMessage(f"Refreshing… {scanned_count} commits scanned")
487
+ else:
488
+ window.show_search_progress(len(session.matching_commits), scanned_count)
489
+ return
490
+ session.search_pending = False
491
+ refresh_anchor = session.view_anchor
492
+ navigation_commit = session.navigation_commit
493
+ if (session.mode != "file_history" and navigation_commit is not None and navigation_commit.oid == session.selected_oid
494
+ and not any(commit.oid == navigation_commit.oid for commit in session.matching_commits)):
495
+ session.matching_commits.append(summarize_commit(navigation_commit))
496
+ session.visible_oid_set = set()
497
+ for commit in session.matching_commits:
498
+ session.visible_oid_set.add(commit.oid)
499
+ parent_map = visible_parent_map(session.snapshot, session.visible_oid_set) if session.mode == "file_history" else None
500
+ missing_checkout_selection = (identity.session_id in self.refresh_controller.checkout_refresh_session_set and session.selected_oid is not None
501
+ and session.selected_oid not in session.visible_oid_set)
502
+ if missing_checkout_selection:
503
+ with QSignalBlocker(window):
504
+ window.show_matches(session.snapshot, session.matching_commits, session.filter_text, session.selected_oid, parent_map)
505
+ if session.refresh_active and refresh_anchor is not None:
506
+ window.restore_anchor(refresh_anchor)
507
+ window.clear_commit_selection()
508
+ session.selected_oid = None
509
+ session.selection_generation += 1
510
+ session.invalidate_file_selection()
511
+ session.navigation_commit = None
512
+ navigation_commit = None
513
+ else:
514
+ window.show_matches(session.snapshot, session.matching_commits, session.filter_text, session.selected_oid, parent_map)
515
+ if session.refresh_active and refresh_anchor is not None:
516
+ window.restore_anchor(refresh_anchor)
517
+ self.refresh_controller.checkout_refresh_session_set.discard(identity.session_id)
518
+ # Only the last read of a refresh moves the view: a read queued behind this one keeps the position it starts from.
519
+ if session.refresh_active and session.refresh_queued is None and identity.session_id in self.history_top_session_set:
520
+ self.history_top_session_set.discard(identity.session_id)
521
+ window.scroll_history_to_top()
522
+ self.history_published.emit(identity.session_id, identity.generation)
523
+ session.matching_commits = []
524
+ if navigation_commit is not None and navigation_commit.oid == session.selected_oid:
525
+ self._finish_navigation(identity.session_id, navigation_commit)
526
+ if not window.history_model.commits:
527
+ session.selected_oid = None
528
+ session.selection_generation += 1
529
+ session.invalidate_file_selection()
530
+ window.diff_panel.clear_selection()
531
+ window.file_tree_panel.clear_selection()
532
+ if session.refresh_active and session.refresh_queued is None:
533
+ # The history just published wrote its own status line: a line owed about an ended rebase is added to it,
534
+ # naming the HEAD this very refresh read.
535
+ self.history_status_written.emit(identity.session_id, self.reference_read_controller.reference_snapshot_by_session.get(identity.session_id))
536
+ if session.refresh_active:
537
+ session.refresh_active = False
538
+ session.view_anchor = None
539
+ self.refresh_controller.recovery_request_set.discard(self.session_registry_controller.identity_by_session[identity.session_id])
540
+ self.refresh_controller.leave_full_refresh_read(identity.session_id)
541
+ if session.refresh_queued is not None:
542
+ request = session.refresh_queued
543
+ session.refresh_queued = None
544
+ self.refresh_controller.submit_refresh_request(identity.session_id, request)
545
+
546
+ def commit_read(self, identity: RequestIdentity, commit: Commit) -> None:
547
+ """Discard superseded detail responses without disturbing ongoing history reads."""
548
+ session = self.session_registry_controller.session_by_id.get(identity.session_id)
549
+ if session is None or session.generation != identity.generation or session.selection_generation != identity.selection_generation or session.selected_oid != commit.oid or identity.oid != commit.oid:
550
+ return
551
+ window = self.session_registry_controller.window_by_session[identity.session_id]
552
+ window.commit = commit
553
+ session.commit_identity = identity
554
+ window.commit_panel.show_commit(commit)
555
+ if not commit.detail_error:
556
+ file_identity = FileReadIdentity(identity, panel_generation=session.panel_generation)
557
+ if session.file_identity_by_kind.get(FilePanel.DIFF) is None:
558
+ session.file_identity_by_kind[FilePanel.DIFF] = file_identity
559
+ self.repository.read_changes(commit, file_identity)
560
+ self.read_visible_tree(identity.session_id)
561
+ if session.navigation_oid == commit.oid and not commit.detail_error:
562
+ if commit.oid in window.history_model.row_by_oid and not window.search_pending:
563
+ self._finish_navigation(identity.session_id, commit)
564
+ elif session.mode == "file_history":
565
+ session.navigation_commit = commit
566
+ else:
567
+ session.navigation_commit = commit
568
+ # The files a commit changes come from another read than this one, so in the file name mode the filter is
569
+ # cleared to show the commit: a commit the finished search left out is one the filter hides.
570
+ hidden_by_filter = window.filter_mode == HistoryFilterMode.FILES or not TextQuery(session.filter_text).matches(commit.message)
571
+ if session.filter_text and hidden_by_filter:
572
+ session.navigation_filter_cleared = True
573
+ window.clear_filter()
574
+ else:
575
+ session.search(session.filter_text)
576
+ window.begin_search()
577
+ self.start_search(identity.session_id)
578
+ self._read_relation(identity.session_id)
579
+
580
+ def failed(self, identity: RequestIdentity, failure: RepositoryFailure) -> None:
581
+ """Present one prepared diagnostic while allowing a failed opening to retry."""
582
+ if identity.session_id in self.session_registry_controller.navigation_owner_by_id:
583
+ return
584
+ session = self.session_registry_controller.session_by_id.get(identity.session_id)
585
+ selected_request = session is not None and identity.oid is not None and session.generation == identity.generation and session.selection_generation == identity.selection_generation and session.selected_oid == identity.oid
586
+ relation_request = selected_request and bool(identity.relation_generation)
587
+ if relation_request and session.relation_identity != identity:
588
+ return
589
+ search_request = session is not None and session.search_identity == identity and session.search_pending
590
+ if self.session_registry_controller.identity_by_session.get(identity.session_id) != identity and not selected_request and not search_request:
591
+ return
592
+ if identity in self.reported_request_set:
593
+ return
594
+ self.reported_request_set.add(identity)
595
+ diagnostic = Diagnostic(failure.operation, failure.repository_dir, failure.program_filename, failure.arguments, failure.cause, failure.stderr, failure.return_code).prepare()
596
+ clone_dialog = self.session_registry_controller.clone_dialog_by_session.get(identity.session_id)
597
+ if clone_dialog is not None and clone_dialog.operation_id is not None and identity.session_id not in self.session_registry_controller.window_by_session:
598
+ run = self.clone.run_by_id[clone_dialog.operation_id]
599
+ if run.clone_status == OperationStatus.SUCCESS.value or run.incomplete_repository:
600
+ self.session_registry_controller.identity_by_session.pop(identity.session_id, None)
601
+ self.session_registry_controller.welcome_by_session[identity.session_id].set_loading(False)
602
+ self.clone.open_result(run.operation_id, False, diagnostic)
603
+ clone_dialog.show_failure(failure_summary(diagnostic), partial=True)
604
+ run.trace.append_diagnostic(run.operation_id, "error", diagnostic, OperationStatus.FAILED)
605
+ return
606
+ if relation_request:
607
+ self.session_registry_controller.window_by_session[identity.session_id].commit_panel.child_label.setText("Children unknown — incomplete read: " + diagnostic.cause)
608
+ elif selected_request:
609
+ self.session_registry_controller.window_by_session[identity.session_id].commit_panel.show_error(identity.oid, diagnostic.cause)
610
+ parent = self.session_registry_controller.welcome_by_session.get(identity.session_id) or self.session_registry_controller.window_by_session.get(identity.session_id)
611
+ if parent is None:
612
+ return
613
+ if isinstance(parent, WelcomeWindow):
614
+ parent.set_loading(False)
615
+ parent.mark_unavailable(self.session_registry_controller.opening_dir_by_session.get(identity.session_id, failure.repository_dir))
616
+ self.session_registry_controller.identity_by_session.pop(identity.session_id, None)
617
+ else:
618
+ session = self.session_registry_controller.session_by_id[identity.session_id]
619
+ if session.refresh_active and not selected_request:
620
+ self.refresh_failed.emit(identity.session_id, diagnostic)
621
+ session.refresh_active = False
622
+ session.refresh_queued = None
623
+ self.refresh_controller.leave_full_refresh_read(identity.session_id)
624
+ session.search_pending = False
625
+ session.search_identity = None
626
+ parent.search_pending = False
627
+ parent.commit_list.setEnabled(True)
628
+ parent.history_state_label.hide()
629
+ parent.statusBar().showMessage("State not refreshed — references not refreshed")
630
+ elif search_request:
631
+ session.search_pending = False
632
+ session.search_identity = None
633
+ parent.statusBar().showMessage("State not refreshed — search incomplete")
634
+ elif not selected_request and session.snapshot is not None and session.snapshot.generation == identity.generation:
635
+ session.snapshot.error = diagnostic.cause
636
+ parent.update_history_status()
637
+ else:
638
+ parent.statusBar().showMessage("State not refreshed — references not refreshed")
639
+ if self.diagnostic_log is not None:
640
+ self.diagnostic_log.write(diagnostic)
641
+ if self.diagnostic_log.warning:
642
+ if isinstance(parent, WelcomeWindow):
643
+ parent.show_status(self.diagnostic_log.warning)
644
+ else:
645
+ parent.statusBar().showMessage(self.diagnostic_log.warning)
646
+ if identity in self.refresh_controller.recovery_request_set:
647
+ self.refresh_controller.recovery_request_set.discard(identity)
648
+ return
649
+ dialog = ErrorDialog(diagnostic, parent, self.diagnostic_log.log_filename if self.diagnostic_log is not None else None)
650
+ if isinstance(parent, WelcomeWindow):
651
+ dialog.setWindowTitle("Could not open repository")
652
+ opening_diagnostic = Diagnostic(failure.operation, failure.repository_dir, failure.program_filename, [],
653
+ f"Cannot open {failure.repository_dir}: {failure.cause}. Check the path and access permissions, then try again.", failure.stderr).prepare()
654
+ dialog.cause_label.setText(opening_diagnostic.cause)
655
+ self.session_registry_controller.dialogs.append(dialog)
656
+
657
+ def finish() -> None:
658
+ """Attempt one recovery after dismissal, provided the original view still exists."""
659
+ if dialog in self.session_registry_controller.dialogs:
660
+ self.session_registry_controller.dialogs.remove(dialog)
661
+ navigation_failed = (selected_request and not relation_request and session is not None and session.navigation_oid == identity.oid
662
+ and session.selection_generation == identity.selection_generation)
663
+ if navigation_failed:
664
+ session.navigation_oid = None
665
+ session.navigation_commit = None
666
+ if (self.session_registry_controller.identity_by_session.get(identity.session_id) == identity or navigation_failed) and identity.session_id in self.session_registry_controller.window_by_session:
667
+ self.refresh_controller.refresh(identity.session_id, recovery=True)
668
+ dialog.deleteLater()
669
+
670
+ dialog.finished.connect(finish)
671
+ open_window_modal(dialog)