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,309 @@
1
+ """Serve each rebase edit request from its window until Git gets its answer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING
7
+
8
+ from PySide6.QtCore import QObject, Signal
9
+ from shiboken6 import isValid
10
+
11
+ from gitpane.config import LINE_SEPARATOR
12
+ from gitpane.sequencer.commit_message_rule import message_is_empty, read_commit_message_rule
13
+ from gitpane.sequencer.rebase_edit_file import read_edit_file, write_edit_file
14
+ from gitpane.sequencer.rebase_state import read_message_context
15
+ from gitpane.tool.editor import EditorAnswer, EditorRequestKind
16
+ from gitpane.ui.sequencer.rebase_editor_window import EditorValidation, RebaseEditorText, RebaseEditorWindow
17
+ from gitpane.worktree.command_sequence import FileTask
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Generator
21
+
22
+ from PySide6.QtWidgets import QWidget
23
+
24
+ from gitpane.history.repository_value import RepositoryContext
25
+ from gitpane.sequencer.commit_message_rule import CommitMessageRule
26
+ from gitpane.sequencer.rebase_edit_file import EditFileContent
27
+ from gitpane.sequencer.rebase_state import RebaseMessageCommit
28
+ from gitpane.tool.editor import EditorRequest, EditorService, PendingEdit
29
+ from gitpane.worktree.worktree_service import WorktreeService
30
+
31
+ @dataclass(frozen=True)
32
+ class ExpectedEdit:
33
+ """What one rebase operation needs to show the edits Git asks for: its repository, its Git and how its windows validate."""
34
+
35
+ context: RepositoryContext
36
+ git_filename: str
37
+ validation: EditorValidation
38
+ parent_window: QWidget | None
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class EditRead:
43
+ """What the window of one edit needs: the file Git waits for and, for a commit message, the commits it is about and the repository's rule for an empty message."""
44
+
45
+ content: EditFileContent
46
+ commits: tuple[RebaseMessageCommit, ...] = ()
47
+ rule: CommitMessageRule | None = None
48
+
49
+
50
+ # Between the short hash of a commit and its subject in the header of a message window.
51
+ COMMIT_LINE_SEPARATOR = " "
52
+
53
+
54
+ def message_context_lines(repository_dir: str, commits: tuple[RebaseMessageCommit, ...]) -> tuple[str, ...]:
55
+ """Answer the header of a message window: the repository folder, then the short hash and subject of each commit the message is about."""
56
+ lines = [repository_dir]
57
+ for commit in commits:
58
+ lines.append(commit.short_oid + COMMIT_LINE_SEPARATOR + commit.subject)
59
+ return tuple(lines)
60
+
61
+
62
+ def edit_failure_reason(error: Exception) -> str:
63
+ """Name why a read or a write failed without quoting the file's content or its path.
64
+
65
+ The system's own reason is kept for a system error; the errors raised by the file module carry fixed texts.
66
+ """
67
+ if isinstance(error, OSError) and error.strerror:
68
+ return error.strerror
69
+ if isinstance(error, UnicodeError):
70
+ return RebaseEditorText.ENCODING_FAILED.value
71
+ return str(error)
72
+
73
+
74
+ def save_failure_text(error: Exception | None) -> str:
75
+ """Answer the line a failed save shows: its fixed start, then why the file could not be written, never the text."""
76
+ cause = edit_failure_reason(error) if error is not None else RebaseEditorText.WRITE_GAVE_NO_RESULT.value
77
+ return RebaseEditorText.SAVE_FAILED.value.format(cause=cause)
78
+
79
+
80
+ class RebaseEditorController(QObject):
81
+ """Turn the edits Git asks for during an expected rebase operation into windows, then their answers into the codes Git receives."""
82
+
83
+ # Tells that the window of an edit Git waits for is shown: the EditorRequest and its RebaseEditorWindow.
84
+ window_shown = Signal(object, object)
85
+
86
+ def __init__(self, *, editor: EditorService, worktree_service: WorktreeService, parent: QObject | None = None) -> None:
87
+ """Receive the editor service whose requests it serves and the scheduler that reads and writes Git's files off the Qt thread.
88
+
89
+ The rebase coordinator builds it and says which operations it serves; a test builds it on bare services.
90
+ """
91
+ super().__init__(parent)
92
+ self.editor = editor
93
+ self.worktree_service = worktree_service
94
+ self.expected_by_operation_id: dict[str, ExpectedEdit] = {}
95
+ self.window_by_operation_id: dict[str, RebaseEditorWindow] = {}
96
+ self.pending_by_operation_id: dict[str, PendingEdit] = {}
97
+ self.content_by_operation_id: dict[str, EditFileContent] = {}
98
+ # The repository's rule for an empty commit message, read when Git asked for the message the window shows.
99
+ self.rule_by_operation_id: dict[str, CommitMessageRule] = {}
100
+ # The problem a command was started to fix, shown once in the first window of that command.
101
+ self.error_text_by_operation_id: dict[str, str] = {}
102
+ # The error line a window opened with, such as Git's refusal of the instructions: a successful save shows it
103
+ # again instead of clearing the line.
104
+ self.opening_error_text_by_operation_id: dict[str, str] = {}
105
+ editor.edit_requested.connect(self._edit_requested)
106
+ editor.edit_cancelled.connect(self._edit_cancelled)
107
+
108
+ def expect_edits(self, operation_id: str, context: RepositoryContext, git_filename: str, validation: EditorValidation, parent_window: QWidget | None, error_text: str = "") -> None:
109
+ """Serve the edits Git asks for during this operation, each in a window parented to the given one.
110
+
111
+ An error text, such as the problem Git found in the instructions, is shown above the text of the first window.
112
+ """
113
+ self.expected_by_operation_id[operation_id] = ExpectedEdit(context, git_filename, validation, parent_window)
114
+ if error_text:
115
+ self.error_text_by_operation_id[operation_id] = error_text
116
+ else:
117
+ self.error_text_by_operation_id.pop(operation_id, None)
118
+
119
+ def raise_windows(self) -> None:
120
+ """Bring every editor window still waiting for its answer to the front, so a close that waits for Git sends the user to it."""
121
+ for window in tuple(self.window_by_operation_id.values()):
122
+ if not isValid(window) or window.answered:
123
+ continue
124
+ window.show()
125
+ window.raise_()
126
+ window.activateWindow()
127
+
128
+ def forget(self, operation_id: str) -> None:
129
+ """Stop serving this operation and close the window left from its last edit."""
130
+ self.expected_by_operation_id.pop(operation_id, None)
131
+ self.pending_by_operation_id.pop(operation_id, None)
132
+ self.content_by_operation_id.pop(operation_id, None)
133
+ self.rule_by_operation_id.pop(operation_id, None)
134
+ self.error_text_by_operation_id.pop(operation_id, None)
135
+ self.opening_error_text_by_operation_id.pop(operation_id, None)
136
+ window = self.window_by_operation_id.pop(operation_id, None)
137
+ if window is not None:
138
+ window.close_after_answer()
139
+ window.deleteLater()
140
+
141
+ def _edit_requested(self, pending: PendingEdit) -> None:
142
+ """Read the file of an edit this controller expects; an edit of another operation is left to its own owner."""
143
+ request = pending.request
144
+ expected = self.expected_by_operation_id.get(request.operation_id)
145
+ if expected is None:
146
+ return
147
+ self.pending_by_operation_id[request.operation_id] = pending
148
+ self.content_by_operation_id.pop(request.operation_id, None)
149
+ self.rule_by_operation_id.pop(request.operation_id, None)
150
+ self.worktree_service.run(expected.context, expected.git_filename, self._read(request, expected.context.git_dir),
151
+ lambda read, error: self._show(pending, expected, read, error))
152
+
153
+ def _edit_cancelled(self, request: EditorRequest) -> None:
154
+ """Close the window of a request whose bridge was lost; a click left on it no longer reaches Git."""
155
+ pending = self.pending_by_operation_id.get(request.operation_id)
156
+ if pending is None or pending.request is not request:
157
+ return
158
+ del self.pending_by_operation_id[request.operation_id]
159
+ window = self.window_by_operation_id.get(request.operation_id)
160
+ if window is not None:
161
+ window.close_after_answer()
162
+
163
+ def _read(self, request: EditorRequest, git_dir: str) -> Generator:
164
+ """Read the file Git waits for off the Qt thread; for a commit message, read first the repository's message settings, then the commits it is about.
165
+
166
+ One git config call gives the comment prefix and cleanup the empty-message rule follows, and the commit encoding
167
+ a message that is not UTF-8 is decoded and written back with.
168
+ """
169
+ if request.kind != EditorRequestKind.MESSAGE:
170
+ content = yield FileTask(lambda: read_edit_file(request.file_filename, request.file_identity))
171
+ return EditRead(content)
172
+ rule = yield from read_commit_message_rule()
173
+ content = yield FileTask(lambda: read_edit_file(request.file_filename, request.file_identity, fallback_codec=rule.commit_encoding))
174
+ commits = yield FileTask(lambda: read_message_context(git_dir))
175
+ return EditRead(content, commits, rule)
176
+
177
+ def _show(self, pending: PendingEdit, expected: ExpectedEdit, read: EditRead | None, error: Exception | None) -> None:
178
+ """Open the window of a read edit, read-only with its reason when the file cannot be written back exactly.
179
+
180
+ A message window names above its text the repository and the commits the message is about.
181
+ """
182
+ operation_id = pending.request.operation_id
183
+ content = read.content if read is not None else None
184
+ if pending.answered or self.pending_by_operation_id.get(operation_id) is not pending:
185
+ return
186
+ previous = self.window_by_operation_id.pop(operation_id, None)
187
+ if previous is not None:
188
+ previous.close_after_answer()
189
+ previous.deleteLater()
190
+ # A message always hands its text to Git: saving it while the rebase stays paused would mean nothing.
191
+ validation = EditorValidation.CONTINUE if pending.request.kind == EditorRequestKind.MESSAGE else expected.validation
192
+ window = RebaseEditorWindow(pending.request.kind, validation, content.text if content is not None else "", expected.context.repository_dir, expected.parent_window)
193
+ if pending.request.kind == EditorRequestKind.MESSAGE:
194
+ window.show_context(message_context_lines(expected.context.repository_dir, read.commits if read is not None else ()))
195
+ if error is not None:
196
+ window.set_read_only(RebaseEditorText.READ_FAILED.value.format(button=validation.label, cause=edit_failure_reason(error)))
197
+ elif content.read_only_cause is not None:
198
+ window.set_read_only(content.read_only_cause.label)
199
+ error_text = self.error_text_by_operation_id.pop(operation_id, "")
200
+ if error_text:
201
+ # The problem the command was started to fix comes first; a reason the text is read-only stays under it.
202
+ error_lines = [error_text]
203
+ if window.error_label.text():
204
+ error_lines.append(window.error_label.text())
205
+ window.show_error(LINE_SEPARATOR.join(error_lines))
206
+ self.opening_error_text_by_operation_id[operation_id] = window.error_label.text()
207
+ if content is not None:
208
+ self.content_by_operation_id[operation_id] = content
209
+ if read is not None and read.rule is not None:
210
+ self.rule_by_operation_id[operation_id] = read.rule
211
+ self.window_by_operation_id[operation_id] = window
212
+ window.validate_requested.connect(lambda: self._validate(operation_id, window))
213
+ window.save_requested.connect(lambda: self._save(operation_id, window))
214
+ window.abort_requested.connect(lambda: self._abort(operation_id, window))
215
+ window.present()
216
+ self.window_shown.emit(pending.request, window)
217
+
218
+ def _served(self, operation_id: str, window: RebaseEditorWindow) -> PendingEdit | None:
219
+ """Answer the request this window still serves, or nothing once it was answered, lost or replaced."""
220
+ pending = self.pending_by_operation_id.get(operation_id)
221
+ if pending is None or pending.answered or self.window_by_operation_id.get(operation_id) is not window:
222
+ return None
223
+ return pending
224
+
225
+ def _validate(self, operation_id: str, window: RebaseEditorWindow) -> None:
226
+ """Write the edited text, then hand it to Git; a file that cannot be written back goes to Git unchanged.
227
+
228
+ A commit message Git would find empty under the repository's settings is refused: the window stays open with
229
+ the reason and Git keeps waiting, since Git would abort the commit and pause the rebase.
230
+ """
231
+ pending = self._served(operation_id, window)
232
+ if pending is None:
233
+ return
234
+ content = self.content_by_operation_id.get(operation_id)
235
+ if content is None or content.read_only_cause is not None:
236
+ self._answer(operation_id, pending, window, EditorAnswer.CONTINUE)
237
+ return
238
+ expected = self.expected_by_operation_id.get(operation_id)
239
+ if expected is None:
240
+ return
241
+ text = window.edited_text()
242
+ rule = self.rule_by_operation_id.get(operation_id)
243
+ if rule is not None and message_is_empty(text, rule):
244
+ window.show_error(RebaseEditorText.EMPTY_MESSAGE.value)
245
+ return
246
+ window.set_busy(True)
247
+ self.worktree_service.run(expected.context, expected.git_filename, self._write(content, text),
248
+ lambda written, error: self._written(operation_id, pending, window, written, error))
249
+
250
+ def _write(self, content: EditFileContent, text: str) -> Generator:
251
+ """Write the edited text off the Qt thread."""
252
+ return (yield FileTask(lambda: write_edit_file(content, text)))
253
+
254
+ def _written(self, operation_id: str, pending: PendingEdit, window: RebaseEditorWindow, written: EditFileContent | None, error: Exception | None) -> None:
255
+ """Hand a saved text to Git, or keep the window open with the cause when saving failed: Git never receives an unsaved text."""
256
+ if pending.answered or self.pending_by_operation_id.get(operation_id) is not pending:
257
+ return
258
+ if error is not None or written is None:
259
+ window.show_error(save_failure_text(error))
260
+ window.set_busy(False)
261
+ return
262
+ self.content_by_operation_id[operation_id] = written
263
+ self._answer(operation_id, pending, window, EditorAnswer.CONTINUE)
264
+
265
+ def _save(self, operation_id: str, window: RebaseEditorWindow) -> None:
266
+ """Write the edited text without answering Git, as Ctrl+S asks: the window stays open and Git keeps waiting.
267
+
268
+ A file shown read-only, or one that could not be read, has nothing to save. The window stays busy during the
269
+ write, so neither a second Ctrl+S nor Continue starts another one.
270
+ """
271
+ pending = self._served(operation_id, window)
272
+ if pending is None:
273
+ return
274
+ content = self.content_by_operation_id.get(operation_id)
275
+ expected = self.expected_by_operation_id.get(operation_id)
276
+ if content is None or content.read_only_cause is not None or expected is None:
277
+ return
278
+ window.set_busy(True)
279
+ self.worktree_service.run(expected.context, expected.git_filename, self._write(content, window.edited_text()),
280
+ lambda written, error: self._saved(operation_id, pending, window, written, error))
281
+
282
+ def _saved(self, operation_id: str, pending: PendingEdit, window: RebaseEditorWindow, written: EditFileContent | None, error: Exception | None) -> None:
283
+ """Keep the saved text as the one Git's file now holds, or show why it could not be saved; Git receives no answer either way.
284
+
285
+ A successful save clears only what the window showed itself, a failed save or an empty message, and shows again
286
+ the error the window opened with: Git has not checked the saved text yet, so its refusal still says what to fix.
287
+ """
288
+ if pending.answered or self.pending_by_operation_id.get(operation_id) is not pending:
289
+ return
290
+ window.set_busy(False)
291
+ if error is not None or written is None:
292
+ window.show_error(save_failure_text(error))
293
+ return
294
+ # The saved text and its new file identity: Continue on the same text writes nothing more, and a later write checks this file.
295
+ self.content_by_operation_id[operation_id] = written
296
+ window.show_error(self.opening_error_text_by_operation_id.get(operation_id, ""))
297
+
298
+ def _abort(self, operation_id: str, window: RebaseEditorWindow) -> None:
299
+ """Answer Abort to Git for the request this window serves."""
300
+ pending = self._served(operation_id, window)
301
+ if pending is None:
302
+ return
303
+ self._answer(operation_id, pending, window, EditorAnswer.ABORT)
304
+
305
+ def _answer(self, operation_id: str, pending: PendingEdit, window: RebaseEditorWindow, answer: EditorAnswer) -> None:
306
+ """Give Git its one answer, then close the window; the window stays known until the next edit or the end of the operation."""
307
+ self.pending_by_operation_id.pop(operation_id, None)
308
+ pending.answer(answer)
309
+ window.close_after_answer()