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,658 @@
1
+ """Move a repository window to another repository, a submodule or its parent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import uuid
7
+ from dataclasses import dataclass, replace
8
+ from typing import TYPE_CHECKING
9
+
10
+ from PySide6.QtCore import QObject, QSignalBlocker, Qt, QTimer, Signal
11
+ from PySide6.QtWidgets import QDialog, QMessageBox
12
+ from shiboken6 import isValid
13
+
14
+ from gitpane.account.authentication import transfer_account
15
+ from gitpane.config import ConsoleOperation
16
+ from gitpane.controller.session_registry import NavigationKind, RepositorySession, keep_streamed_commits
17
+ from gitpane.history.file_history import WindowCapability
18
+ from gitpane.history.history_filter import HISTORY_FILTER_INITIAL_MODE
19
+ from gitpane.history.repository_value import RequestIdentity
20
+ from gitpane.operation.diagnostic import Diagnostic
21
+ from gitpane.operation.operation_status import OperationStatus
22
+ from gitpane.preference.preference_store import saved_recent_identity
23
+ from gitpane.submodule.submodule_discovery import OPEN_AGAIN_RETRY_SENTENCE, declared_directory, initialization_command
24
+ from gitpane.system.path_identity import canonical_path
25
+ from gitpane.transport.git_refusal import refused_https_url
26
+ from gitpane.transport.refusal_notice import SUBMODULE_NOTICE_SUBJECT_TEXT, account_notice
27
+ from gitpane.ui.console.error_dialog import ErrorDialog
28
+ from gitpane.ui.console.operation_dialog import OperationDialog
29
+ from gitpane.ui.welcome.invalid_repository_prompt import InvalidRepositoryPrompt
30
+ from gitpane.ui.window_control.dialog_modality import open_window_modal, prepare_message_box
31
+ from gitpane.worktree.worktree_snapshot import WorktreeKey
32
+
33
+ if TYPE_CHECKING:
34
+ from gitpane.account.authentication import AuthenticationService
35
+ from gitpane.controller.account_controller import AccountController
36
+ from gitpane.controller.close_readiness import CloseReadiness
37
+ from gitpane.controller.history_read_controller import HistoryReadController
38
+ from gitpane.controller.opening_controller import OpeningController
39
+ from gitpane.controller.operation_policy import OperationPolicy
40
+ from gitpane.controller.rebase_controller import RebaseController
41
+ from gitpane.controller.reference_read_controller import ReferenceReadController
42
+ from gitpane.controller.refresh_controller import RefreshController
43
+ from gitpane.controller.replay_controller import ReplayController
44
+ from gitpane.controller.session_registry import SessionRegistry, StreamedHistory
45
+ from gitpane.controller.staging_session_registry import StagingSessionRegistry
46
+ from gitpane.controller.transport_controller import TransportController
47
+ from gitpane.history.repository import GitRepository
48
+ from gitpane.history.repository_value import Commit, HistorySnapshot, RepositoryContext, RepositoryFailure
49
+ from gitpane.operation.diagnostic import DiagnosticLog
50
+ from gitpane.preference.preference_store import PreferenceStore
51
+ from gitpane.reference.reference_edit import ReferenceService
52
+ from gitpane.reference.reference_snapshot import ReferenceSnapshot
53
+ from gitpane.submodule.submodule_discovery import SubmoduleEntry, SubmoduleInitRun, SubmoduleService, SubmoduleSnapshot
54
+ from gitpane.ui.repository.repository_window import ViewAnchor
55
+
56
+
57
+ @dataclass
58
+ class NavigationFamily:
59
+ """The family one walk into another repository read, bound to the session, identity and family read it started from.
60
+
61
+ It only spares the new window a second read of the same family for display. It is never a proof for a write:
62
+ every child write reads that child again.
63
+ """
64
+
65
+ session: RepositorySession
66
+ identity: RequestIdentity
67
+ generation: int
68
+ family: SubmoduleSnapshot | None = None
69
+
70
+
71
+ SWITCH_OPENING_STATUS = "Opening {directory}…"
72
+ SWITCH_PENDING_STATUS = "Already opening {directory}. Choose again once it is open."
73
+ SWITCH_WAITING_STATUS = "{directory} opens here once Git has finished in the current repository."
74
+ SWITCH_MISSING_STATUS = "The folder {directory} no longer exists. The current repository is still open."
75
+ FORGET_OPERATION = "Save recent repositories"
76
+ SWITCH_REFUSED_TITLE = "Change repository"
77
+ SWITCH_REFUSED_TEXT = "The window \"{title}\" is still open on this repository. Close it, then choose the repository again."
78
+ # Names a dialog of the window that has no title of its own when the change of repository is refused because of it.
79
+ UNTITLED_RELATED_WINDOW_TITLE = "Untitled dialog"
80
+
81
+
82
+ class NavigationController(QObject):
83
+ """The changes of repository of every repository window: a walk into a submodule, a repository chosen in the switch menu, the reads they own until the window shows the new repository."""
84
+
85
+ # Asks for the closing of the session a published change of repository leaves behind, with the id of the window that
86
+ # keeps going on the new repository. Closing a session belongs to the shutdown, which sits above this controller.
87
+ repository_close_requested = Signal(str)
88
+
89
+ def __init__(self, *, session_registry_controller: SessionRegistry, operation_policy_controller: OperationPolicy, staging_session_registry_controller: StagingSessionRegistry, reference_read_controller: ReferenceReadController,
90
+ refresh_controller: RefreshController, history_read_controller: HistoryReadController, account_controller: AccountController, opening_controller: OpeningController, transport_controller: TransportController,
91
+ replay_controller: ReplayController, rebase_controller: RebaseController, close_readiness_controller: CloseReadiness, preference: PreferenceStore, repository: GitRepository, reference: ReferenceService,
92
+ authentication: AuthenticationService, diagnostic_log: DiagnosticLog | None, submodule: SubmoduleService, parent: QObject | None = None) -> None:
93
+ """Receive the services and the lower controllers a change of repository works with; the wiring builds it after the close readiness, and closes the session a published change leaves behind when this controller asks it through a signal."""
94
+ super().__init__(parent)
95
+ self.session_registry_controller = session_registry_controller
96
+ self.operation_policy_controller = operation_policy_controller
97
+ self.staging_session_registry_controller = staging_session_registry_controller
98
+ self.reference_read_controller = reference_read_controller
99
+ self.refresh_controller = refresh_controller
100
+ self.history_read_controller = history_read_controller
101
+ self.account_controller = account_controller
102
+ self.opening_controller = opening_controller
103
+ self.transport_controller = transport_controller
104
+ self.replay_controller = replay_controller
105
+ self.rebase_controller = rebase_controller
106
+ self.close_readiness_controller = close_readiness_controller
107
+ self.preference = preference
108
+ self.repository = repository
109
+ self.reference = reference
110
+ self.authentication = authentication
111
+ self.diagnostic_log = diagnostic_log
112
+ self.submodule = submodule
113
+ # The prompt offering to remove a recent repository whose folder is gone, at most one per window.
114
+ self.invalid_repository_prompt_by_session: dict[str, InvalidRepositoryPrompt] = {}
115
+ self.submodule_output_by_id: dict[str, OperationDialog] = {}
116
+ self.submodule_run_by_navigation: dict[str, SubmoduleInitRun] = {}
117
+ self.navigation_target_by_session: dict[str, str] = {}
118
+ self.navigation_context_by_id: dict[str, tuple[RepositoryContext, Commit | None]] = {}
119
+ self.navigation_snapshot_by_id: dict[str, HistorySnapshot] = {}
120
+ self.navigation_commits_by_id: dict[str, list[Commit]] = {}
121
+ self.navigation_streamed_history_by_id: dict[str, StreamedHistory] = {}
122
+ self.navigation_family_by_id: dict[str, NavigationFamily] = {}
123
+ self.navigation_kind_by_id: dict[str, NavigationKind] = {}
124
+ self.anchor_by_window: dict[str, dict[WorktreeKey, ViewAnchor]] = {}
125
+ self.navigation_resume_by_target: dict[str, HistorySnapshot] = {}
126
+
127
+ def navigation_opened(self, identity: RequestIdentity, context: RepositoryContext, commit: Commit | None) -> None:
128
+ """Read the references of a repository a navigation opened, then its history, while that navigation still owns it.
129
+
130
+ Connected before the opening's own slot, which ignores the identities a navigation owns, so the two never act on
131
+ the same read. A repository other than the one the navigation asked for ends the navigation.
132
+ """
133
+ owner_id = self.session_registry_controller.navigation_owner_by_id.get(identity.session_id)
134
+ if owner_id is None:
135
+ return
136
+ target_dir = self.navigation_target_by_session.get(owner_id)
137
+ if target_dir is None or canonical_path(context.repository_dir) != canonical_path(target_dir):
138
+ kind = self.navigation_kind_by_id.get(identity.session_id, NavigationKind.SUBMODULE)
139
+ self._navigation_failed(identity.session_id, kind.target_mismatch_text)
140
+ return
141
+ self.navigation_context_by_id[identity.session_id] = (context, commit)
142
+ target_id = identity.session_id
143
+
144
+ def target_references_read(snapshot: ReferenceSnapshot | None, error: Exception | None) -> None:
145
+ """Build the target's history from its own capture, only while this very walk still owns the target."""
146
+ if self.session_registry_controller.navigation_owner_by_id.get(target_id) != owner_id or self.repository.identity_by_session.get(target_id) != identity:
147
+ return
148
+ self.repository.read_snapshot(identity, reference_snapshot=snapshot if error is None else None)
149
+
150
+ self.reference.read(context, self.repository.git_filename, identity.generation, target_references_read)
151
+
152
+ def navigation_snapshot_read(self, identity: RequestIdentity, snapshot: HistorySnapshot) -> None:
153
+ """Keep the capture of a repository a navigation reads, then find the commits its window will show.
154
+
155
+ Connected before the history read's own slot, which ignores the identities a navigation owns: the two never act
156
+ on the same capture.
157
+ """
158
+ if identity.session_id not in self.session_registry_controller.navigation_owner_by_id:
159
+ return
160
+ if snapshot.error:
161
+ self._navigation_failed(identity.session_id, snapshot.error)
162
+ elif snapshot.complete and identity.session_id not in self.navigation_snapshot_by_id:
163
+ self.navigation_snapshot_by_id[identity.session_id] = snapshot
164
+ self.navigation_commits_by_id[identity.session_id] = []
165
+ owner_id = self.session_registry_controller.navigation_owner_by_id[identity.session_id]
166
+ context, _commit = self.navigation_context_by_id[identity.session_id]
167
+ # A repository chosen in the switch menu opens like a new window: no remembered filter applies to it.
168
+ anchor = None
169
+ if self.navigation_kind_by_id.get(identity.session_id) is NavigationKind.SUBMODULE:
170
+ anchor = self.anchor_by_window.get(owner_id, {}).get(WorktreeKey.from_context(context))
171
+ filter_text = anchor.filter_text if anchor else ""
172
+ filter_mode = anchor.filter_mode if anchor else HISTORY_FILTER_INITIAL_MODE
173
+ streamed_history = self.navigation_streamed_history_by_id.get(identity.session_id)
174
+ commits = streamed_history.complete_commits(snapshot) if streamed_history is not None and not filter_text else None
175
+ if commits is not None:
176
+ # Tied to the owner's history list: a window closed before the next turn takes the answer with it,
177
+ # instead of publishing into a list that no longer exists.
178
+ QTimer.singleShot(0, self.session_registry_controller.window_by_session[owner_id].commit_list, lambda: self.navigation_matching_read(identity, commits, len(commits), True))
179
+ return
180
+ self.repository.read_filter_matches(snapshot, filter_text, filter_mode, identity)
181
+
182
+ def navigate_repository(self, session_id: str, repository_dir: str) -> None:
183
+ """Revalidate a declared target and explain refusals while retaining the current view."""
184
+ if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE) or session_id in self.navigation_target_by_session:
185
+ return
186
+ session = self.session_registry_controller.session_by_id[session_id]
187
+ if canonical_path(repository_dir) == canonical_path(session.repository_context.repository_dir):
188
+ return
189
+ snapshot = self.refresh_controller.submodule_snapshot_by_session.get(session_id)
190
+ if snapshot is None or not any(entry.repository_dir and canonical_path(entry.repository_dir) == canonical_path(repository_dir) for entry in snapshot.entries):
191
+ return
192
+ showing_window = self.opening_controller.window_showing_folder(repository_dir, other_than=session_id)
193
+ if showing_window is not None:
194
+ self.opening_controller.bring_forward(showing_window)
195
+ return
196
+ if not self.close_readiness_controller.navigation_ready(session_id):
197
+ self.session_registry_controller.navigation_request_by_session[session_id] = (session, repository_dir, NavigationKind.SUBMODULE)
198
+ self.close_readiness_controller.show_close_wait(session_id, navigation=True)
199
+ self.close_readiness_controller.close_timer.start()
200
+ return
201
+ self.anchor_by_window.setdefault(session_id, {})[WorktreeKey.from_context(session.repository_context)] = self.session_registry_controller.window_by_session[session_id].capture_anchor()
202
+ target_id = uuid.uuid4().hex
203
+ self.session_registry_controller.navigation_owner_by_id[target_id] = session_id
204
+ self.navigation_target_by_session[session_id] = repository_dir
205
+ self.navigation_kind_by_id[target_id] = NavigationKind.SUBMODULE
206
+ observation = NavigationFamily(session, self.session_registry_controller.identity_by_session[session_id], self.refresh_controller.submodule_read_generation_by_session.get(session_id, 0))
207
+ self.navigation_family_by_id[target_id] = observation
208
+ self.session_registry_controller.window_by_session[session_id].statusBar().showMessage("Reading submodules…")
209
+
210
+ def prepared(family: SubmoduleSnapshot | None, error: Exception | None) -> None:
211
+ """Recheck declarations and path components before reading the target repository."""
212
+ if self.session_registry_controller.navigation_owner_by_id.get(target_id) != session_id:
213
+ return
214
+ if self.session_registry_controller.session_by_id.get(session_id) is not observation.session:
215
+ self._navigation_failed(target_id, "The requesting window changed repository. Open the submodule again.")
216
+ return
217
+ if error is not None or family is None:
218
+ self._navigation_failed(target_id, str(error))
219
+ return
220
+ # A newer family read or a refresh of the source window started after this walk: the new window reads its family again instead.
221
+ if self.session_registry_controller.identity_by_session.get(session_id) == observation.identity and self.refresh_controller.submodule_read_generation_by_session.get(session_id, 0) == observation.generation:
222
+ observation.family = family
223
+ for entry in family.entries:
224
+ if not entry.repository_dir or canonical_path(entry.repository_dir) != canonical_path(repository_dir):
225
+ continue
226
+ try:
227
+ if entry.parent_dir is not None:
228
+ declared_directory(entry.parent_dir, entry.path)
229
+ except (OSError, ValueError) as failure:
230
+ self._navigation_failed(target_id, str(failure))
231
+ return
232
+ self.refresh_controller.submodule_snapshot_by_session[session_id] = family
233
+ partial = self.submodule.has_incomplete_initialization(entry.repository_dir)
234
+ if entry.availability == "not initialized" or entry.availability == "initialized" and partial:
235
+ self._initialize_submodule(target_id, entry)
236
+ return
237
+ if entry.availability != "initialized":
238
+ command = ""
239
+ hint = ""
240
+ if entry.availability == "occupied" and entry.parent_dir is not None:
241
+ powershell = os.name == "nt"
242
+ command = initialization_command(self.repository.git_filename, entry.parent_dir, entry.path, powershell=powershell)
243
+ hint = "After clearing the folder, run this command in " + ("PowerShell:" if powershell else "your terminal:")
244
+ self._navigation_failed(target_id, entry.cause or "The submodule path is not available.", recovery_command=command, recovery_hint=hint)
245
+ return
246
+ self.repository.open_repository(repository_dir, RequestIdentity(target_id, session.generation + 1))
247
+ return
248
+ self._navigation_failed(target_id, "The declared repository is no longer available. Refresh the repository to retry.")
249
+
250
+ self.submodule.read(session.repository_context, self.repository.git_filename, prepared, snapshot.root_dir)
251
+
252
+ def switch_repository(self, session_id: str, repository_dir: str) -> None:
253
+ """Open a repository chosen in the switch menu in the same window.
254
+
255
+ The current repository stays displayed and usable until the chosen one has been read; only then does the window
256
+ change, through the same publication as a walk into a submodule. Choosing the displayed repository does nothing.
257
+ While the chosen repository is being read, another choice is refused, since that read cannot be taken back.
258
+ While the change still waits for Git in the current repository, nothing has started yet: the last choice
259
+ replaces the waiting one, and the status bar names the repository that will open.
260
+ """
261
+ if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
262
+ return
263
+ session = self.session_registry_controller.session_by_id[session_id]
264
+ current_dir = session.repository_context.repository_dir
265
+ # Menu entries are saved resolved, so the text alone recognizes the current repository without touching the disk.
266
+ if saved_recent_identity(repository_dir) == saved_recent_identity(current_dir):
267
+ return
268
+ window = self.session_registry_controller.window_by_session[session_id]
269
+ opening_dir = self.navigation_target_by_session.get(session_id)
270
+ if opening_dir is not None:
271
+ window.statusBar().showMessage(SWITCH_PENDING_STATUS.format(directory=opening_dir))
272
+ return
273
+ # A folder that no longer exists is not handed to Git: the current repository stays, and the entry may be removed.
274
+ # It is checked before any path is resolved, since resolving a folder on an unreachable drive freezes the window.
275
+ if not os.path.isdir(repository_dir):
276
+ self._offer_invalid_repository_removal(session_id, repository_dir)
277
+ return
278
+ if canonical_path(repository_dir) == canonical_path(current_dir):
279
+ return
280
+ showing_window = self.opening_controller.window_showing_folder(repository_dir, other_than=session_id)
281
+ if showing_window is not None:
282
+ self.opening_controller.bring_forward(showing_window)
283
+ return
284
+ # Changing the repository closes every window tied to the current one, with what the user typed in it: the user closes them instead.
285
+ related_title = self._open_related_window_title(session_id)
286
+ if related_title:
287
+ self._refuse_switch(session_id, related_title)
288
+ return
289
+ # Git may still be writing in the current repository: the change waits, then resumes through this same method.
290
+ if not self.close_readiness_controller.navigation_ready(session_id):
291
+ self.session_registry_controller.navigation_request_by_session[session_id] = (session, repository_dir, NavigationKind.SWITCH)
292
+ self.close_readiness_controller.show_close_wait(session_id, navigation=True)
293
+ window.statusBar().showMessage(SWITCH_WAITING_STATUS.format(directory=repository_dir))
294
+ self.close_readiness_controller.close_timer.start()
295
+ return
296
+ target_id = uuid.uuid4().hex
297
+ self.session_registry_controller.navigation_owner_by_id[target_id] = session_id
298
+ self.navigation_target_by_session[session_id] = repository_dir
299
+ self.navigation_kind_by_id[target_id] = NavigationKind.SWITCH
300
+ window.statusBar().showMessage(SWITCH_OPENING_STATUS.format(directory=repository_dir))
301
+ self.repository.open_repository(repository_dir, RequestIdentity(target_id, session.generation + 1))
302
+
303
+ def _offer_invalid_repository_removal(self, session_id: str, repository_dir: str) -> None:
304
+ """Say that the chosen repository has no folder any more, and offer to remove it from the recent history.
305
+
306
+ As in Git Extensions, every other recent entry whose folder is gone can be removed at once. The other entries are
307
+ checked only now that the chosen one turned out to be missing, never when the menu opens, so a disconnected
308
+ drive in the history cannot slow down the menu. They are the entries the menu listed, compared as saved text:
309
+ one folder check each, with no path resolved again and no rereading of the stored preferences.
310
+ """
311
+ if session_id in self.invalid_repository_prompt_by_session:
312
+ return
313
+ window = self.session_registry_controller.window_by_session[session_id]
314
+ window.statusBar().showMessage(SWITCH_MISSING_STATUS.format(directory=repository_dir))
315
+ chosen_identity = saved_recent_identity(repository_dir)
316
+ absent_dirs = [repository_dir]
317
+ for recent_dir in window.recent_dirs:
318
+ if saved_recent_identity(recent_dir) == chosen_identity or os.path.isdir(recent_dir):
319
+ continue
320
+ absent_dirs.append(recent_dir)
321
+ prompt = InvalidRepositoryPrompt(repository_dir, len(absent_dirs), window)
322
+ self.invalid_repository_prompt_by_session[session_id] = prompt
323
+ prompt.remove_selected_requested.connect(lambda: self._forget_repositories(session_id, [repository_dir]))
324
+ prompt.remove_all_requested.connect(lambda: self._forget_repositories(session_id, absent_dirs))
325
+
326
+ def finish() -> None:
327
+ """Forget the prompt once answered, unless a newer one already replaced it."""
328
+ if self.invalid_repository_prompt_by_session.get(session_id) is prompt:
329
+ self.invalid_repository_prompt_by_session.pop(session_id, None)
330
+ prompt.deleteLater()
331
+
332
+ prompt.finished.connect(finish)
333
+ open_window_modal(prompt)
334
+
335
+ def _forget_repositories(self, session_id: str, repository_dirs: list[str]) -> None:
336
+ """Remove these entries from the recent history, never any folder.
337
+
338
+ The menus of every window and the open chooser follow through the usual preference change relay. A refused save
339
+ is explained in the status bar of the window and written to the log.
340
+ """
341
+ try:
342
+ self.preference.forget(repository_dirs)
343
+ except OSError as error:
344
+ diagnostic = Diagnostic(FORGET_OPERATION, repository_dirs[0] if repository_dirs else "", self.repository.git_filename, [], str(error)).prepare()
345
+ window = self.session_registry_controller.window_by_session.get(session_id)
346
+ if window is not None and isValid(window):
347
+ window.statusBar().showMessage(diagnostic.cause)
348
+ if self.diagnostic_log is not None:
349
+ self.diagnostic_log.write(diagnostic)
350
+
351
+ def _open_related_window_title(self, session_id: str) -> str:
352
+ """Name a window still open on the repository of this view, which has to be closed before the view changes repository.
353
+
354
+ Every dialog and console of the window has the window as parent, finished consoles and confirmations included;
355
+ the Staging window is separate but opened from this view. Nothing is closed on the user's behalf, since these
356
+ windows hold what the user typed or still has to read. Settings is not checked: it is shared by every window and
357
+ has no Qt parent, so it stays on the repository it was opened for, whose path its local section shows. File
358
+ history windows are separate views with their own repository. An empty result means nothing is in the way.
359
+ """
360
+ window = self.session_registry_controller.window_by_session.get(session_id)
361
+ if window is None or not isValid(window):
362
+ return ""
363
+ for dialog in window.findChildren(QDialog):
364
+ if dialog.isVisible():
365
+ return dialog.windowTitle() or UNTITLED_RELATED_WINDOW_TITLE
366
+ session = self.session_registry_controller.session_by_id.get(session_id)
367
+ if session is None:
368
+ return ""
369
+ staging = self.staging_session_registry_controller.staging_by_key.get(WorktreeKey.from_context(session.repository_context))
370
+ if staging is None or staging.commit_push_session_id != session_id:
371
+ return ""
372
+ staging_window = staging.window
373
+ if staging_window is None or not isValid(staging_window) or not staging_window.isVisible():
374
+ return ""
375
+ return staging_window.windowTitle()
376
+
377
+ def _refuse_switch(self, session_id: str, title: str) -> None:
378
+ """Explain that the repository stays because a window tied to it is still open, in the status bar and in a message."""
379
+ window = self.session_registry_controller.window_by_session[session_id]
380
+ text = SWITCH_REFUSED_TEXT.format(title=title)
381
+ window.statusBar().showMessage(text)
382
+ message = QMessageBox(QMessageBox.Icon.Information, SWITCH_REFUSED_TITLE, text, QMessageBox.StandardButton.Ok, window)
383
+ prepare_message_box(message, SWITCH_REFUSED_TITLE)
384
+ message.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
385
+ open_window_modal(message)
386
+
387
+ def _initialize_submodule(self, target_id: str, entry: SubmoduleEntry) -> None:
388
+ """Keep the source window intact while displaying supervised initialization output."""
389
+ session_id = self.session_registry_controller.navigation_owner_by_id[target_id]
390
+ session = self.session_registry_controller.session_by_id[session_id]
391
+ window = self.session_registry_controller.window_by_session[session_id]
392
+ window.statusBar().showMessage("Initializing…")
393
+ window.show_submodule_initializing(entry.repository_dir)
394
+
395
+ def current() -> bool:
396
+ """Reject results after closure or a replacement repository session."""
397
+ return self.session_registry_controller.navigation_owner_by_id.get(target_id) == session_id and self.session_registry_controller.session_by_id.get(session_id) is session
398
+
399
+ def complete(run: SubmoduleInitRun) -> None:
400
+ """Open only after complete recursive validation, retaining partial failures for retry."""
401
+ output = self.submodule_output_by_id.get(run.operation_id)
402
+ if output is not None and isValid(output):
403
+ output.show_result(OperationStatus(run.status))
404
+ if not current():
405
+ return
406
+ if run.status != OperationStatus.SUCCESS.value or run.validation_status != OperationStatus.SUCCESS.value:
407
+ window.show_submodules(self.refresh_controller.submodule_snapshot_by_session[session_id])
408
+ # The console of a failure never closes by itself and already shows Git's lines and the cause: an error
409
+ # window opened after it would only cover it. Without a visible console, that window is the only place
410
+ # where the failure can be read.
411
+ console_shown = output is not None and isValid(output) and output.isVisible()
412
+ # A refused credential manager opens the install window, which already says what to do while the console
413
+ # keeps its red line: no error window comes on top of it.
414
+ install_window_shown = run.credential_manager_refusal is not None and self.account_controller.offer_credential_manager_install(run.credential_manager_refusal, window)
415
+ self._navigation_failed(target_id, run.diagnostic.detail if run.diagnostic else "Submodule initialization did not complete. " + OPEN_AGAIN_RETRY_SENTENCE,
416
+ show_window=not console_shown and not install_window_shown)
417
+ self._offer_account_after_submodule_refusal(session_id, entry, run)
418
+ return
419
+ self.refresh_controller.submodule_snapshot_by_session[session_id] = run.snapshot
420
+ observation = self.navigation_family_by_id.get(target_id)
421
+ if observation is not None:
422
+ # The family read before initialization showed the child as missing; the one validated at its end replaces it.
423
+ observation.family = run.snapshot
424
+ self.repository.open_repository(entry.repository_dir, RequestIdentity(target_id, session.generation + 1))
425
+
426
+ run = self.submodule.initialize(session.repository_context, self.repository.git_filename, entry, complete,
427
+ session_id=session_id, generation=session.generation, is_current=current)
428
+ self.submodule_run_by_navigation[target_id] = run
429
+ output = OperationDialog(run.trace, "Initialize submodule", window, preference=self.preference, console_operation=ConsoleOperation.SUBMODULE_INIT)
430
+ self.submodule_output_by_id[run.operation_id] = output
431
+ output.abort_requested.connect(lambda: self.submodule.cancel(run.operation_id))
432
+ output.finished.connect(run.trace.close)
433
+ output.set_running()
434
+ open_window_modal(output, keeps_window_buttons=True)
435
+
436
+ def _offer_account_after_submodule_refusal(self, session_id: str, entry: SubmoduleEntry, run: SubmoduleInitRun) -> None:
437
+ """Open Settings on Accounts when a submodule initialization was refused for an HTTPS account.
438
+
439
+ The run keeps no URL, so the address comes from Git's own refusal line; the account named is the one this
440
+ address would use now, or none when the account list cannot be read.
441
+ """
442
+ if run.diagnostic is None:
443
+ return
444
+ url = refused_https_url(run.diagnostic.stderr)
445
+ if not url:
446
+ return
447
+ try:
448
+ account = transfer_account(self.authentication.account_for_endpoint(url), url)
449
+ except (OSError, ValueError, TypeError, KeyError):
450
+ account = transfer_account(None, url)
451
+ notice = account_notice(run.diagnostic, account, SUBMODULE_NOTICE_SUBJECT_TEXT.format(path=entry.path))
452
+ if notice is not None:
453
+ self.account_controller.offer_account_after_refusal(session_id, notice)
454
+
455
+ def _abandon_navigation(self, target_id: str) -> tuple[str | None, str, NavigationKind | None]:
456
+ """Drop everything an unpublished change of repository read or planned, leaving the source window as it is.
457
+
458
+ Returns the window that asked for the change, the folder it was going to and the kind of change, so the caller
459
+ can explain why it stopped; the window is None when it no longer waits for this change. A wait message the
460
+ change left in the window is replaced by the real rebase state, since the repository stays.
461
+ """
462
+ run = self.submodule_run_by_navigation.pop(target_id, None)
463
+ if run is not None:
464
+ run.open_status = OperationStatus.FAILED.value
465
+ owner_id = self.session_registry_controller.navigation_owner_by_id.pop(target_id, None)
466
+ kind = self.navigation_kind_by_id.pop(target_id, None)
467
+ self.navigation_family_by_id.pop(target_id, None)
468
+ self.navigation_context_by_id.pop(target_id, None)
469
+ self.navigation_snapshot_by_id.pop(target_id, None)
470
+ self.navigation_commits_by_id.pop(target_id, None)
471
+ self.navigation_streamed_history_by_id.pop(target_id, None)
472
+ self.navigation_resume_by_target.pop(target_id, None)
473
+ self.repository.cancel(target_id)
474
+ self.repository.context_by_session.pop(target_id, None)
475
+ if owner_id is None:
476
+ return None, "", kind
477
+ directory = self.navigation_target_by_session.pop(owner_id, "")
478
+ self.close_readiness_controller.clear_navigation_wait(owner_id)
479
+ return owner_id, directory, kind
480
+
481
+ def _navigation_failed(self, target_id: str, cause: str, *, recovery_command: str = "", recovery_hint: str = "", show_window: bool = True) -> None:
482
+ """Discard unpublished reads and explain recovery while retaining the source repository.
483
+
484
+ The status bar always says the change of repository failed. With ``show_window`` false the caller's own console
485
+ already shows the failure and has written it to the --log file, so no error window opens over it and the
486
+ diagnostic is not written a second time.
487
+ """
488
+ owner_id, directory, kind = self._abandon_navigation(target_id)
489
+ if owner_id is None:
490
+ return
491
+ if kind is None:
492
+ kind = NavigationKind.SUBMODULE
493
+ window = self.session_registry_controller.window_by_session.get(owner_id)
494
+ if window is not None:
495
+ window.statusBar().showMessage(kind.failure_status.format(directory=directory))
496
+ if not show_window:
497
+ return
498
+ diagnostic = Diagnostic(kind.operation_label, directory, "", [], cause).prepare()
499
+ dialog = ErrorDialog(diagnostic, window, recovery_command=recovery_command, recovery_hint=recovery_hint)
500
+ self.session_registry_controller.dialogs.append(dialog)
501
+ dialog.finished.connect(lambda: self.session_registry_controller.dialogs.remove(dialog) if dialog in self.session_registry_controller.dialogs else None)
502
+ dialog.finished.connect(dialog.deleteLater)
503
+ open_window_modal(dialog)
504
+ if self.diagnostic_log is not None:
505
+ self.diagnostic_log.write(diagnostic)
506
+
507
+ def publish_repository(self, target_id: str, snapshot: HistorySnapshot) -> None:
508
+ """Replace all repository state together after target validation succeeds."""
509
+ owner_id = self.session_registry_controller.navigation_owner_by_id.get(target_id)
510
+ if owner_id not in self.session_registry_controller.window_by_session or target_id not in self.navigation_context_by_id:
511
+ self._navigation_failed(target_id, "The requesting window is closed.")
512
+ return
513
+ if snapshot.error:
514
+ self._navigation_failed(target_id, snapshot.error)
515
+ return
516
+ if not self.close_readiness_controller.navigation_ready(owner_id):
517
+ self.navigation_resume_by_target[target_id] = snapshot
518
+ self.close_readiness_controller.show_close_wait(owner_id, navigation=True)
519
+ self.close_readiness_controller.close_timer.start()
520
+ return
521
+ if self.navigation_kind_by_id.get(target_id) is NavigationKind.SWITCH:
522
+ # The current repository stayed usable while the chosen one was read, so a dialog may have opened on it
523
+ # meanwhile; publishing now would close it with its content. The change gives up instead, remembering nothing.
524
+ related_title = self._open_related_window_title(owner_id)
525
+ if related_title:
526
+ self._abandon_navigation(target_id)
527
+ self._refuse_switch(owner_id, related_title)
528
+ return
529
+ run = self.submodule_run_by_navigation.get(target_id)
530
+ if run is not None and snapshot.head_oid != run.intent.expected_oid:
531
+ self._navigation_failed(target_id, "The submodule checkout changed before opening. The current repository is still open. Refresh and retry.")
532
+ return
533
+ run = self.submodule_run_by_navigation.pop(target_id, None)
534
+ if run is not None:
535
+ run.open_status = OperationStatus.SUCCESS.value
536
+ observation = self.navigation_family_by_id.pop(target_id, None)
537
+ # The family this walk read still describes the source window only if nothing replaced that window's session,
538
+ # identity or family read since the walk started; otherwise the new window reads its family again.
539
+ family = None
540
+ if (observation is not None and observation.session is self.session_registry_controller.session_by_id.get(owner_id) and observation.identity == self.session_registry_controller.identity_by_session.get(owner_id)
541
+ and observation.generation == self.refresh_controller.submodule_read_generation_by_session.get(owner_id, 0)):
542
+ family = observation.family
543
+ context, commit = self.navigation_context_by_id.pop(target_id)
544
+ commits = self.navigation_commits_by_id.pop(target_id)
545
+ streamed_history = self.navigation_streamed_history_by_id.pop(target_id, None)
546
+ self.navigation_snapshot_by_id.pop(target_id, None)
547
+ self.session_registry_controller.navigation_owner_by_id.pop(target_id)
548
+ kind = self.navigation_kind_by_id.pop(target_id, NavigationKind.SUBMODULE)
549
+ self.navigation_target_by_session.pop(owner_id, None)
550
+ window = self.session_registry_controller.window_by_session[owner_id]
551
+ previous = self.session_registry_controller.session_by_id[owner_id]
552
+ generation = previous.generation + 1
553
+ self.repository_close_requested.emit(owner_id)
554
+ self.repository.cancel(target_id)
555
+ self.repository.context_by_session.pop(target_id, None)
556
+ identity = RequestIdentity(owner_id, generation)
557
+ session = RepositorySession(owner_id, context, self.repository)
558
+ session.generation = generation
559
+ self.session_registry_controller.session_by_id[owner_id] = session
560
+ self.session_registry_controller.window_by_session[owner_id] = window
561
+ self.session_registry_controller.identity_by_session[owner_id] = identity
562
+ self.session_registry_controller.generation_by_session[owner_id] = generation
563
+ self.repository.context_by_session[owner_id] = context
564
+ self.repository.identity_by_session[owner_id] = identity
565
+ self.repository.executable_by_session[owner_id] = self.repository.git_filename
566
+ session.selection_changed.connect(lambda oid: window.diff_panel.clear_selection("Loading diff…"))
567
+ session.selection_changed.connect(lambda oid: window.file_tree_panel.clear_selection("Loading tree…"))
568
+ session.closed.connect(window.file_tree_panel.clear_selection)
569
+ session.navigation_requested.connect(lambda oid: self.history_read_controller.read_navigation(owner_id, oid))
570
+ # The window has seen nothing of this worktree's pause state yet. A walk into a submodule asks no observation of
571
+ # its own and relies on the automatic one of its first worktree read; an observation made for an earlier visit,
572
+ # less than the refresh interval ago, would hold that one back, and a quick return to a parent paused in a rebase
573
+ # or a replay would then show neither its banner nor its rebase window.
574
+ new_key = WorktreeKey.from_context(context)
575
+ self.rebase_controller.rebase_observed_at_by_key.pop(new_key, None)
576
+ self.replay_controller.replay_observed_at_by_key.pop(new_key, None)
577
+ switched = kind is NavigationKind.SWITCH
578
+ if switched:
579
+ # A repository chosen in the switch menu opens like a new window: no position, filter or submodule family
580
+ # of the repository left behind may come back, now or when the window later reads its family again.
581
+ self.anchor_by_window.pop(owner_id, None)
582
+ self.refresh_controller.submodule_snapshot_by_session.pop(owner_id, None)
583
+ window.show_repository(context, fresh=switched)
584
+ anchor = None if switched else self.anchor_by_window.get(owner_id, {}).get(WorktreeKey.from_context(context))
585
+ selected_oid = anchor.selected_oid if anchor else (commit.oid if commit else None)
586
+ window.clear_commit_selection()
587
+ with QSignalBlocker(window.filter_edit):
588
+ window.filter_edit.setText(anchor.filter_text if anchor else "")
589
+ window.show_filter_mode(anchor.filter_mode if anchor else HISTORY_FILTER_INITIAL_MODE)
590
+ session.filter_text = window.filter_edit.text()
591
+ snapshot.generation = generation
592
+ session.snapshot = snapshot
593
+ session.streamed_history = streamed_history
594
+ self.repository.snapshot_by_session[owner_id] = snapshot
595
+ missing_selection = anchor is not None and selected_oid is not None and not any(item.oid == selected_oid for item in commits)
596
+ if missing_selection:
597
+ with QSignalBlocker(window):
598
+ window.show_matches(snapshot, commits, session.filter_text, selected_oid)
599
+ window.restore_anchor(anchor)
600
+ window.clear_commit_selection()
601
+ window.statusBar().showMessage("The previously selected commit is no longer available. Select a commit to continue.")
602
+ else:
603
+ window.show_matches(snapshot, commits, session.filter_text, selected_oid)
604
+ if anchor is not None:
605
+ window.restore_anchor(anchor)
606
+ # A remote read the previous repository started for the Fetch menu was dropped with its session and never
607
+ # releases the button: the busy state follows the transports of the repository shown now.
608
+ window.set_fetch_busy(self.transport_controller.fetch_busy_for(context.common_git_dir))
609
+ if switched:
610
+ # As when a window opens: the pause banners of the new repository are read now rather than at the next
611
+ # working-tree count, and a paused rebase offers its dialog. The automatic observation that follows is held
612
+ # back by the rebase refresh interval, so the state is not read twice.
613
+ self.rebase_controller.observe_rebase(owner_id, open_paused=True)
614
+ if family is not None:
615
+ family_generation = self.refresh_controller.submodule_read_generation_by_session.get(owner_id, 0) + 1
616
+ self.refresh_controller.submodule_read_generation_by_session[owner_id] = family_generation
617
+ self.refresh_controller.publish_submodule_family(owner_id, family, identity, family_generation)
618
+ elif not switched:
619
+ self.session_registry_controller.window_by_session[owner_id].show_submodules(self.refresh_controller.submodule_snapshot_by_session[owner_id])
620
+ # The capture the target's history was built from becomes the new window's branch list once its reference files
621
+ # are checked unchanged; only its generation changes, to the new window's.
622
+ previous = replace(snapshot.reference_snapshot, generation=generation) if snapshot.reference_snapshot is not None else None
623
+ self.reference_read_controller.read_references(owner_id, previous=previous)
624
+ if WindowCapability.MANAGE in session.capabilities and not context.is_bare:
625
+ # The window shows no count of this worktree yet (a switch cleared it, a walk still shows the previous
626
+ # repository's), and a worktree read earlier in the run answers with what it already had.
627
+ self.staging_session_registry_controller.staging_session(context).refresh(announce=True)
628
+ if family is None:
629
+ self.refresh_controller.read_submodules(owner_id)
630
+ diagnostic = self.opening_controller.remember_repository(context.repository_dir)
631
+ if switched:
632
+ # A window just opened takes its reading proportions from the preferences, not from the repository left behind.
633
+ window.apply_reading_preference(self.preference.value_by_key)
634
+ if diagnostic is not None:
635
+ self.opening_controller.show_preference_failure(diagnostic, window)
636
+
637
+ def navigation_commit_batch_read(self, identity: RequestIdentity, commits: list[Commit]) -> None:
638
+ """Keep the rows a navigation's history read just parsed, so the window it replaces can show them without reading Git again."""
639
+ if identity.session_id not in self.session_registry_controller.navigation_owner_by_id:
640
+ return
641
+ snapshot = self.repository.snapshot_by_session.get(identity.session_id)
642
+ if snapshot is not None:
643
+ previous = self.navigation_streamed_history_by_id.get(identity.session_id)
644
+ self.navigation_streamed_history_by_id[identity.session_id] = keep_streamed_commits(previous, snapshot, commits)
645
+
646
+ def navigation_matching_read(self, identity: RequestIdentity, commits: list[Commit], scanned_count: int, complete: bool) -> None:
647
+ """Gather the commits found for a navigation, then replace the window's repository once the last ones arrive."""
648
+ if identity.session_id not in self.session_registry_controller.navigation_owner_by_id:
649
+ return
650
+ self.navigation_commits_by_id[identity.session_id].extend(commits)
651
+ if complete:
652
+ self.publish_repository(identity.session_id, self.navigation_snapshot_by_id[identity.session_id])
653
+
654
+ def navigation_read_failed(self, identity: RequestIdentity, failure: RepositoryFailure) -> None:
655
+ """Give up a navigation whose repository read failed, keeping the window on the repository it shows."""
656
+ if identity.session_id not in self.session_registry_controller.navigation_owner_by_id:
657
+ return
658
+ self._navigation_failed(identity.session_id, failure.cause)