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,367 @@
1
+ """Manage the stashes of a repository window."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from dataclasses import dataclass
7
+ from typing import TYPE_CHECKING
8
+
9
+ from PySide6.QtCore import QObject, Qt, Signal
10
+ from PySide6.QtWidgets import QMessageBox
11
+ from shiboken6 import isValid
12
+
13
+ from gitpane.config import ConsoleOperation
14
+ from gitpane.controller.refresh_controller import RefreshScope
15
+ from gitpane.history.file_history import WindowCapability
16
+ from gitpane.operation.console_line import END_TEXT_TEMPLATE, REPOSITORY_HEADER_LABEL, STASH_POP_LABEL, ConsoleHeader, operation_end_text
17
+ from gitpane.operation.diagnostic import Diagnostic
18
+ from gitpane.operation.operation_status import OperationStatus
19
+ from gitpane.operation.operation_trace import OperationTrace, publish_after_trace
20
+ from gitpane.operation.repository_operation import OperationAction, OperationIntent
21
+ from gitpane.reference.stash import DROP_OPERATION_LABEL, StashDropIntent, StashObservation, pop_stopped_on_conflict
22
+ from gitpane.reference.stash_list import stash_selector
23
+ from gitpane.sequencer.conflict import snapshot_has_conflicts
24
+ from gitpane.system.path_identity import canonical_path
25
+ from gitpane.ui.console.operation_dialog import OperationDialog
26
+ from gitpane.ui.reference.stash_dialog import StashDialog
27
+ from gitpane.ui.window_control.dialog_modality import open_window_modal, prepare_message_box
28
+ from gitpane.worktree.worktree_snapshot import WorktreeKey
29
+
30
+ if TYPE_CHECKING:
31
+ from gitpane.controller.operation_policy import OperationPolicy
32
+ from gitpane.controller.refresh_controller import FullRefreshRead, RefreshController
33
+ from gitpane.controller.replay_controller import ReplayController
34
+ from gitpane.controller.session_registry import SessionRegistry
35
+ from gitpane.controller.staging_session_registry import StagingSessionRegistry
36
+ from gitpane.controller.window_activation import WindowActivationFilter
37
+ from gitpane.history.repository import GitRepository
38
+ from gitpane.operation.diagnostic import DiagnosticLog
39
+ from gitpane.operation.repository_operation import OperationOutcome
40
+ from gitpane.preference.preference_store import PreferenceStore
41
+ from gitpane.reference.stash import StashIntent, StashService
42
+ from gitpane.reference.stash_list import StashEntry
43
+ from gitpane.worktree.worktree_service import WorktreeService
44
+
45
+ # The question Drop stash asks, filled with the stash selector and the message the left panel shows.
46
+ STASH_DROP_QUESTION = "Drop {selector}: {message}?\n\nThe stash is deleted. Working files, the index and submodules are not changed."
47
+ STASH_DROP_CANCEL_LABEL = "Cancel"
48
+ STASH_DROPPED_STATUS = "Dropped {selector}."
49
+ STASH_DROP_FAILED_PREFIX = "Drop stash refused: "
50
+ STASH_DROP_VIEW_CHANGED_CAUSE = "The repository view changed; refresh it and try again"
51
+ # Where a stash drop came from, as the --log journal names it.
52
+ STASH_DROP_MANUAL_ORIGIN = "Drop stash from the left panel"
53
+ STASH_DROP_AUTOMATIC_ORIGIN = "Stash kept by a Stash pop, dropped after its conflicts were resolved in GitPane"
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class StashDropAfterResolution:
58
+ """Remember that a conflict of a Stash pop was just resolved in GitPane, so the stash that pop kept may be dropped.
59
+
60
+ Git keeps a stash whose pop stopped on conflicts. Once the last of those conflicts is resolved in GitPane's conflict
61
+ list or merge tool, the stash is dropped with nothing shown, the way users expect a pop to end. ``known_generation``
62
+ is the worktree read generation when the resolution finished: only a later read shows its result, and that read
63
+ decides. A read still listing a conflict ends the request; the next resolution in GitPane asks again.
64
+ """
65
+
66
+ session_id: str
67
+ retained_oid: str
68
+ known_generation: int
69
+
70
+
71
+ class StashController(QObject):
72
+ """The stashes of every repository window: the Stash form, Stash pop with its console, Drop stash with its question, and the drop of a stash a Stash pop kept, once GitPane resolved its conflicts."""
73
+
74
+ # A Stash pop that ended with its stash kept tells the conflict, which watches that stash while the pop's conflicts
75
+ # remain: the worktree key and the observation of the kept stash. Connected directly, so the watch starts before
76
+ # the rest of the pop's result is handled, as when the stash called the conflict itself.
77
+ conflict_retention_requested = Signal(object, object)
78
+
79
+ def __init__(self, *, session_registry_controller: SessionRegistry, operation_policy_controller: OperationPolicy, staging_session_registry_controller: StagingSessionRegistry, window_activation_controller: WindowActivationFilter,
80
+ refresh_controller: RefreshController, replay_controller: ReplayController, preference: PreferenceStore, repository: GitRepository, worktree_service: WorktreeService,
81
+ stash: StashService, diagnostic_log: DiagnosticLog | None, parent: QObject | None = None) -> None:
82
+ """Receive the services and the lower controllers a stash works with; the wiring builds it after the rebase controller, and the conflict hears from it through a signal the wiring connects."""
83
+ super().__init__(parent)
84
+ self.session_registry_controller = session_registry_controller
85
+ self.operation_policy_controller = operation_policy_controller
86
+ self.staging_session_registry_controller = staging_session_registry_controller
87
+ self.window_activation_controller = window_activation_controller
88
+ self.refresh_controller = refresh_controller
89
+ self.preference = preference
90
+ self.repository = repository
91
+ self.worktree_service = worktree_service
92
+ self.stash = stash
93
+ self.diagnostic_log = diagnostic_log
94
+ self.replay_controller = replay_controller
95
+ self.stash_dialog_by_session: dict[str, StashDialog] = {}
96
+ self.stash_intent_by_id: dict[str, StashIntent] = {}
97
+ self.stash_pop_intent_by_id: dict[str, OperationIntent] = {}
98
+ self.stash_pop_dialog_by_id: dict[str, OperationDialog] = {}
99
+ self.stash_pop_trace_by_id: dict[str, OperationTrace] = {}
100
+ self.stash_drop_by_id: dict[str, StashDropIntent] = {}
101
+ # The drops GitPane makes on its own, after the conflicts of a Stash pop were resolved: they show nothing.
102
+ self.stash_drop_automatic_id_set: set[str] = set()
103
+ self.stash_drop_confirmation_by_session: dict[str, QMessageBox] = {}
104
+ self.stash_drop_after_resolution_by_key: dict[WorktreeKey, StashDropAfterResolution] = {}
105
+
106
+ def open_stash(self, session_id: str) -> StashDialog | None:
107
+ """Open or explicitly reload a retained stash draft under current management authority."""
108
+ if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
109
+ return None
110
+ window = self.session_registry_controller.window_by_session[session_id]
111
+ if window.context.is_bare:
112
+ return None
113
+ existing = self.stash_dialog_by_session.get(session_id)
114
+ if existing is not None and isValid(existing) and not existing.closed:
115
+ existing.raise_()
116
+ if existing.running or existing.capture_pending:
117
+ return existing
118
+ dialog = existing
119
+ else:
120
+ dialog = StashDialog(session_id, self.preference, window)
121
+ dialog.installEventFilter(self.window_activation_controller)
122
+ self.stash_dialog_by_session[session_id] = dialog
123
+ dialog.submit_requested.connect(lambda intent: self.submit_stash(session_id, intent))
124
+ session = self.session_registry_controller.session_by_id[session_id]
125
+ generation = session.generation
126
+ dialog.begin_capture()
127
+ dialog.show()
128
+
129
+ def complete(snapshot: object, error: Exception | None) -> None:
130
+ """Publish only to the still-authorized form that requested this read."""
131
+ if dialog.closed or not isValid(dialog):
132
+ return
133
+ if session.is_closed or session.generation != generation or not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
134
+ dialog.show_error("The repository changed. Click Stash in the toolbar to reload this draft.")
135
+ elif error is not None:
136
+ dialog.show_error(str(error))
137
+ else:
138
+ dialog.populate(snapshot)
139
+
140
+ self.worktree_service.read(window.context, self.repository.git_filename, generation, complete)
141
+ return dialog
142
+
143
+ def submit_stash(self, session_id: str, intent: StashIntent) -> str | None:
144
+ """Submit only the original visible form's immutable intention."""
145
+ dialog = self.stash_dialog_by_session.get(session_id)
146
+ if (not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE) or dialog is None or dialog.closed
147
+ or dialog.intent != intent or intent.operation.session_id != session_id):
148
+ return None
149
+
150
+ def is_current() -> bool:
151
+ """Recheck authority after asynchronous reads and any native queue wait."""
152
+ return (not dialog.closed and self.operation_policy_controller.operation_allowed(intent.operation)
153
+ and self.repository.git_filename == intent.operation.git_filename)
154
+
155
+ self.stash_intent_by_id[intent.operation.operation_id] = intent
156
+ return self.stash.create(intent, is_current=is_current)
157
+
158
+ def stash_finished(self, outcome: OperationOutcome) -> None:
159
+ """Keep uncertain drafts while refreshing every view of the original worktree."""
160
+ intent = self.stash_intent_by_id.pop(outcome.operation_id, None)
161
+ if intent is None:
162
+ return
163
+ self.stash.outcome_by_id[outcome.operation_id] = outcome
164
+ dialog = self.stash_dialog_by_session.get(intent.operation.session_id)
165
+ if dialog is not None and isValid(dialog) and not dialog.closed and dialog.intent == intent:
166
+ dialog.show_result(outcome)
167
+ if outcome.diagnostic is not None and self.diagnostic_log is not None:
168
+ self.diagnostic_log.write(outcome.diagnostic)
169
+ shared_by_key: dict[WorktreeKey, FullRefreshRead] = {}
170
+ for session_id, session in tuple(self.session_registry_controller.session_by_id.items()):
171
+ if not session.is_closed and WorktreeKey.from_context(session.repository_context) == intent.operation.key:
172
+ self.refresh_controller.refresh(session_id, full_refresh_read=self.refresh_controller.full_refresh_read_for(session, shared_by_key))
173
+ refreshed_key_set = self.refresh_controller.observe_full_refresh_pauses(shared_by_key)
174
+ staging = self.staging_session_registry_controller.staging_by_key.get(intent.operation.key)
175
+ # A preparation session without any open repository window still shows the result.
176
+ if staging is not None and intent.operation.key not in refreshed_key_set:
177
+ staging.refresh()
178
+
179
+ def stash_pop(self, session_id: str) -> OperationDialog | None:
180
+ """Apply the latest stash directly with the shared output preference and write owner."""
181
+ if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
182
+ return None
183
+ session = self.session_registry_controller.session_by_id[session_id]
184
+ if session.repository_context.is_bare:
185
+ return None
186
+ operation = OperationIntent.capture(uuid.uuid4().hex, session.repository_context, self.repository.git_filename,
187
+ session_id=session_id, generation=self.session_registry_controller.generation_by_session[session_id], operation=STASH_POP_LABEL, action=OperationAction.STASH)
188
+ trace = OperationTrace(operation.operation_id, self.repository.runner, self.diagnostic_log, header=ConsoleHeader(REPOSITORY_HEADER_LABEL, operation.context.repository_dir))
189
+ dialog = OperationDialog(trace, STASH_POP_LABEL, self.session_registry_controller.window_by_session[session_id], preference=self.preference, console_operation=ConsoleOperation.STASH_POP)
190
+ self.stash_pop_intent_by_id[operation.operation_id] = operation
191
+ self.stash_pop_trace_by_id[operation.operation_id] = trace
192
+ self.stash_pop_dialog_by_id[operation.operation_id] = dialog
193
+ dialog.finished.connect(trace.close)
194
+ dialog.set_running()
195
+ open_window_modal(dialog, keeps_window_buttons=True)
196
+ self.stash.pop(operation, is_current=lambda: self.operation_policy_controller.operation_allowed(operation) and self.repository.git_filename == operation.git_filename)
197
+ return dialog
198
+
199
+ def stash_pop_finished(self, outcome: OperationOutcome) -> None:
200
+ """Retain output independently and refresh only the original current repository session."""
201
+ operation = self.stash_pop_intent_by_id.pop(outcome.operation_id, None)
202
+ if operation is None:
203
+ return
204
+ self.stash.outcome_by_id[outcome.operation_id] = outcome
205
+ trace = self.stash_pop_trace_by_id[outcome.operation_id]
206
+ allowed = self.operation_policy_controller.operation_allowed(operation)
207
+
208
+ def publish_result() -> None:
209
+ """Write the result in the console once the trace has ended the last process with its own line."""
210
+ if outcome.diagnostic is not None:
211
+ # The log keeps the cause alone here, as it always did; the console shows the diagnostic's own short line.
212
+ trace.append_record(outcome.operation_id, "status", outcome.diagnostic.cause,
213
+ trace.diagnostic_console_line(outcome.diagnostic, outcome.status, paused=pop_stopped_on_conflict(outcome)))
214
+ trace.append_end_line(outcome.operation_id, "status", END_TEXT_TEMPLATE.format(label=STASH_POP_LABEL, word=outcome.status.value),
215
+ operation_end_text(STASH_POP_LABEL, outcome.status), outcome.status, expected_stop=pop_stopped_on_conflict(outcome))
216
+ dialog = self.stash_pop_dialog_by_id.get(outcome.operation_id)
217
+ if dialog is not None and isValid(dialog):
218
+ # Conflicts are a pause: the conflict question and the repository window wait for the user, not the console.
219
+ dialog.show_result(outcome.status, expected_stop=pop_stopped_on_conflict(outcome))
220
+ if not allowed:
221
+ trace.close()
222
+
223
+ publish_after_trace(publish_result, self)
224
+ if not allowed:
225
+ return
226
+ for step in outcome.steps:
227
+ if isinstance(step.after, StashObservation) and step.after.retained_oid is not None:
228
+ self.conflict_retention_requested.emit(operation.key, step.after)
229
+ if step.after.unmerged_paths:
230
+ self.session_registry_controller.conflict_pending_session_set.add(operation.session_id)
231
+ shared_by_key: dict[WorktreeKey, FullRefreshRead] = {}
232
+ for session_id, session in tuple(self.session_registry_controller.session_by_id.items()):
233
+ if not session.is_closed and WorktreeKey.from_context(session.repository_context) == operation.key:
234
+ self.refresh_controller.checkout_refresh_session_set.add(session_id)
235
+ self.refresh_controller.refresh(session_id, full_refresh_read=self.refresh_controller.full_refresh_read_for(session, shared_by_key, explicit=True))
236
+ refreshed_key_set = self.refresh_controller.observe_full_refresh_pauses(shared_by_key)
237
+ staging = self.staging_session_registry_controller.staging_by_key.get(operation.key)
238
+ # A preparation session without any open repository window still shows the result.
239
+ if staging is not None and operation.key not in refreshed_key_set:
240
+ staging.refresh(explicit=True)
241
+
242
+ def drop_stash(self, session_id: str, entry: StashEntry) -> QMessageBox | None:
243
+ """Ask before dropping the stash chosen in the left panel, and drop exactly that stash once the user agrees.
244
+
245
+ The question names the stash by its selector and message, with Cancel as the default and Escape answer. The
246
+ stash is checked again by its commit just before Git drops it, so a list changed meanwhile drops nothing.
247
+ """
248
+ if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
249
+ return None
250
+ window = self.session_registry_controller.window_by_session.get(session_id)
251
+ session = self.session_registry_controller.session_by_id[session_id]
252
+ if window is None or session.repository_context.is_bare:
253
+ return None
254
+ previous = self.stash_drop_confirmation_by_session.pop(session_id, None)
255
+ if previous is not None and isValid(previous):
256
+ previous.close()
257
+ previous.deleteLater()
258
+ operation = OperationIntent.capture(uuid.uuid4().hex, session.repository_context, self.repository.git_filename, session_id=session_id,
259
+ generation=self.session_registry_controller.generation_by_session[session_id], operation=DROP_OPERATION_LABEL, action=OperationAction.STASH)
260
+ intent = StashDropIntent.of_entry(operation, entry)
261
+ text = STASH_DROP_QUESTION.format(selector=stash_selector(entry.index), message=entry.message)
262
+ confirmation = QMessageBox(QMessageBox.Icon.Question, DROP_OPERATION_LABEL, text, QMessageBox.StandardButton.NoButton, window)
263
+ prepare_message_box(confirmation, DROP_OPERATION_LABEL)
264
+ confirmation.setTextFormat(Qt.TextFormat.PlainText)
265
+ drop_button = confirmation.addButton(DROP_OPERATION_LABEL, QMessageBox.ButtonRole.DestructiveRole)
266
+ cancel_button = confirmation.addButton(STASH_DROP_CANCEL_LABEL, QMessageBox.ButtonRole.RejectRole)
267
+ confirmation.setDefaultButton(cancel_button)
268
+ confirmation.setEscapeButton(cancel_button)
269
+ self.stash_drop_confirmation_by_session[session_id] = confirmation
270
+
271
+ def answer() -> None:
272
+ """Drop only on an explicit Drop stash; any other ending leaves every stash in place."""
273
+ if self.stash_drop_confirmation_by_session.get(session_id) is confirmation:
274
+ del self.stash_drop_confirmation_by_session[session_id]
275
+ accepted = confirmation.clickedButton() is drop_button
276
+ confirmation.deleteLater()
277
+ if accepted:
278
+ self._submit_stash_drop(intent, automatic=False)
279
+
280
+ confirmation.finished.connect(answer)
281
+ open_window_modal(confirmation)
282
+ return confirmation
283
+
284
+ def _submit_stash_drop(self, intent: StashDropIntent, *, automatic: bool) -> None:
285
+ """Hand one drop to the stash service while its window may still write, under the Git it was asked with."""
286
+ operation = intent.operation
287
+ if not self.operation_policy_controller.operation_allowed(operation) or self.repository.git_filename != operation.git_filename:
288
+ self._report_stash_drop(intent, automatic, OperationStatus.REFUSED, STASH_DROP_VIEW_CHANGED_CAUSE)
289
+ return
290
+ self.stash_drop_by_id[operation.operation_id] = intent
291
+ if automatic:
292
+ self.stash_drop_automatic_id_set.add(operation.operation_id)
293
+ self.stash.drop(intent, is_current=lambda: self.operation_policy_controller.operation_allowed(operation) and self.repository.git_filename == operation.git_filename)
294
+
295
+ def stash_drop_finished(self, outcome: OperationOutcome) -> None:
296
+ """Report a finished drop and read the stash list again in every window of that repository, whatever the result."""
297
+ intent = self.stash_drop_by_id.pop(outcome.operation_id, None)
298
+ if intent is None:
299
+ return
300
+ automatic = outcome.operation_id in self.stash_drop_automatic_id_set
301
+ self.stash_drop_automatic_id_set.discard(outcome.operation_id)
302
+ cause = outcome.diagnostic.cause if outcome.diagnostic is not None else ""
303
+ self._report_stash_drop(intent, automatic, outcome.status, cause)
304
+ # The stash list lives in the shared Git directory: every window of the repository, linked worktrees included, shows it.
305
+ for session_id, session in tuple(self.session_registry_controller.session_by_id.items()):
306
+ if (not session.is_closed and WindowCapability.MANAGE in session.capabilities
307
+ and canonical_path(session.repository_context.common_git_dir) == intent.operation.common_git_dir):
308
+ self.refresh_controller.refresh(session_id, scope=RefreshScope.REFERENCES)
309
+
310
+ def _report_stash_drop(self, intent: StashDropIntent, automatic: bool, status: OperationStatus, cause: str) -> None:
311
+ """Write every drop to the --log journal with the stash commit, and tell the user about the drops they asked for.
312
+
313
+ The dropped commit stays in the object database until Git prunes it, so with --log the journal line is what lets
314
+ it be recovered. A drop GitPane makes on its own after a Stash pop shows nothing at all, by design: without
315
+ --log no trace of that commit is kept, and only ``git fsck --unreachable`` finds it before the next prune. A
316
+ failed drop that carries no cause shows the word of its status instead.
317
+ """
318
+ operation = intent.operation
319
+ if self.diagnostic_log is not None:
320
+ origin = STASH_DROP_AUTOMATIC_ORIGIN if automatic else STASH_DROP_MANUAL_ORIGIN
321
+ message = f"{origin}: {status.value}. Stash commit: {intent.oid}." + (" " + cause if cause else "")
322
+ self.diagnostic_log.write(Diagnostic(DROP_OPERATION_LABEL, operation.context.repository_dir, operation.git_filename, [], message).prepare())
323
+ if automatic or operation.session_id is None:
324
+ return
325
+ window = self.session_registry_controller.window_by_session.get(operation.session_id)
326
+ if window is None or not isValid(window):
327
+ return
328
+ if status == OperationStatus.SUCCESS:
329
+ window.statusBar().showMessage(STASH_DROPPED_STATUS.format(selector=stash_selector(intent.index)))
330
+ return
331
+ window.statusBar().showMessage(STASH_DROP_FAILED_PREFIX + (cause or status.value))
332
+
333
+ def continue_stash_drops_after_resolution(self) -> None:
334
+ """Look again at every waiting drop, for instance once the merge tool or another write has released the repository."""
335
+ for key in tuple(self.stash_drop_after_resolution_by_key):
336
+ self.continue_stash_drop_after_resolution(key)
337
+
338
+ def continue_stash_drop_after_resolution(self, key: WorktreeKey) -> None:
339
+ """Drop the kept stash once a worktree read newer than the resolution shows no conflict and nothing holds the repository.
340
+
341
+ A read still listing a conflict ends the request without dropping anything. A closed or departed window ends it
342
+ too. The drop itself checks again, under the repository reservation, that no conflict remains and that the
343
+ stash commit is still in the list, and shows nothing to the user whatever happens; only the --log journal keeps
344
+ the dropped commit.
345
+ """
346
+ request = self.stash_drop_after_resolution_by_key.get(key)
347
+ if request is None:
348
+ return
349
+ session = self.session_registry_controller.session_by_id.get(request.session_id)
350
+ if (session is None or not self.operation_policy_controller.allows(request.session_id, WindowCapability.MANAGE) or WorktreeKey.from_context(session.repository_context) != key
351
+ or self.replay_controller.replay_session_leaving(request.session_id)):
352
+ del self.stash_drop_after_resolution_by_key[key]
353
+ return
354
+ staging = self.staging_session_registry_controller.staging_by_key.get(key)
355
+ snapshot = staging.snapshot if staging is not None else None
356
+ if (snapshot is None or not snapshot.fresh or staging.refresh_active or staging.refresh_queued or staging.explicit_refresh
357
+ or snapshot.generation <= request.known_generation):
358
+ return
359
+ if snapshot_has_conflicts(snapshot):
360
+ del self.stash_drop_after_resolution_by_key[key]
361
+ return
362
+ if self.replay_controller.replay_writer_busy(session.repository_context):
363
+ return
364
+ del self.stash_drop_after_resolution_by_key[key]
365
+ operation = OperationIntent.capture(uuid.uuid4().hex, session.repository_context, self.repository.git_filename, session_id=request.session_id,
366
+ generation=self.session_registry_controller.generation_by_session[request.session_id], operation=DROP_OPERATION_LABEL, action=OperationAction.STASH)
367
+ self._submit_stash_drop(StashDropIntent.of_retained(operation, request.retained_oid), automatic=True)