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
File without changes
@@ -0,0 +1,88 @@
1
+ """Select commit verification candidates without reading otherwise clean file contents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ctypes
6
+ import os
7
+ from ctypes import wintypes
8
+ from typing import TYPE_CHECKING
9
+
10
+ from gitpane.worktree.worktree_file import stat_identity, validate_destination
11
+
12
+ if TYPE_CHECKING:
13
+ from gitpane.history.repository_value import RepositoryContext
14
+
15
+ FILE_READ_ATTRIBUTES = 0x0080
16
+ FILE_SHARE_READ_WRITE_DELETE = 0x0007
17
+ OPEN_EXISTING = 3
18
+ FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
19
+ FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
20
+ FILE_BASIC_INFO_CLASS = 0
21
+ INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
22
+
23
+
24
+ class _FileBasicInformation(ctypes.Structure):
25
+ """Match the Windows structure that distinguishes change time from creation time."""
26
+
27
+ _fields_ = [("creation_time", ctypes.c_longlong), ("last_access_time", ctypes.c_longlong), ("last_write_time", ctypes.c_longlong),
28
+ ("change_time", ctypes.c_longlong), ("file_attributes", wintypes.DWORD)]
29
+
30
+
31
+ if os.name == "nt":
32
+ _kernel = ctypes.WinDLL("kernel32", use_last_error=True)
33
+ _kernel.CreateFileW.argtypes = [wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, ctypes.c_void_p, wintypes.DWORD, wintypes.DWORD, wintypes.HANDLE]
34
+ _kernel.CreateFileW.restype = wintypes.HANDLE
35
+ _kernel.GetFileInformationByHandleEx.argtypes = [wintypes.HANDLE, ctypes.c_int, ctypes.c_void_p, wintypes.DWORD]
36
+ _kernel.GetFileInformationByHandleEx.restype = wintypes.BOOL
37
+ _kernel.CloseHandle.argtypes = [wintypes.HANDLE]
38
+ _kernel.CloseHandle.restype = wintypes.BOOL
39
+
40
+
41
+ def _windows_change_time(filename: str) -> int:
42
+ """Read NTFS change time because Windows stat ctime can still mean creation time."""
43
+ handle = _kernel.CreateFileW(filename, FILE_READ_ATTRIBUTES, FILE_SHARE_READ_WRITE_DELETE, None, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, None)
44
+ if handle == INVALID_HANDLE_VALUE:
45
+ raise ctypes.WinError(ctypes.get_last_error())
46
+ try:
47
+ information = _FileBasicInformation()
48
+ if not _kernel.GetFileInformationByHandleEx(handle, FILE_BASIC_INFO_CLASS, ctypes.byref(information), ctypes.sizeof(information)):
49
+ raise ctypes.WinError(ctypes.get_last_error())
50
+ return information.change_time
51
+ finally:
52
+ _kernel.CloseHandle(handle)
53
+
54
+
55
+ def read_path_metadata(filename: str) -> tuple[int, ...] | None:
56
+ """Read the identity and change time of one absolute path without opening its content.
57
+
58
+ Answers None only when the path does not exist; an access error or a path that changes while it is read is
59
+ propagated, never replaced by the modification time alone. Git can miss a same-size write whose modification time
60
+ was put back; the Windows ChangeTime survives that restoration, and POSIX stat already supplies the change ctime.
61
+ Opening a Windows handle for these attributes reads no byte of the file.
62
+ """
63
+ try:
64
+ identity = stat_identity(os.lstat(filename))
65
+ except FileNotFoundError:
66
+ return None
67
+ change_time = _windows_change_time(filename) if os.name == "nt" else os.lstat(filename).st_ctime_ns
68
+ if identity != stat_identity(os.lstat(filename)):
69
+ raise ValueError("A file changed while its metadata was read")
70
+ return (*identity, change_time)
71
+
72
+
73
+ def read_commit_metadata(context: RepositoryContext, paths: tuple[bytes, ...]) -> dict[bytes, tuple[int, ...] | None]:
74
+ """Observe tracked-file identities and change times; propagate inaccessible or unstable reads."""
75
+ metadata_by_path: dict[bytes, tuple[int, ...] | None] = {}
76
+ for path in paths:
77
+ if path in metadata_by_path:
78
+ continue
79
+ filename, parents = validate_destination(context, path, allow_missing_parents=True)
80
+ metadata = read_path_metadata(filename)
81
+ if metadata is None:
82
+ metadata_by_path[path] = None
83
+ continue
84
+ _filename, after_parents = validate_destination(context, path, allow_missing_parents=True)
85
+ if parents != after_parents:
86
+ raise ValueError("Working directory changed during commit metadata verification")
87
+ metadata_by_path[path] = metadata
88
+ return metadata_by_path
@@ -0,0 +1,276 @@
1
+ """Keep a successful local commit separate from its later network delivery."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from dataclasses import dataclass, replace
7
+ from typing import TYPE_CHECKING
8
+
9
+ from PySide6.QtCore import QObject, Signal
10
+
11
+ from gitpane.account.authentication import transfer_account
12
+ from gitpane.config import LOCAL_PREFIX
13
+ from gitpane.history.file_history import WindowCapability
14
+ from gitpane.operation.console_line import NOT_STARTED_STATUS_SET
15
+ from gitpane.operation.operation_status import OperationStatus
16
+ from gitpane.operation.repository_operation import OperationIntent
17
+ from gitpane.process.process_runner import READ_PROFILE
18
+ from gitpane.reference.reference_edit import reference_signature
19
+ from gitpane.transport.branch_exchange import TransportRequest
20
+ from gitpane.transport.refusal_notice import failure_headline, failure_summary
21
+ from gitpane.transport.remote_config import config_signature, default_push_destination_name, fetch_remote_name
22
+ from gitpane.worktree.command_sequence import FileTask, GitCommand
23
+
24
+ if TYPE_CHECKING:
25
+ from collections.abc import Callable, Generator
26
+
27
+ from gitpane.commit.commit_write import CommitOutcome, CommitRequest
28
+ from gitpane.history.repository_value import RepositoryContext
29
+ from gitpane.transport.branch_exchange import PushTarget, TransportOutcome, TransportRun, TransportService
30
+ from gitpane.transport.remote_config import RemoteSnapshot
31
+
32
+ COMMIT_PUSH_CHOOSE = "Commit created. Choose a push destination."
33
+ COMMIT_PUSH_FAILED = "Commit created, but push failed. The local commit is kept. Review the details and retry the push."
34
+ NO_PROPOSED_REMOTE_TEXT = "Push proposes no remote for this branch."
35
+ NO_PROPOSED_DESTINATION_TEXT = "Push proposes no remote branch for this branch."
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class PushDestination:
40
+ """Retain the remote, branch and exact endpoint the toolbar Push window proposes."""
41
+
42
+ remote_name: str
43
+ destination_ref: str
44
+ target: PushTarget
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class CommitPushIntent:
49
+ """Bind the sole commit request to its original branch, config and window authority.
50
+
51
+ ``reason`` explains why the push did not happen by itself; ``push_failed`` tells that it comes from a push that ran
52
+ and failed, so the Push window shows it as a failure rather than as a routing note. ``reason_headline`` is the
53
+ sentence naming the address and account a refused push used; it is kept apart from ``reason`` because the Push
54
+ window filters the reason but not this sentence, which holds no secret.
55
+ """
56
+
57
+ operation: OperationIntent
58
+ commit_request: CommitRequest
59
+ source_ref: str = ""
60
+ snapshot: RemoteSnapshot | None = None
61
+ destination: PushDestination | None = None
62
+ created_oid: str = ""
63
+ reason: str = ""
64
+ push_failed: bool = False
65
+ reason_headline: str = ""
66
+
67
+
68
+ class CommitPushService(QObject):
69
+ """Own the chain beyond staging closure and never replay a successful commit."""
70
+
71
+ choose_requested = Signal(object, str)
72
+ push_started = Signal(object)
73
+ # The commit exists and has a complete destination: the controller decides on the tracking reference from the
74
+ # capture taken before the commit, asking the user when nothing decides it already, then calls push_after_decision.
75
+ tracking_decision_requested = Signal(object)
76
+
77
+ def __init__(self, transport: TransportService, *, current_operation: Callable[[OperationIntent], OperationIntent | None], parent: QObject | None = None) -> None:
78
+ """Inject transport and the live management authority for each independent repository."""
79
+ super().__init__(parent)
80
+ self.transport = transport
81
+ self.current_operation = current_operation
82
+ self.intent_by_id: dict[str, CommitPushIntent] = {}
83
+ self.intent_by_push_id: dict[str, CommitPushIntent] = {}
84
+ self.finished_id_set: set[str] = set()
85
+ transport.finished.connect(self._push_finished)
86
+
87
+ def resolve_destination(self, snapshot: RemoteSnapshot, source_ref: str) -> PushDestination:
88
+ """Pick the remote, branch and address the toolbar Push window proposes for the current branch.
89
+
90
+ Commit & push sends exactly where Push would send. Git's own push defaults (push.default, pushRemote,
91
+ remote.pushDefault, a configured push mapping) are not followed, as Push does not follow them. An upstream is set
92
+ only as Push sets it: after the tracking question answered Yes, or a kept Always add, and once the Push is
93
+ verified. Without a complete proposal (no remote, several addresses, no remote branch), the created commit
94
+ waits for a choice in the Push window.
95
+ """
96
+ if not source_ref.startswith(LOCAL_PREFIX) or source_ref not in snapshot.tracking_by_ref:
97
+ raise ValueError("A current local branch is required for a default push.")
98
+ remote_name = fetch_remote_name(snapshot, source_ref)
99
+ entry = snapshot.entry_by_name.get(remote_name)
100
+ if entry is None:
101
+ raise ValueError(NO_PROPOSED_REMOTE_TEXT)
102
+ if len(entry.push_urls) != 1:
103
+ raise ValueError("The default Push remote needs one unambiguous endpoint.")
104
+ endpoint, = entry.push_urls
105
+ destination_name = default_push_destination_name(snapshot, source_ref, remote_name)
106
+ if not destination_name:
107
+ raise ValueError(NO_PROPOSED_DESTINATION_TEXT)
108
+ return PushDestination(remote_name, LOCAL_PREFIX + destination_name, self.transport.capture_push_target(snapshot, remote_name, endpoint))
109
+
110
+ def capture(self, operation: OperationIntent, request: CommitRequest, complete: Callable[[CommitPushIntent | None, Exception | None], None]) -> None:
111
+ """Read routing before the sole commit submission without contacting any server."""
112
+ if (operation.capability != WindowCapability.MANAGE or self.current_operation(operation) is None or request.operation_id in self.intent_by_id
113
+ or request.snapshot.context != operation.context or request.snapshot.git_filename != operation.git_filename or request.key != operation.key):
114
+ complete(None, ValueError("Commit & push is no longer authorized."))
115
+ return
116
+ intent = CommitPushIntent(operation, request)
117
+ self.intent_by_id[request.operation_id] = intent
118
+ captured_ref: str | None = None
119
+
120
+ def read() -> Generator:
121
+ """Capture HEAD and its destination in one routing capture under the original index authority.
122
+
123
+ The capture observes HEAD between its own reference signatures. When it fails, HEAD is read alone, only while
124
+ the operation is still current, so a routing error still allows the commit with a manual push choice.
125
+ """
126
+ nonlocal captured_ref
127
+ try:
128
+ snapshot = yield from self.transport.remote.capture(operation.context, operation.git_filename, operation.generation)
129
+ except (ValueError, OSError, RuntimeError):
130
+ if self.current_operation(operation) is None:
131
+ raise
132
+ captured_ref = yield from self._read_current_head(operation.context)
133
+ raise
134
+ if snapshot.head_ref is None:
135
+ raise ValueError("The current branch could not be captured.")
136
+ source_ref = snapshot.head_ref
137
+ captured_ref = source_ref
138
+ reason = ""
139
+ destination = None
140
+ try:
141
+ destination = self.resolve_destination(snapshot, source_ref)
142
+ except (ValueError, OSError, RuntimeError) as error:
143
+ reason = str(error)
144
+ return replace(intent, commit_request=replace(request, expected_head_ref=source_ref), source_ref=source_ref, snapshot=snapshot, destination=destination, reason=reason)
145
+
146
+ def captured(value: CommitPushIntent | None, error: Exception | None) -> None:
147
+ """Preserve a recoverable routing failure while rejecting revoked commit authority."""
148
+ if self.current_operation(operation) is None:
149
+ complete(None, ValueError("Commit & push is no longer authorized."))
150
+ return
151
+ if captured_ref is None:
152
+ complete(None, error or ValueError("The current branch could not be captured."))
153
+ return
154
+ value = value or replace(intent, commit_request=replace(request, expected_head_ref=captured_ref), source_ref=captured_ref, reason=str(error))
155
+ self.intent_by_id[request.operation_id] = value
156
+ complete(value, None)
157
+
158
+ self.transport.remote.worktree_service.run(operation.context, operation.git_filename, read(), captured)
159
+
160
+ def commit_finished(self, outcome: CommitOutcome) -> None:
161
+ """Accept only the successful outcome's observed OID, once, after staging closes."""
162
+ intent = self.intent_by_id.get(outcome.operation_id)
163
+ if intent is None or outcome.operation_id in self.finished_id_set:
164
+ return
165
+ if outcome.key != intent.commit_request.key or (outcome.after is not None and outcome.after.snapshot.context != intent.operation.context):
166
+ return
167
+ self.finished_id_set.add(outcome.operation_id)
168
+ if outcome.status != OperationStatus.SUCCESS or outcome.after is None or not outcome.after.snapshot.head_oid:
169
+ return
170
+ intent = replace(intent, created_oid=outcome.after.snapshot.head_oid)
171
+ self.intent_by_id[outcome.operation_id] = intent
172
+ if intent.destination is None or intent.snapshot is None:
173
+ self.choose_requested.emit(intent, COMMIT_PUSH_CHOOSE)
174
+ return
175
+ self.tracking_decision_requested.emit(intent)
176
+
177
+ def push_after_decision(self, intent: CommitPushIntent, add_tracking: bool, not_sent: Callable[[], None]) -> None:
178
+ """Read the configuration, HEAD and destination again once the tracking reference is decided, then push the created commit.
179
+
180
+ The decision can wait on the user as long as they like, so this read comes after it and right before the Push:
181
+ a remote address, account or branch changed meanwhile sends the same created commit to the Push window instead.
182
+ ``add_tracking`` links the branch to the remote branch once the Push is verified. ``not_sent`` runs when the
183
+ window's management authority or the captured commit is gone, before or after the read, so nothing is pushed.
184
+ """
185
+ operation = self.current_operation(intent.operation)
186
+ if operation is None:
187
+ not_sent()
188
+ return
189
+
190
+ def read() -> Generator:
191
+ """Reject changed config or branch identity while retaining the created commit."""
192
+ if config_signature(intent.snapshot.context, intent.snapshot.values) != intent.snapshot.signature:
193
+ raise ValueError("Push configuration changed after the commit.")
194
+ snapshot = yield from self.transport.remote.capture(operation.context, operation.git_filename, operation.generation)
195
+ branch = snapshot.tracking_by_ref.get(intent.source_ref)
196
+ # The branch name comes from HEAD itself: another branch can hold the same commit.
197
+ if (snapshot.head_ref is None or snapshot.head_ref != intent.source_ref or snapshot.head_oid != intent.created_oid
198
+ or branch is None or branch.oid != intent.created_oid):
199
+ raise ValueError("HEAD changed after the commit.")
200
+ if snapshot.values != intent.snapshot.values or self.resolve_destination(snapshot, intent.source_ref) != intent.destination:
201
+ raise ValueError("The default push destination changed after the commit.")
202
+ return snapshot
203
+
204
+ def ready(snapshot: RemoteSnapshot | None, error: Exception | None) -> None:
205
+ """Route a stale or failed destination to the same immutable commit's Push form, else push to the destination just read."""
206
+ if error is not None:
207
+ self.choose_requested.emit(replace(intent, reason=str(error)), COMMIT_PUSH_CHOOSE)
208
+ return
209
+ destination = intent.destination
210
+ if self.retry_push(intent, snapshot, destination.remote_name, destination.target.endpoint, destination.destination_ref, destination.target,
211
+ add_tracking=add_tracking) is None:
212
+ not_sent()
213
+
214
+ self.transport.remote.worktree_service.run(operation.context, operation.git_filename, read(), ready)
215
+
216
+ def _read_current_head(self, context: RepositoryContext) -> Generator:
217
+ """Read the branch HEAD is on, alone, after a failed routing capture, and only if no reference moved meanwhile.
218
+
219
+ Answers the full branch name, or an empty string for a detached HEAD. A failed or cut answer, or references
220
+ that moved during the read, refuse the commit rather than guessing which branch it would land on.
221
+ """
222
+ before = yield FileTask(lambda: reference_signature(context))
223
+ output = yield GitCommand(["symbolic-ref", "--quiet", "HEAD"], accepted_codes=(0, 1), profile=READ_PROFILE)
224
+ after = yield FileTask(lambda: reference_signature(context))
225
+ if before != after:
226
+ raise ValueError("The current branch changed while capturing Commit & push.")
227
+ if output and not output.endswith(b"\n"):
228
+ raise ValueError("Git returned an incomplete current branch.")
229
+ return output.decode("utf-8", "surrogateescape").strip()
230
+
231
+ def retry_push(self, intent: CommitPushIntent, snapshot: RemoteSnapshot, remote_name: str, endpoint: str, destination_ref: str, target: PushTarget,
232
+ force: bool = False, *, add_tracking: bool = False) -> TransportRun | None:
233
+ """Submit only the already-created commit under freshly selected destination authority, the way the toolbar Push sends.
234
+
235
+ ``add_tracking`` links the branch to the remote branch once the Push is verified, as the tracking answer asked.
236
+ """
237
+ operation = self.current_operation(intent.operation)
238
+ captured = self.intent_by_id.get(intent.commit_request.operation_id)
239
+ if (not intent.created_oid or operation is None or operation.capability != WindowCapability.MANAGE or captured is None
240
+ or captured.created_oid != intent.created_oid or captured.commit_request != intent.commit_request or captured.operation != intent.operation or captured.source_ref != intent.source_ref):
241
+ return None
242
+ operation = replace(operation, operation_id=uuid.uuid4().hex, operation="Push", targets=(intent.created_oid, destination_ref, endpoint))
243
+ snapshot = replace(snapshot, generation=operation.generation)
244
+ self.intent_by_push_id[operation.operation_id] = intent
245
+ run = self.transport.push(TransportRequest(operation, mode="push", snapshot=snapshot, remote_name=remote_name, source_ref=intent.source_ref,
246
+ source_oid=intent.created_oid, destination_ref=destination_ref, endpoint=endpoint, push_target=target, force=force,
247
+ immutable_commit=True, add_tracking=add_tracking))
248
+ self.push_started.emit(run)
249
+ return run
250
+
251
+ def _push_finished(self, outcome: TransportOutcome) -> None:
252
+ """Offer a push-only recovery after an unsuccessful push without touching the local commit.
253
+
254
+ The note under the created commit is red only when Git really contacted the remote and the push failed there,
255
+ a refused authentication on the first contact included. A push refused before any contact, for example by a
256
+ destination name Git rejects, only asks to choose a destination.
257
+
258
+ The reason shown is the same short summary as the Push console's: a sentence naming the address and the account
259
+ used when the server refused it, then Git's own lines. An account already known to have lost its stored secret
260
+ is said to need a reconnect there too; one found out later is only marked in the Push console and in Settings.
261
+ """
262
+ intent = self.intent_by_push_id.get(outcome.operation_id)
263
+ if intent is not None and outcome.status != OperationStatus.SUCCESS:
264
+ run = self.transport.run_by_id[outcome.operation_id]
265
+ # A recorded step whose process never started, refused by its launch check for instance, contacted no server;
266
+ # checks refused before any process record no step at all.
267
+ attempted = False
268
+ for step in run.steps:
269
+ if step.process_status not in NOT_STARTED_STATUS_SET:
270
+ attempted = True
271
+ break
272
+ reason, headline = intent.reason, ""
273
+ if outcome.diagnostic:
274
+ reason = failure_summary(outcome.diagnostic)
275
+ headline = failure_headline(outcome.diagnostic, transfer_account(outcome.selection, outcome.endpoint, self.transport.authentication.missing_secret_namespace_set))
276
+ self.choose_requested.emit(replace(intent, reason=reason, reason_headline=headline, push_failed=attempted), COMMIT_PUSH_FAILED if attempted else COMMIT_PUSH_CHOOSE)