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,224 @@
1
+ """Keep the file Git handed to the rebase editor byte-exact from its read to the write of the user's text."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import codecs
6
+ import os
7
+ import stat
8
+ import tempfile
9
+ from dataclasses import dataclass
10
+ from enum import Enum
11
+
12
+ from gitpane.config import QT_PARAGRAPH_SEPARATOR
13
+ from gitpane.system.path_identity import link_resolved_path
14
+ from gitpane.worktree.worktree_file import SINGLE_LINK_COUNT
15
+
16
+ # Git writes its instruction list and its messages in UTF-8 unless the repository asks for another commit encoding.
17
+ EDIT_FILE_CODEC = "utf-8"
18
+ EDIT_FILE_TEMPORARY_PREFIX = "gitpane-rebase-edit-"
19
+ # The editor widget turns a carriage return and a paragraph separator into a plain line break, and drops a byte order
20
+ # mark at the start of the document: text holding one of them would come back with bytes the user never changed.
21
+ CARRIAGE_RETURN_CHARACTER = "\r"
22
+ BYTE_ORDER_MARK_CHARACTER = "\ufeff"
23
+
24
+
25
+ class ReadOnlyCause(Enum):
26
+ """Why a file Git waits for can be shown but not written back; each value is the line the editor window shows above it."""
27
+
28
+ UNDECODABLE = "Read-only: the file is in an encoding GitPane cannot read back exactly."
29
+ CARRIAGE_RETURN = "Read-only: the file has carriage returns GitPane cannot keep."
30
+ BYTE_ORDER_MARK = "Read-only: the file starts with a byte order mark GitPane cannot keep."
31
+ PARAGRAPH_SEPARATOR = "Read-only: the file holds a paragraph separator GitPane cannot keep."
32
+
33
+ @property
34
+ def label(self) -> str:
35
+ """Answer the line the editor window shows above a file it keeps read-only."""
36
+ return self.value
37
+
38
+
39
+ class EditFileFailure(Enum):
40
+ """Why the file Git waits for could not be read or written; each value is the fixed start of the cause shown to the user."""
41
+
42
+ CHANGED = "The file Git is waiting for changed."
43
+ # The codec is named, never the character it cannot hold.
44
+ UNENCODABLE = "The text cannot be written in {codec}."
45
+ CREATE = "Cannot create the new file next to the file Git is waiting for:"
46
+ WRITE = "Cannot write the new file:"
47
+ REPLACE = "Cannot replace the file Git is waiting for:"
48
+ # Out of reach in practice, os.name being posix or nt, both in the table of open flags.
49
+ UNKNOWN_SYSTEM = "GitPane does not know how to open the file Git is waiting for on this system."
50
+
51
+
52
+ class EditFileWriteError(OSError):
53
+ """A write of the file Git waits for that did not happen; the file is left as it was.
54
+
55
+ Its cause is a readable sentence, never the content of the file, and is also the error's strerror so that code
56
+ reading system errors shows it as is.
57
+ """
58
+
59
+ def __init__(self, cause: str, error_number: int | None = None) -> None:
60
+ super().__init__(error_number, cause)
61
+ self.cause = cause
62
+
63
+ def __str__(self) -> str:
64
+ return self.cause
65
+
66
+
67
+ def _system_failure(failure: EditFileFailure, error: OSError) -> EditFileWriteError:
68
+ """Turn the system error of one write step into a write error naming the step, then the system's reason."""
69
+ return EditFileWriteError(failure.value + " " + (error.strerror or str(error)), error.errno)
70
+
71
+
72
+ def read_only_cause(text: str) -> ReadOnlyCause | None:
73
+ """Answer why the editor could not give this text back unchanged, or None when it can.
74
+
75
+ The editor widget changes a carriage return into a line break, cannot tell a paragraph separator from a line break
76
+ once it holds the text, and drops a byte order mark at the start of the document; any of them would rewrite bytes
77
+ the user never touched.
78
+ """
79
+ if text.startswith(BYTE_ORDER_MARK_CHARACTER):
80
+ return ReadOnlyCause.BYTE_ORDER_MARK
81
+ if CARRIAGE_RETURN_CHARACTER in text:
82
+ return ReadOnlyCause.CARRIAGE_RETURN
83
+ if QT_PARAGRAPH_SEPARATOR in text:
84
+ return ReadOnlyCause.PARAGRAPH_SEPARATOR
85
+ return None
86
+
87
+
88
+ @dataclass(frozen=True)
89
+ class EditFileContent:
90
+ """The text of one file Git waits for, with what writing it back needs: its codec, the identity it was read from and its permission bits."""
91
+
92
+ filename: str
93
+ text: str
94
+ codec: str
95
+ identity: tuple[int, int]
96
+ mode: int
97
+ read_only_cause: ReadOnlyCause | None = None
98
+
99
+
100
+ EDIT_FILE_OPEN_FLAG_NAMES_BY_OS_NAME: dict[str, tuple[str, ...]] = {
101
+ # Binary mode, so the C library of Windows neither turns CR LF into LF nor stops reading at the first Ctrl+Z byte.
102
+ "nt": ("O_RDONLY", "O_BINARY"),
103
+ # The path can become a FIFO after Git wrote it: reading must never wait for a writer, nor follow a link put in place of the file.
104
+ "posix": ("O_RDONLY", "O_NONBLOCK", "O_NOFOLLOW"),
105
+ }
106
+
107
+
108
+ def edit_file_open_flags(os_name: str) -> int:
109
+ """Answer the flags, for the system named like os.name, that open the file Git waits for byte for byte and without any wait.
110
+
111
+ The file is read in binary mode on Windows, and without waiting on a FIFO or following a replacement link on a
112
+ POSIX system. The flag names come from the table by system and are resolved on the os module of the running Python,
113
+ at the call: the function is meant to be called with os.name, and the name of another system can miss one of its
114
+ flags there (AttributeError), which is why the tests put the Windows flag in place. A system the table does not
115
+ know raises OSError.
116
+ """
117
+ flag_names = EDIT_FILE_OPEN_FLAG_NAMES_BY_OS_NAME.get(os_name)
118
+ if flag_names is None:
119
+ raise OSError(EditFileFailure.UNKNOWN_SYSTEM.value)
120
+ # Each flag only exists in the os module of its own system, and the function is called with os.name; reading them
121
+ # at the call, rather than at import, also lets a test put the Windows flag in place.
122
+ flags = 0
123
+ for flag_name in flag_names:
124
+ flags |= getattr(os, flag_name)
125
+ return flags
126
+
127
+
128
+ def _same_single_file(status: os.stat_result, identity: tuple[int, int] | None) -> bool:
129
+ """Tell whether a status is the regular file with one link that the bridge validated."""
130
+ return stat.S_ISREG(status.st_mode) and status.st_nlink == SINGLE_LINK_COUNT and identity == (status.st_dev, status.st_ino)
131
+
132
+
133
+ def _decoded(data: bytes, fallback_codec: str) -> tuple[str, str] | None:
134
+ """Decode the bytes in strict UTF-8, else in the fallback text codec, answering the text and the codec's normalized name.
135
+
136
+ None means neither decodes: no known text codec is given, or the bytes are not valid in it.
137
+ """
138
+ try:
139
+ return data.decode(EDIT_FILE_CODEC), codecs.lookup(EDIT_FILE_CODEC).name
140
+ except UnicodeDecodeError:
141
+ pass
142
+ if not fallback_codec:
143
+ return None
144
+ try:
145
+ codec = codecs.lookup(fallback_codec).name
146
+ # bytes.decode refuses a codec that is not a text encoding (rot13, base64) with LookupError.
147
+ return data.decode(codec), codec
148
+ except (LookupError, UnicodeDecodeError):
149
+ return None
150
+
151
+
152
+ def read_edit_file(filename: str, identity: tuple[int, int] | None, fallback_codec: str = "") -> EditFileContent:
153
+ """Read the file Git waits for, refusing any other file than the one the bridge validated.
154
+
155
+ The bytes are decoded in strict UTF-8, else with the fallback codec (the repository's commit encoding, in which Git
156
+ writes a commit message). A file neither decodes is still shown, decoded with replacement characters, and a text the
157
+ editor could not give back unchanged is shown too, but both are marked read-only: writing them back would change
158
+ bytes the user never touched.
159
+ """
160
+ descriptor = os.open(filename, edit_file_open_flags(os.name))
161
+ with os.fdopen(descriptor, "rb") as stream:
162
+ opened = os.fstat(stream.fileno())
163
+ if not _same_single_file(opened, identity):
164
+ raise OSError(EditFileFailure.CHANGED.value)
165
+ data = stream.read()
166
+ mode = stat.S_IMODE(opened.st_mode)
167
+ file_identity = (opened.st_dev, opened.st_ino)
168
+ decoded = _decoded(data, fallback_codec)
169
+ if decoded is None:
170
+ return EditFileContent(filename, data.decode(EDIT_FILE_CODEC, errors="replace"), EDIT_FILE_CODEC, file_identity, mode, ReadOnlyCause.UNDECODABLE)
171
+ text, codec = decoded
172
+ return EditFileContent(filename, text, codec, file_identity, mode, read_only_cause(text))
173
+
174
+
175
+ def _verify_unchanged(content: EditFileContent) -> None:
176
+ """Refuse to replace the file when its path no longer names the single regular file that was read: a link, another file, a second link or no file at all."""
177
+ try:
178
+ current = os.lstat(content.filename)
179
+ except OSError:
180
+ raise EditFileWriteError(EditFileFailure.CHANGED.value) from None
181
+ if not _same_single_file(current, content.identity) or link_resolved_path(content.filename) != os.path.normcase(content.filename):
182
+ raise EditFileWriteError(EditFileFailure.CHANGED.value)
183
+
184
+
185
+ def write_edit_file(content: EditFileContent, text: str) -> EditFileContent:
186
+ """Replace the file Git waits for with the user's text in one step, or leave it untouched when the text did not change.
187
+
188
+ An unchanged text writes nothing, so Git reads back exactly the bytes it wrote. A changed text goes to a private
189
+ file of the same folder, which takes the permission bits of the original and replaces it only once it is complete
190
+ on disk and the original is proven to be the file that was read. Any failure raises EditFileWriteError with a
191
+ readable cause, leaves the original as it was and removes the private file. A read-only content raises ValueError.
192
+ """
193
+ if text == content.text:
194
+ return content
195
+ if content.read_only_cause is not None:
196
+ raise ValueError(content.read_only_cause.label)
197
+ try:
198
+ data = text.encode(content.codec)
199
+ except UnicodeEncodeError:
200
+ # The encoding error holds the whole text; dropping it keeps the user's text out of any diagnostic.
201
+ raise EditFileWriteError(EditFileFailure.UNENCODABLE.value.format(codec=content.codec)) from None
202
+ try:
203
+ descriptor, candidate_filename = tempfile.mkstemp(prefix=EDIT_FILE_TEMPORARY_PREFIX, dir=os.path.dirname(content.filename))
204
+ except OSError as error:
205
+ raise _system_failure(EditFileFailure.CREATE, error) from error
206
+ try:
207
+ try:
208
+ with os.fdopen(descriptor, "wb") as stream:
209
+ os.fchmod(stream.fileno(), content.mode)
210
+ stream.write(data)
211
+ stream.flush()
212
+ os.fsync(stream.fileno())
213
+ written = os.fstat(stream.fileno())
214
+ except OSError as error:
215
+ raise _system_failure(EditFileFailure.WRITE, error) from error
216
+ _verify_unchanged(content)
217
+ try:
218
+ os.replace(candidate_filename, content.filename)
219
+ except OSError as error:
220
+ raise _system_failure(EditFileFailure.REPLACE, error) from error
221
+ finally:
222
+ if os.path.lexists(candidate_filename):
223
+ os.unlink(candidate_filename)
224
+ return EditFileContent(content.filename, text, content.codec, (written.st_dev, written.st_ino), content.mode)