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,378 @@
1
+ """Prepare history removal in storage isolated from the source repository."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import os
7
+ import tempfile
8
+ from dataclasses import replace
9
+ from typing import TYPE_CHECKING
10
+
11
+ from gitpane.config import (
12
+ EXACT_PATH_CASE_ARGUMENTS,
13
+ GIT_ALTERNATE_OBJECT_ENVIRONMENT_NAME,
14
+ GIT_MUTATION_TIMEOUT_MS,
15
+ HISTORY_REWRITE_DIAGNOSTIC_LIMIT_CHARS,
16
+ HISTORY_REWRITE_SPOOL_LIMIT_BYTES,
17
+ HISTORY_STREAM_TIMEOUT_MS,
18
+ PROCESS_CHUNK_BYTES,
19
+ SUBMODULE_RECURSION_OFF_ARGUMENTS,
20
+ )
21
+ from gitpane.history.repository_value import validate_oid
22
+ from gitpane.history_rewrite.graph import analyze_history
23
+ from gitpane.history_rewrite.guard import capture_rewrite_state, validate_branch_state, validate_history_protection, validate_initial_state, validate_rewrite_capture
24
+ from gitpane.history_rewrite.index import prepare_candidate_index
25
+ from gitpane.history_rewrite.protocol import check_cancelled, read_export, read_line, read_nul_records, read_raw_commits, write_import
26
+ from gitpane.history_rewrite.value import RewritePreparation, RewriteRefusal, RewriteTargetKind
27
+ from gitpane.process.process_runner import LOCAL_WRITE_PROFILE, READ_PROFILE
28
+ from gitpane.system.private_tree_removal import remove_private_tree
29
+ from gitpane.tool.git_version import GIT_VERSION_PREFIX, git_version_answer_line, git_version_numbers
30
+ from gitpane.worktree.command_sequence import FileTask, GitCommand
31
+
32
+ if TYPE_CHECKING:
33
+ from collections.abc import Callable, Generator
34
+ from typing import BinaryIO
35
+
36
+ from gitpane.history_rewrite.value import ExportCommit, HistoryAnalysis, RawCommit, RewriteCapture, RewriteTarget
37
+
38
+ PRIVATE_REF = b"refs/heads/gitpane-rewrite"
39
+ PRIVATE_REPOSITORY_BASENAME = "repository.git"
40
+ OWNER_BASENAME = "gitpane-history-rewrite"
41
+ OWNER_CONTENT = b"GitPane private history preparation\n"
42
+ # First Git release whose fast-export knows --signed-commits. Older releases refuse the option, but their export
43
+ # already drops commit signatures, which is what the option asks for.
44
+ SIGNED_COMMITS_EXPORT_MINIMUM_VERSION = (2, 50, 0)
45
+ SIGNED_COMMITS_STRIP_OPTION = "--signed-commits=strip"
46
+ UNREADABLE_GIT_VERSION_CAUSE = "The version of this Git could not be read, so the export options cannot be chosen; check the Git executable in Settings"
47
+ # Settings accepts a Git whose version line has no release numbers, so this cause does not send the user there.
48
+ NUMBERLESS_GIT_VERSION_CAUSE = "This Git reports its version without release numbers, so the export options cannot be chosen"
49
+
50
+
51
+ def export_arguments(git_version: str) -> list[str]:
52
+ """Build a new fast-export argument list for the Git whose `git --version` line is given.
53
+
54
+ --signed-commits=strip is only passed to a Git that knows it: an older Git refuses the option, and drops the
55
+ signatures without it, so both forms give the same stream. Two versions are refused rather than given a default
56
+ form: one that is not a version line at all, which Settings refuses too, and a version line without release
57
+ numbers, which Settings accepts. The options are never guessed: a recent Git exported without the option follows
58
+ Git's own default for signed commits, which Git may change, and rewritten commits that kept their old signatures
59
+ would carry invalid ones.
60
+ """
61
+ if not git_version.startswith(GIT_VERSION_PREFIX):
62
+ raise ValueError(UNREADABLE_GIT_VERSION_CAUSE)
63
+ version_numbers = git_version_numbers(git_version)
64
+ if version_numbers is None:
65
+ raise ValueError(NUMBERLESS_GIT_VERSION_CAUSE)
66
+ arguments = ["fast-export", "--no-data", "--no-renames", "--show-original-ids", "--reencode=no"]
67
+ if version_numbers >= SIGNED_COMMITS_EXPORT_MINIMUM_VERSION:
68
+ arguments.append(SIGNED_COMMITS_STRIP_OPTION)
69
+ arguments.extend(["--use-done-feature", "--stdin"])
70
+ return arguments
71
+
72
+
73
+ def with_git_version(capture: RewriteCapture) -> Generator[GitCommand, object, RewriteCapture]:
74
+ """Return the capture with a Git version, reading `git --version` of its Git only when the version is empty.
75
+
76
+ A non-empty version comes from the check of the chosen Git, at start or in Settings, and is never read again. An
77
+ empty one means that check has not answered yet, or failed; the export options depend on the version, so they are
78
+ never guessed: the preparation reads the version itself, once, before the first export, by the rule Settings
79
+ applies to accept a Git.
80
+ """
81
+ if capture.git_version:
82
+ return capture
83
+ output = yield GitCommand(["--version"])
84
+ return replace(capture, git_version=git_version_answer_line(output))
85
+
86
+
87
+ def _create_storage(capture: RewriteCapture, storage_root_dir: str) -> tuple[str, tuple[int, int]]:
88
+ """Keep private files outside all source locations, including linked-worktree storage."""
89
+ storage_root_dir = os.path.realpath(storage_root_dir)
90
+ for source_dir in (capture.context.repository_dir, capture.context.git_dir, capture.context.common_git_dir):
91
+ source_dir = os.path.realpath(source_dir)
92
+ try:
93
+ inside = os.path.commonpath((source_dir, storage_root_dir)) == source_dir
94
+ except ValueError:
95
+ inside = False
96
+ if inside:
97
+ raise ValueError("History preparation storage must be outside the source repository and its Git storage; choose another storage folder")
98
+ os.makedirs(storage_root_dir, mode=0o700, exist_ok=True)
99
+ operation_dir = tempfile.mkdtemp(prefix="rewrite-", dir=storage_root_dir)
100
+ try:
101
+ with open(os.path.join(operation_dir, OWNER_BASENAME), "xb") as stream:
102
+ stream.write(OWNER_CONTENT)
103
+ os.mkdir(os.path.join(operation_dir, "hooks"))
104
+ os.mkdir(os.path.join(operation_dir, PRIVATE_REPOSITORY_BASENAME))
105
+ information = os.stat(operation_dir)
106
+ return operation_dir, (information.st_dev, information.st_ino)
107
+ except OSError:
108
+ remove_private_tree(operation_dir)
109
+ raise
110
+
111
+
112
+ def discard_preparation(preparation: RewritePreparation) -> None:
113
+ """Remove only owned private storage after the sequence and its processes have drained."""
114
+ operation_dir = preparation.operation_dir
115
+ if not operation_dir or not os.path.lexists(operation_dir):
116
+ return
117
+ if os.path.islink(operation_dir):
118
+ raise ValueError("Private preparation directory was replaced; cleanup refused")
119
+ information = os.stat(operation_dir)
120
+ if preparation.operation_identity != (information.st_dev, information.st_ino):
121
+ raise ValueError("Private preparation directory identity changed; cleanup refused")
122
+ with open(os.path.join(operation_dir, OWNER_BASENAME), "rb") as stream:
123
+ if stream.read(len(OWNER_CONTENT) + 1) != OWNER_CONTENT:
124
+ raise ValueError("Private preparation ownership does not match; cleanup refused")
125
+ remove_private_tree(operation_dir)
126
+
127
+
128
+ def _write_requests(operation_dir: str, records: list[ExportCommit], target: RewriteTarget, cancelled: Callable[[], bool] | None = None) -> None:
129
+ """Create batch requests on disk so argument and input queues do not grow with history."""
130
+ with open(os.path.join(operation_dir, "commits.in"), "wb") as commits, open(os.path.join(operation_dir, "presence.in"), "wb") as presence:
131
+ for record in records:
132
+ check_cancelled(cancelled)
133
+ commits.write(record.oid.encode("ascii") + b"\n")
134
+ presence.write(record.oid.encode("ascii") + b":" + target.path + b"\0")
135
+
136
+
137
+ def _read_presence(filename: str, records: list[ExportCommit], target: RewriteTarget, cancelled: Callable[[], bool] | None = None) -> dict[str, bool]:
138
+ """Check literal file or subtree presence without confusing historical directory entries with files."""
139
+ present_by_oid = {}
140
+ with open(filename, "rb") as stream:
141
+ for record, part in zip(records, read_nul_records(stream, cancelled), strict=True):
142
+ absent = part == record.oid.encode("ascii") + b":" + target.path + b" missing"
143
+ if not absent:
144
+ oid, object_type, size = part.split(b" ")
145
+ validate_oid(oid.decode("ascii"))
146
+ if object_type not in (b"blob", b"tree", b"commit") or not size.isdigit():
147
+ raise ValueError("Invalid history path check; source unchanged")
148
+ if target.kind == RewriteTargetKind.FILE and object_type == b"tree":
149
+ absent = True
150
+ present_by_oid[record.oid] = not absent
151
+ return present_by_oid
152
+
153
+
154
+ def _analyze(capture: RewriteCapture, target: RewriteTarget, operation_dir: str, records: list[ExportCommit], cancelled: Callable[[], bool] | None = None) -> HistoryAnalysis:
155
+ """Calculate graph decisions with raw messages left in their private spool."""
156
+ with open(os.path.join(operation_dir, "raw.out"), "rb") as raw_source:
157
+ raw_by_oid = read_raw_commits(raw_source, cancelled)
158
+ validate_history_protection(capture, target, records, raw_by_oid, cancelled)
159
+ analysis = analyze_history(capture, target, records, raw_by_oid, raw_source, cancelled)
160
+ with open(os.path.join(operation_dir, "import.in"), "wb") as destination:
161
+ write_import(destination, analysis.commits, raw_source, PRIVATE_REF, cancelled)
162
+ return analysis
163
+
164
+
165
+ def _resolve_mapping(operation_dir: str, analysis: HistoryAnalysis, cancelled: Callable[[], bool] | None = None) -> dict[str, str | None]:
166
+ """Resolve import marks into actual object IDs before any proposal is complete."""
167
+ oid_by_mark = {}
168
+ with open(os.path.join(operation_dir, "marks.out"), "rb") as stream:
169
+ while line := read_line(stream, cancelled):
170
+ mark, oid = line.rstrip(b"\n").split(b" ")
171
+ oid_by_mark[mark.decode("ascii")] = validate_oid(oid.decode("ascii"))
172
+ replacement_by_oid = {}
173
+ for old_oid, replacement in analysis.replacement_by_oid.items():
174
+ check_cancelled(cancelled)
175
+ if replacement is not None and replacement.startswith(":"):
176
+ if replacement not in oid_by_mark:
177
+ raise ValueError(f"Import omitted replacement for {old_oid}; source unchanged")
178
+ replacement = oid_by_mark[replacement]
179
+ replacement_by_oid[old_oid] = replacement
180
+ return replacement_by_oid
181
+
182
+
183
+ def _spool_command(command: GitCommand, filename: str, cancelled: Callable[[], bool] | None) -> Generator[GitCommand, object]:
184
+ """Bound each output write and total private disk use while keeping stdout out of the console.
185
+
186
+ Every spooled command reads its input from the private folder, which a refusal or a cancellation deletes as soon
187
+ as the command ends, so each one says so to the runner.
188
+ """
189
+ size = 0
190
+ with open(filename, "wb") as stream:
191
+ def consume(chunk: bytes) -> None:
192
+ """Stop the producer if its private output becomes incomplete or too large."""
193
+ nonlocal size
194
+ check_cancelled(cancelled)
195
+ size += len(chunk)
196
+ if size > HISTORY_REWRITE_SPOOL_LIMIT_BYTES:
197
+ raise ValueError("History preparation exceeds its private file size limit; source unchanged")
198
+ if stream.write(chunk) != len(chunk):
199
+ raise OSError("Incomplete private history spool write")
200
+
201
+ yield replace(command, consume=consume, stdout_limit_bytes=None, before_start=lambda: check_cancelled(cancelled), opens_files_deleted_at_end=True)
202
+
203
+
204
+ def _message_digest(stream: BinaryIO, raw: RawCommit, cancelled: Callable[[], bool] | None) -> bytes:
205
+ """Compare binary messages using bounded reads instead of retaining their content."""
206
+ stream.seek(raw.message_offset)
207
+ remaining = raw.message_length
208
+ digest = hashlib.sha256()
209
+ while remaining:
210
+ check_cancelled(cancelled)
211
+ chunk = stream.read(min(remaining, PROCESS_CHUNK_BYTES))
212
+ if not chunk:
213
+ raise ValueError(f"Truncated verification message for {raw.oid}")
214
+ digest.update(chunk)
215
+ remaining -= len(chunk)
216
+ return digest.digest()
217
+
218
+
219
+ def _verify_import(operation_dir: str, analysis: HistoryAnalysis, replacement_by_oid: dict[str, str | None], cancelled: Callable[[], bool] | None) -> None:
220
+ """Verify original metadata, every message byte, ordered parents and retained tree deltas."""
221
+ with open(os.path.join(operation_dir, "verified-raw.out"), "rb") as verified_source, open(os.path.join(operation_dir, "raw.out"), "rb") as raw_source:
222
+ verified_by_oid = read_raw_commits(verified_source, cancelled)
223
+ expected_by_oid = {}
224
+ for commit in analysis.commits:
225
+ check_cancelled(cancelled)
226
+ raw = commit.raw
227
+ new_oid = replacement_by_oid[raw.oid]
228
+ verified = verified_by_oid.get(new_oid)
229
+ parents = []
230
+ for parent_oid in raw.parent_oids:
231
+ replacement = replacement_by_oid[parent_oid]
232
+ if replacement is not None:
233
+ parents.append(replacement)
234
+ if (verified is None or verified.author != raw.author or verified.committer != raw.committer or verified.encoding != raw.encoding
235
+ or verified.parent_oids != tuple(parents) or verified.message_length != raw.message_length or verified.signed):
236
+ raise ValueError(f"Private commit metadata verification failed for {raw.oid}; source unchanged")
237
+ if _message_digest(raw_source, raw, cancelled) != _message_digest(verified_source, verified, cancelled):
238
+ raise ValueError(f"Private commit message verification failed for {raw.oid}; source unchanged")
239
+ expected_by_oid[new_oid] = commit
240
+ with open(os.path.join(operation_dir, "verified-export.out"), "rb") as source:
241
+ verified_records = read_export(source, cancelled)
242
+ verified_oid_set = set()
243
+ for record in verified_records:
244
+ check_cancelled(cancelled)
245
+ expected = expected_by_oid.get(record.oid)
246
+ if expected is None:
247
+ continue
248
+ if set(record.changes) != set(expected.changes):
249
+ raise ValueError(f"Private commit tree verification failed for {expected.raw.oid}; source unchanged")
250
+ verified_oid_set.add(record.oid)
251
+ if verified_oid_set != set(expected_by_oid):
252
+ raise ValueError("Private verification omitted rewritten commits; source unchanged")
253
+
254
+
255
+ def _verification_request(operation_dir: str, analysis: HistoryAnalysis, records: list[ExportCommit], replacement_by_oid: dict[str, str | None],
256
+ target: RewriteTarget, cancelled: Callable[[], bool] | None) -> list[ExportCommit]:
257
+ """Prepare deduplicated verification queries off the GUI thread."""
258
+ verification_records = []
259
+ replacement_set = set()
260
+ for record in records:
261
+ check_cancelled(cancelled)
262
+ new_oid = replacement_by_oid.get(record.oid)
263
+ if new_oid is not None and new_oid not in replacement_set:
264
+ verification_records.append(replace(record, oid=new_oid))
265
+ replacement_set.add(new_oid)
266
+ _write_requests(operation_dir, verification_records, target, cancelled)
267
+ with open(os.path.join(operation_dir, "verified-tips.in"), "wb") as stream:
268
+ for branch in analysis.branches:
269
+ check_cancelled(cancelled)
270
+ stream.write(replacement_by_oid[branch.old_oid].encode("ascii") + b"\n")
271
+ return verification_records
272
+
273
+
274
+ def _write_tips(capture: RewriteCapture, operation_dir: str, cancelled: Callable[[], bool] | None) -> None:
275
+ """Keep export local while collecting known remote ancestry in one separate batch."""
276
+ with open(os.path.join(operation_dir, "tips.in"), "wb") as stream:
277
+ for oid in sorted(set(capture.local_ref_by_name.values())):
278
+ check_cancelled(cancelled)
279
+ stream.write(validate_oid(oid).encode("ascii") + b"\n")
280
+ with open(os.path.join(operation_dir, "ancestry.in"), "wb") as stream:
281
+ for oid in sorted(set(capture.local_ref_by_name.values()) | set(capture.remote_ref_by_name.values())):
282
+ check_cancelled(cancelled)
283
+ stream.write(validate_oid(oid).encode("ascii") + b"\n")
284
+
285
+
286
+ def prepare_rewrite(capture: RewriteCapture, target: RewriteTarget, storage_root_dir: str,
287
+ *, cancelled: Callable[[], bool] | None = None) -> Generator[GitCommand | FileTask, object, RewritePreparation]:
288
+ """Export, filter and verify a private rewrite without modifying source objects or files."""
289
+ preparation = RewritePreparation(capture, target)
290
+ try:
291
+ check_cancelled(cancelled)
292
+ observed = yield from capture_rewrite_state(capture)
293
+ if capture.complete:
294
+ validate_rewrite_capture(capture, observed)
295
+ elif capture.local_ref_by_name and (capture.local_ref_by_name != observed.local_ref_by_name or (capture.head_ref is not None and capture.head_ref != observed.head_ref)):
296
+ raise ValueError("Local branches changed before preparation; refresh and retry")
297
+ capture = observed
298
+ capture = yield from with_git_version(capture)
299
+ preparation = replace(preparation, capture=capture)
300
+ # Both exports, of the source and of the private result, take the options of the same Git. A version that
301
+ # gives none can only end in a refusal, so it is refused here, before the private storage is created and
302
+ # before any further Git read.
303
+ export_argument_list = export_arguments(capture.git_version)
304
+ yield from validate_initial_state(capture, target)
305
+ operation_dir, operation_identity = yield FileTask(lambda: _create_storage(capture, storage_root_dir))
306
+ preparation = replace(preparation, operation_dir=operation_dir, operation_identity=operation_identity, stream_filename=os.path.join(operation_dir, "import.in"))
307
+ yield FileTask(lambda: _write_tips(capture, operation_dir, cancelled))
308
+ object_format, shallow = (yield GitCommand(["rev-parse", "--show-object-format", "--is-shallow-repository"])).decode("ascii").splitlines()
309
+ if shallow != "false":
310
+ raise ValueError("Cannot rewrite a shallow repository; obtain its complete history before retrying")
311
+ if object_format not in ("sha1", "sha256"):
312
+ raise ValueError("Unsupported Git object format; source unchanged")
313
+ tips_filename = os.path.join(operation_dir, "tips.in")
314
+ export_filename = os.path.join(operation_dir, "export.out")
315
+ yield from _spool_command(GitCommand(export_argument_list, stdin_filename=tips_filename, timeout_ms=HISTORY_STREAM_TIMEOUT_MS, profile=READ_PROFILE), export_filename, cancelled)
316
+
317
+ def parse_export() -> list[ExportCommit]:
318
+ """Parse the finished export on the file worker, not the GUI thread."""
319
+ with open(export_filename, "rb") as source:
320
+ return read_export(source, cancelled)
321
+
322
+ records = yield FileTask(parse_export)
323
+ yield FileTask(lambda: _write_requests(operation_dir, records, target, cancelled))
324
+ yield from _spool_command(GitCommand(["cat-file", "--batch"], stdin_filename=os.path.join(operation_dir, "commits.in"), timeout_ms=HISTORY_STREAM_TIMEOUT_MS),
325
+ os.path.join(operation_dir, "raw.out"), cancelled)
326
+ analysis = yield FileTask(lambda: _analyze(capture, target, operation_dir, records, cancelled))
327
+ selected_records = []
328
+ for record in records:
329
+ if record.oid in analysis.replacement_by_oid:
330
+ selected_records.append(record)
331
+ yield FileTask(lambda: _write_requests(operation_dir, selected_records, target, cancelled))
332
+ yield from _spool_command(GitCommand(["cat-file", "--batch-check", "-Z"], stdin_filename=os.path.join(operation_dir, "presence.in"), timeout_ms=HISTORY_STREAM_TIMEOUT_MS),
333
+ os.path.join(operation_dir, "presence.out"), cancelled)
334
+ present_by_oid = yield FileTask(lambda: _read_presence(os.path.join(operation_dir, "presence.out"), selected_records, target, cancelled))
335
+ ancestry_filename = os.path.join(operation_dir, "ancestry.out")
336
+ yield from _spool_command(GitCommand(["rev-list", "--parents", "--stdin"], stdin_filename=os.path.join(operation_dir, "ancestry.in"),
337
+ timeout_ms=HISTORY_STREAM_TIMEOUT_MS), ancestry_filename, cancelled)
338
+ analysis = yield FileTask(lambda: validate_branch_state(capture, analysis, ancestry_filename, cancelled))
339
+ private_dir = os.path.join(operation_dir, PRIVATE_REPOSITORY_BASENAME)
340
+ private_arguments = [*SUBMODULE_RECURSION_OFF_ARGUMENTS, *EXACT_PATH_CASE_ARGUMENTS, "-C", private_dir, "-c", "core.hooksPath=" + os.path.join(operation_dir, "hooks")]
341
+ yield GitCommand([*private_arguments, "init", "--bare", "--template=", "--initial-branch=private", "--object-format=" + object_format], profile=LOCAL_WRITE_PROFILE,
342
+ before_start=lambda: check_cancelled(cancelled))
343
+ environment_by_name = {GIT_ALTERNATE_OBJECT_ENVIRONMENT_NAME: os.path.join(capture.context.common_git_dir, "objects")}
344
+ yield GitCommand([*private_arguments, "fast-import", "--quiet", "--export-marks=" + os.path.join(operation_dir, "marks.out")],
345
+ stdin_filename=preparation.stream_filename, environment_by_name=environment_by_name, profile=LOCAL_WRITE_PROFILE, timeout_ms=GIT_MUTATION_TIMEOUT_MS,
346
+ before_start=lambda: check_cancelled(cancelled))
347
+ replacement_by_oid = yield FileTask(lambda: _resolve_mapping(operation_dir, analysis, cancelled))
348
+ verification_records = yield FileTask(lambda: _verification_request(operation_dir, analysis, records, replacement_by_oid, target, cancelled))
349
+ yield from _spool_command(GitCommand([*private_arguments, "cat-file", "--batch-check", "-Z"], stdin_filename=os.path.join(operation_dir, "presence.in"),
350
+ timeout_ms=HISTORY_STREAM_TIMEOUT_MS, environment_by_name=environment_by_name, profile=READ_PROFILE),
351
+ os.path.join(operation_dir, "verified-presence.out"), cancelled)
352
+ verified_by_oid = yield FileTask(lambda: _read_presence(os.path.join(operation_dir, "verified-presence.out"), verification_records, target, cancelled))
353
+ if any(verified_by_oid.values()):
354
+ raise ValueError("The private result still contains the target; source unchanged, report this failure")
355
+ yield from _spool_command(GitCommand([*private_arguments, "cat-file", "--batch"], stdin_filename=os.path.join(operation_dir, "commits.in"),
356
+ timeout_ms=HISTORY_STREAM_TIMEOUT_MS, environment_by_name=environment_by_name, profile=READ_PROFILE), os.path.join(operation_dir, "verified-raw.out"), cancelled)
357
+ yield from _spool_command(GitCommand([*private_arguments, *export_argument_list], stdin_filename=os.path.join(operation_dir, "verified-tips.in"),
358
+ timeout_ms=HISTORY_STREAM_TIMEOUT_MS, environment_by_name=environment_by_name, profile=READ_PROFILE), os.path.join(operation_dir, "verified-export.out"), cancelled)
359
+ yield FileTask(lambda: _verify_import(operation_dir, analysis, replacement_by_oid, cancelled))
360
+ index = yield from prepare_candidate_index(capture, target, operation_dir, private_dir, cancelled)
361
+ observed = yield from capture_rewrite_state(capture)
362
+ validate_rewrite_capture(capture, observed)
363
+ check_cancelled(cancelled)
364
+ branches = []
365
+ for branch in analysis.branches:
366
+ branches.append(replace(branch, new_oid=replacement_by_oid[branch.old_oid]))
367
+ return replace(preparation, replacement_by_oid=replacement_by_oid, branches=tuple(branches), rows=analysis.rows, signed_count=analysis.signed_count,
368
+ current_absent=not present_by_oid[capture.local_ref_by_name[capture.head_ref]], complete=True, index=index,
369
+ message_filename=os.path.join(operation_dir, "raw.out"))
370
+ except (OSError, ValueError, RuntimeError) as error:
371
+ detail_limit = HISTORY_REWRITE_DIAGNOSTIC_LIMIT_CHARS // 4
372
+ refusal = (f"Cannot prepare removal of {repr(target.path)[:detail_limit]} from {str(capture.head_ref)[:detail_limit]}: {str(error)[:detail_limit]}. "
373
+ "Source unchanged; resolve the cause and prepare again.")
374
+ try:
375
+ yield FileTask(lambda: discard_preparation(preparation))
376
+ except (OSError, ValueError, RuntimeError):
377
+ refusal += f" Private cleanup failed at {preparation.operation_dir[:detail_limit]}; remove this owned preparation after all processes stop."
378
+ return replace(preparation, refusal=refusal, refusal_kind=error.kind if isinstance(error, RewriteRefusal) else None)