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,288 @@
1
+ """Authorize an exact repository state for history removal."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import replace
7
+ from typing import TYPE_CHECKING
8
+
9
+ from gitpane.config import LOCAL_PREFIX, REMOTE_PREFIX
10
+ from gitpane.history.repository_value import validate_oid
11
+ from gitpane.history_rewrite.graph import affected_branch_closure, ordered_history
12
+ from gitpane.history_rewrite.protocol import check_cancelled, matches_target, read_line
13
+ from gitpane.history_rewrite.value import ChangeKind, RewriteProtectedKind, RewriteRefusal, RewriteRefusalKind
14
+ from gitpane.operation.repository_operation import MERGE_MARKER_BASENAMES, REBASE_MARKER_BASENAMES, REPLAY_MARKER_BASENAMES
15
+ from gitpane.process.process_runner import READ_PROFILE
16
+ from gitpane.reference.reference_edit import reference_signature
17
+ from gitpane.reference.reference_snapshot import REFERENCE_FORMAT, parse_reference_read
18
+ from gitpane.system.path_identity import canonical_path
19
+ from gitpane.worktree.command_sequence import FileTask, GitCommand
20
+ from gitpane.worktree.worktree_file import stat_identity
21
+ from gitpane.worktree.worktree_snapshot import read_index
22
+
23
+ if TYPE_CHECKING:
24
+ from collections.abc import Callable, Generator
25
+
26
+ from gitpane.history.repository_value import RepositoryContext
27
+ from gitpane.history_rewrite.value import ExportCommit, HistoryAnalysis, RawCommit, RewriteCapture, RewriteTarget
28
+
29
+ UPSTREAM_CONFIG_PATTERN = r"^(branch\..*\.(remote|merge)|remote\..*\.fetch)$"
30
+ # Git answers the active Git directory, then the common directory that linked worktrees share, one per line.
31
+ GIT_STORAGE_ARGUMENTS = ("rev-parse", "--absolute-git-dir", "--path-format=absolute", "--git-common-dir")
32
+ GIT_ANSWER_LINE_FEED = b"\n"
33
+
34
+
35
+ def git_storage_matches(context: RepositoryContext, output: bytes) -> bool:
36
+ """Tell whether Git's answer to GIT_STORAGE_ARGUMENTS names exactly the Git directory and common directory of the context.
37
+
38
+ The folders are compared by identity, never by spelling: Git for Windows writes forward slashes where Python writes
39
+ backslashes, so a text comparison would refuse the same folder every time. A folder name may hold a line feed, so
40
+ each answer takes as many lines as the context's own path has; an answer of any other shape is refused.
41
+ """
42
+ if not output.endswith(GIT_ANSWER_LINE_FEED):
43
+ return False
44
+ lines = output.removesuffix(GIT_ANSWER_LINE_FEED).split(GIT_ANSWER_LINE_FEED)
45
+ git_dir_line_count = len(os.fsencode(context.git_dir).split(GIT_ANSWER_LINE_FEED))
46
+ common_git_dir_line_count = len(os.fsencode(context.common_git_dir).split(GIT_ANSWER_LINE_FEED))
47
+ if len(lines) != git_dir_line_count + common_git_dir_line_count:
48
+ return False
49
+ git_dir = os.fsdecode(GIT_ANSWER_LINE_FEED.join(lines[:git_dir_line_count]))
50
+ common_git_dir = os.fsdecode(GIT_ANSWER_LINE_FEED.join(lines[git_dir_line_count:]))
51
+ return canonical_path(git_dir) == canonical_path(context.git_dir) and canonical_path(common_git_dir) == canonical_path(context.common_git_dir)
52
+
53
+
54
+ def _worktree_entry(record: bytes) -> tuple[str, str | None]:
55
+ """Interpret one complete native worktree entry without relying on field order."""
56
+ worktree_dir = None
57
+ branch = None
58
+ has_head = False
59
+ for field in record.split(b"\0"):
60
+ key, _separator, value = field.partition(b" ")
61
+ if key == b"worktree":
62
+ if worktree_dir is not None or not value:
63
+ raise ValueError("Incomplete worktree listing; refresh the repository")
64
+ worktree_dir = os.fsdecode(value)
65
+ elif key == b"branch":
66
+ if branch is not None or not value:
67
+ raise ValueError("Incomplete worktree branch; refresh the repository")
68
+ branch = os.fsdecode(value)
69
+ elif key == b"HEAD":
70
+ validate_oid(value.decode("ascii"))
71
+ has_head = True
72
+ elif key == b"bare":
73
+ has_head = True
74
+ if worktree_dir is None or not has_head:
75
+ raise ValueError("Incomplete worktree listing; refresh the repository")
76
+ return worktree_dir, branch
77
+
78
+
79
+ def parse_worktree_listing(output: bytes) -> tuple[bytes, ...]:
80
+ """Normalize complete native worktree records so command ordering grants no authority."""
81
+ if not output or not output.endswith(b"\0\0"):
82
+ raise ValueError("Incomplete worktree listing; refresh the repository")
83
+ records = []
84
+ for record in output.removesuffix(b"\0\0").split(b"\0\0"):
85
+ _worktree_entry(record)
86
+ records.append(b"\0".join(sorted(record.split(b"\0"))))
87
+ return tuple(sorted(records))
88
+
89
+
90
+ def other_worktree_dir_by_branch(context: RepositoryContext, worktrees: tuple[bytes, ...]) -> dict[str, str]:
91
+ """Identify checked-out branches outside the active worktree, including absorbed submodules.
92
+
93
+ Folders are compared by canonical_path, the identity the Git folder check of this guard uses, so the active worktree
94
+ listed by Git in another letter case or Unicode form is still known as the active one.
95
+ """
96
+ active_dir_set = {canonical_path(context.repository_dir)}
97
+ # Git reports an absorbed submodule's main worktree using its administration directory.
98
+ # It denotes this checkout only when the active Git directory is also the common one.
99
+ if canonical_path(context.git_dir) == canonical_path(context.common_git_dir):
100
+ active_dir_set.add(canonical_path(context.git_dir))
101
+ worktree_dir_by_branch = {}
102
+ for record in worktrees:
103
+ worktree_dir, branch = _worktree_entry(record)
104
+ if branch and canonical_path(worktree_dir) not in active_dir_set:
105
+ # The folder is only shown to the user: spell it the system's way, since Git for Windows writes forward slashes.
106
+ worktree_dir_by_branch[branch] = os.path.normpath(worktree_dir)
107
+ return worktree_dir_by_branch
108
+
109
+
110
+ def _read_local_state(capture: RewriteCapture) -> RewriteCapture:
111
+ """Capture exact index bytes plus native operation markers without refreshing the index."""
112
+ index_filename = os.path.join(capture.context.git_dir, "index")
113
+ index_identity = read_index(index_filename)
114
+ index_stat = stat_identity(os.lstat(index_filename)) if index_identity is not None else None
115
+ native_states = []
116
+ for basename in REBASE_MARKER_BASENAMES + REPLAY_MARKER_BASENAMES + MERGE_MARKER_BASENAMES:
117
+ if os.path.lexists(os.path.join(capture.context.git_dir, basename)):
118
+ native_states.append(basename)
119
+ return replace(capture, index_identity=index_identity, index_stat=index_stat, native_state=tuple(sorted(native_states)))
120
+
121
+
122
+ def capture_rewrite_state(capture: RewriteCapture) -> Generator[GitCommand | FileTask, object, RewriteCapture]:
123
+ """Read local authority in a fixed number of native commands, with no network access."""
124
+ if capture.context.is_bare:
125
+ raise RewriteRefusal(RewriteRefusalKind.INCOMPLETE_HISTORY, "A working repository is required; open its worktree before building a preview")
126
+ authority = yield FileTask(lambda: reference_signature(capture.context))
127
+ locations = yield GitCommand(list(GIT_STORAGE_ARGUMENTS), profile=READ_PROFILE)
128
+ if not git_storage_matches(capture.context, locations):
129
+ raise RewriteRefusal(RewriteRefusalKind.STALE_CAPTURE, "The repository Git storage changed; reopen the repository and build a new preview")
130
+ initial = yield FileTask(lambda: _read_local_state(capture))
131
+ output = yield GitCommand(["for-each-ref", "--format=" + REFERENCE_FORMAT, LOCAL_PREFIX, REMOTE_PREFIX])
132
+ reference = parse_reference_read(output)
133
+ for entry in reference.references:
134
+ if entry.full_name.startswith(LOCAL_PREFIX) and entry.symbolic_target is not None:
135
+ raise RewriteRefusal(RewriteRefusalKind.STALE_CAPTURE, f"Local branch {entry.full_name} is symbolic. Use ordinary local branches before building a new preview.")
136
+ symbolic_head = (yield GitCommand(["symbolic-ref", "--quiet", "HEAD"], accepted_codes=(0, 1))).rstrip(b"\n")
137
+ head_ref = os.fsdecode(symbolic_head) if symbolic_head else None
138
+ if reference.head_ref != head_ref and reference.head_ref is not None:
139
+ raise RewriteRefusal(RewriteRefusalKind.STALE_CAPTURE, "HEAD changed during the read; build a new preview")
140
+ local_ref_by_name = {}
141
+ remote_ref_by_name = {}
142
+ for name, oid in reference.object_oid_by_name.items():
143
+ if name.startswith(LOCAL_PREFIX):
144
+ local_ref_by_name[name] = oid
145
+ elif name.startswith(REMOTE_PREFIX):
146
+ remote_ref_by_name[name] = oid
147
+ upstream_config = yield GitCommand(["config", "--null", "--get-regexp", UPSTREAM_CONFIG_PATTERN], accepted_codes=(0, 1))
148
+ worktree_output = yield GitCommand(["worktree", "list", "--porcelain", "-z"])
149
+ worktrees = parse_worktree_listing(worktree_output)
150
+ current = yield FileTask(lambda: _read_local_state(capture))
151
+ if (initial.index_identity, initial.index_stat, initial.native_state) != (current.index_identity, current.index_stat, current.native_state):
152
+ raise RewriteRefusal(RewriteRefusalKind.STALE_CAPTURE, "The index or native operation changed during the read; build a new preview")
153
+ shared_path = (yield GitCommand(["rev-parse", "--path-format=absolute", "--shared-index-path"])).removesuffix(b"\n")
154
+ shared_index_filename = os.fsdecode(shared_path)
155
+ shared_identity = (yield FileTask(lambda: read_index(shared_index_filename))) if shared_index_filename else None
156
+ if (yield FileTask(lambda: reference_signature(capture.context))) != authority:
157
+ raise RewriteRefusal(RewriteRefusalKind.STALE_CAPTURE, "References, configuration or worktrees changed during the read; build a new preview")
158
+ return replace(current, head_ref=head_ref, local_ref_by_name=local_ref_by_name, remote_ref_by_name=remote_ref_by_name,
159
+ upstream_by_name=reference.upstream_by_name, upstream_config=upstream_config, worktrees=worktrees, complete=True,
160
+ shared_index_filename=shared_index_filename, shared_index_identity=shared_identity)
161
+
162
+
163
+ def publication_authority(capture: RewriteCapture) -> tuple:
164
+ """Bracket the final native reads with local evidence of refs, configuration and native operations."""
165
+ local = _read_local_state(capture)
166
+ shared_identity = read_index(capture.shared_index_filename) if capture.shared_index_filename else None
167
+ return reference_signature(capture.context), local.index_identity, local.index_stat, local.native_state, shared_identity
168
+
169
+
170
+ def validate_publication_authority(capture: RewriteCapture, expected: tuple) -> None:
171
+ """Reject local authority changes while the collective reference transaction waits to launch."""
172
+ if publication_authority(capture) != expected:
173
+ raise RewriteRefusal(RewriteRefusalKind.STALE_CAPTURE, "Repository authority changed before publication; build a new preview")
174
+
175
+
176
+ def validate_rewrite_capture(expected: RewriteCapture, observed: RewriteCapture) -> None:
177
+ """Refuse every authority change, including a newly created ref or an absent index."""
178
+ if not expected.complete or not observed.complete or expected != observed:
179
+ raise RewriteRefusal(RewriteRefusalKind.STALE_CAPTURE, "Preview is out of date. Repository, HEAD, references, index, upstream settings or worktrees changed; build a new preview")
180
+
181
+
182
+ def protected_target_refusal(target: RewriteTarget, path: bytes, mode: bytes, commit_oid: str | None = None) -> None:
183
+ """Refuse submodule boundaries or metadata with their exact historical witness."""
184
+ _parent, _separator, basename = path.rpartition(b"/")
185
+ protected_kind = RewriteProtectedKind.SUBMODULE if mode == b"160000" else RewriteProtectedKind.MODULE_CONFIGURATION if basename == b".gitmodules" else None
186
+ if protected_kind is None:
187
+ return
188
+ if not matches_target(path, target) and not (mode == b"160000" and target.path.startswith(path + b"/")):
189
+ return
190
+ witness = f"commit {commit_oid} contains" if commit_oid else "it contains"
191
+ raise RewriteRefusal(RewriteRefusalKind.PROTECTED_TARGET,
192
+ f"Cannot remove {target.path!r}: {witness} {path!r}, which is {protected_kind.value}. Choose a path that does not include a submodule or .gitmodules. No history was changed.",
193
+ path=path, commit_oid=commit_oid)
194
+
195
+
196
+ def validate_history_protection(capture: RewriteCapture, target: RewriteTarget, records: list[ExportCommit], raw_by_oid: dict[str, RawCommit],
197
+ cancelled: Callable[[], bool] | None = None) -> None:
198
+ """Check protected entries only in the final affected ancestry, including deleted gitlinks."""
199
+ ordered_records = ordered_history(records, raw_by_oid, cancelled)
200
+ included_oid_set = affected_branch_closure(capture, target, ordered_records, raw_by_oid, cancelled)
201
+ for record in ordered_records:
202
+ check_cancelled(cancelled)
203
+ if record.oid not in included_oid_set:
204
+ continue
205
+ for change in record.changes:
206
+ if change.kind == ChangeKind.MODIFY:
207
+ protected_target_refusal(target, change.path, change.mode, record.oid)
208
+
209
+
210
+ def validate_initial_state(capture: RewriteCapture, target: RewriteTarget) -> Generator[GitCommand, bytes]:
211
+ """Reject unsafe source states while allowing unrelated staged or unstaged changes."""
212
+ if capture.head_ref is None:
213
+ raise RewriteRefusal(RewriteRefusalKind.DETACHED_HEAD, "HEAD is detached. Check out a local branch, then build a new preview.")
214
+ if capture.head_ref not in capture.local_ref_by_name:
215
+ raise RewriteRefusal(RewriteRefusalKind.UNBORN_BRANCH, f"Branch {capture.head_ref} is unborn. Create a commit before building a new preview.", branch=capture.head_ref)
216
+ if capture.native_state:
217
+ operation = ", ".join(capture.native_state)
218
+ raise RewriteRefusal(RewriteRefusalKind.NATIVE_OPERATION, f"This repository is busy with {operation}. Finish or stop that operation, then build a new preview.")
219
+ protected_target_refusal(target, target.path, b"")
220
+ listing = yield GitCommand(["ls-files", "--stage", "-z"])
221
+ for record in listing.removesuffix(b"\0").split(b"\0") if listing else ():
222
+ metadata, separator, path = record.partition(b"\t")
223
+ if not separator:
224
+ raise ValueError("Incomplete index listing; build a new preview")
225
+ mode, _oid, stage = metadata.split(b" ")
226
+ if stage != b"0":
227
+ raise RewriteRefusal(RewriteRefusalKind.UNMERGED_INDEX, f"The index has conflicts at {path!r}. Resolve the conflicts, then build a new preview.", path=path)
228
+ protected_target_refusal(target, path, mode)
229
+ staged = yield GitCommand(["diff", "--cached", "--name-only", "-z", "--no-renames", "--ita-visible-in-index", "--ignore-submodules=none",
230
+ capture.local_ref_by_name[capture.head_ref], "--"])
231
+ for path in staged.removesuffix(b"\0").split(b"\0") if staged else ():
232
+ if matches_target(path, target):
233
+ raise RewriteRefusal(RewriteRefusalKind.STAGED_TARGET,
234
+ f"{path!r} has staged changes. Unstage this path first so its staged version is not lost, then build a new preview. Other staged files can stay staged.", path=path)
235
+
236
+
237
+ def validate_branch_state(capture: RewriteCapture, analysis: HistoryAnalysis, ancestry_filename: str,
238
+ cancelled: Callable[[], bool] | None = None) -> HistoryAnalysis:
239
+ """Check all moving branches against known upstream ancestry plus other worktrees."""
240
+ parent_oids_by_oid = {}
241
+ with open(ancestry_filename, "rb") as stream:
242
+ while line := read_line(stream, cancelled):
243
+ oid, *parent_oids = line.decode("ascii").split()
244
+ parent_oids_by_oid[validate_oid(oid)] = tuple(validate_oid(parent) for parent in parent_oids)
245
+
246
+ def ancestor_set(tip_oid: str) -> set[str]:
247
+ """Visit each ancestor once, refusing incomplete local knowledge."""
248
+ visited_oid_set = set()
249
+ pending_oids = [tip_oid]
250
+ while pending_oids:
251
+ check_cancelled(cancelled)
252
+ oid = pending_oids.pop()
253
+ if oid in visited_oid_set:
254
+ continue
255
+ if oid not in parent_oids_by_oid:
256
+ raise RewriteRefusal(RewriteRefusalKind.INCOMPLETE_HISTORY, f"Missing history at {oid}; make the missing objects available, then build a new preview. No fetch was performed.", commit_oid=oid)
257
+ visited_oid_set.add(oid)
258
+ pending_oids.extend(parent_oids_by_oid[oid])
259
+ return visited_oid_set
260
+
261
+ worktree_dir_by_branch = other_worktree_dir_by_branch(capture.context, capture.worktrees)
262
+ changed_oid_set = set()
263
+ for oid, replacement in analysis.replacement_by_oid.items():
264
+ if oid != replacement:
265
+ changed_oid_set.add(oid)
266
+ published_oid_set_by_ref = {}
267
+ for name, oid in capture.remote_ref_by_name.items():
268
+ published_oid_set_by_ref[name] = ancestor_set(oid) & changed_oid_set
269
+ branches = []
270
+ for branch in analysis.branches:
271
+ if branch.name in worktree_dir_by_branch:
272
+ raise RewriteRefusal(RewriteRefusalKind.OTHER_WORKTREE,
273
+ f"{branch.name} is checked out in another worktree: {worktree_dir_by_branch[branch.name]}. Release that branch in the other worktree, then build a new preview. GitPane will not change that worktree.",
274
+ branch=branch.name)
275
+ local_ancestor_set = ancestor_set(branch.old_oid)
276
+ if branch.upstream:
277
+ upstream_oid = capture.remote_ref_by_name.get(branch.upstream, capture.local_ref_by_name.get(branch.upstream))
278
+ if upstream_oid is None:
279
+ raise RewriteRefusal(RewriteRefusalKind.INCOMPLETE_HISTORY, f"The known upstream {branch.upstream} of {branch.name} is unavailable. Make its history available, then build a new preview. No fetch was performed.", branch=branch.name)
280
+ if upstream_oid not in local_ancestor_set:
281
+ raise RewriteRefusal(RewriteRefusalKind.BEHIND_UPSTREAM,
282
+ f"{branch.name} is behind {branch.upstream} according to the last fetch. Update that local branch before removing this path from history, then build a new preview.", branch=branch.name)
283
+ remote_refs = []
284
+ for name, published_oid_set in published_oid_set_by_ref.items():
285
+ if published_oid_set & local_ancestor_set:
286
+ remote_refs.append(name)
287
+ branches.append(replace(branch, remote_refs=tuple(sorted(remote_refs))))
288
+ return replace(analysis, branches=tuple(branches))
@@ -0,0 +1,125 @@
1
+ """Prepare a verified private index without changing the working repository."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import os
7
+ import re
8
+ from dataclasses import replace
9
+ from typing import TYPE_CHECKING
10
+
11
+ from gitpane.config import EXACT_PATH_CASE_ARGUMENTS, GIT_ALTERNATE_OBJECT_ENVIRONMENT_NAME, SUBMODULE_RECURSION_OFF_ARGUMENTS
12
+ from gitpane.history.repository_value import validate_oid
13
+ from gitpane.history_rewrite.protocol import check_cancelled, matches_target
14
+ from gitpane.history_rewrite.value import RewriteIndex, RewriteRefusal, RewriteRefusalKind
15
+ from gitpane.process.process_runner import LOCAL_WRITE_PROFILE, READ_PROFILE
16
+ from gitpane.worktree.command_sequence import FileTask, GitCommand
17
+ from gitpane.worktree.worktree_snapshot import read_index
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Callable, Generator
21
+
22
+ from gitpane.history_rewrite.value import RewriteCapture, RewriteTarget
23
+
24
+ # Native debug output is deliberately checked rather than guessed: Git documents it as unstable.
25
+ # Unknown formats must refuse publication, because flags cannot otherwise be verified faithfully.
26
+ INDEX_DEBUG_PATTERN = re.compile(rb" ctime: \d+:\d+\n mtime: \d+:\d+\n dev: \d+\tino: \d+\n uid: \d+\tgid: \d+\n size: \d+\tflags: (?P<flags>[0-9a-fA-F]+)\n")
27
+ INDEX_ASSUME_UNCHANGED_FLAG = 0x8000
28
+ INDEX_INTENT_TO_ADD_FLAG = 1 << 29
29
+ INDEX_SKIP_WORKTREE_FLAG = 1 << 30
30
+ INDEX_PERSISTENT_FLAG_MASK = INDEX_ASSUME_UNCHANGED_FLAG | INDEX_INTENT_TO_ADD_FLAG | INDEX_SKIP_WORKTREE_FLAG
31
+ INDEX_LIST_ARGUMENTS = ["ls-files", "--stage", "--debug", "-z"]
32
+
33
+
34
+ def parse_index_entry(output: bytes, cancelled: Callable[[], bool] | None = None) -> dict[tuple[bytes, bytes], tuple[bytes, str, int]]:
35
+ """Read literal paths, stages and persistent flags from a complete native listing."""
36
+ entry_by_key = {}
37
+ position = 0
38
+ while position < len(output):
39
+ check_cancelled(cancelled)
40
+ end = output.find(b"\0", position)
41
+ if end < 0:
42
+ raise RewriteRefusal(RewriteRefusalKind.UNSUPPORTED_INDEX, "Incomplete private index listing; no history was changed")
43
+ metadata, separator, path = output[position:end].partition(b"\t")
44
+ debug = INDEX_DEBUG_PATTERN.match(output, end + 1)
45
+ if not separator or not path or debug is None:
46
+ raise RewriteRefusal(RewriteRefusalKind.UNSUPPORTED_INDEX, "Cannot verify this Git index format without losing entry flags; no history was changed")
47
+ mode, oid, stage = metadata.split(b" ")
48
+ key = (path, stage)
49
+ if key in entry_by_key:
50
+ raise RewriteRefusal(RewriteRefusalKind.UNSUPPORTED_INDEX, "Duplicate private index entry; no history was changed")
51
+ entry_by_key[key] = (mode, validate_oid(oid.decode("ascii")), int(debug.group("flags"), 16) & INDEX_PERSISTENT_FLAG_MASK)
52
+ position = debug.end()
53
+ return entry_by_key
54
+
55
+
56
+ def verify_candidate_index(target: RewriteTarget, original: bytes, candidate: bytes, cancelled: Callable[[], bool] | None = None) -> None:
57
+ """Require every retained entry to keep its mode, blob, stage and persistent flags."""
58
+ expected_by_key = {}
59
+ for key, entry in parse_index_entry(original, cancelled).items():
60
+ path, _stage = key
61
+ if not matches_target(path, target):
62
+ expected_by_key[key] = entry
63
+ if parse_index_entry(candidate, cancelled) != expected_by_key:
64
+ raise RewriteRefusal(RewriteRefusalKind.UNSUPPORTED_INDEX, "The candidate index changed unrelated entries or flags; no history was changed. Inspect the index before building another preview.")
65
+
66
+
67
+ def index_digest(filename: str) -> bytes | None:
68
+ """Distinguish an absent index from every exact byte representation used for publication."""
69
+ content = read_index(filename)
70
+ return hashlib.sha256(content).digest() if content is not None else None
71
+
72
+
73
+ def _copy_captured_index(capture: RewriteCapture, operation_dir: str, private_dir: str) -> RewriteIndex:
74
+ """Materialize only captured index bytes and its shared base inside owned private storage."""
75
+ source_filename = os.path.join(capture.context.git_dir, "index")
76
+ if read_index(source_filename) != capture.index_identity:
77
+ raise RewriteRefusal(RewriteRefusalKind.STALE_CAPTURE, "The index changed before candidate preparation; build a new preview")
78
+ original_filename = os.path.join(operation_dir, "original.index")
79
+ candidate_filename = os.path.join(private_dir, "index")
80
+ os.mkdir(os.path.join(operation_dir, "index-worktree"))
81
+ if capture.shared_index_filename:
82
+ shared_content = read_index(capture.shared_index_filename)
83
+ if shared_content is None or shared_content != capture.shared_index_identity:
84
+ raise RewriteRefusal(RewriteRefusalKind.STALE_CAPTURE, "The shared index changed before candidate preparation; build a new preview")
85
+ with open(os.path.join(private_dir, os.path.basename(capture.shared_index_filename)), "xb") as stream:
86
+ stream.write(shared_content)
87
+ if capture.index_identity is not None:
88
+ for filename in (original_filename, candidate_filename):
89
+ with open(filename, "xb") as stream:
90
+ stream.write(capture.index_identity)
91
+ return RewriteIndex(original_filename, candidate_filename, index_digest(original_filename), index_digest(candidate_filename))
92
+
93
+
94
+ def _write_removal_request(filename: str, target: RewriteTarget, original: bytes, cancelled: Callable[[], bool] | None) -> bool:
95
+ """Pass only exact indexed target paths to native Git through NUL-framed input."""
96
+ paths = []
97
+ for path, _stage in parse_index_entry(original, cancelled):
98
+ if matches_target(path, target):
99
+ paths.append(path)
100
+ with open(filename, "xb") as stream:
101
+ for path in sorted(paths):
102
+ check_cancelled(cancelled)
103
+ stream.write(path + b"\0")
104
+ return bool(paths)
105
+
106
+
107
+ def prepare_candidate_index(capture: RewriteCapture, target: RewriteTarget, operation_dir: str, private_dir: str,
108
+ cancelled: Callable[[], bool] | None = None) -> Generator[GitCommand | FileTask, object, RewriteIndex]:
109
+ """Remove target entries in private Git storage, preserving every unrelated staged decision."""
110
+ check_cancelled(cancelled)
111
+ index = yield FileTask(lambda: _copy_captured_index(capture, operation_dir, private_dir))
112
+ original = yield GitCommand(INDEX_LIST_ARGUMENTS.copy(), profile=READ_PROFILE)
113
+ request_filename = os.path.join(operation_dir, "remove-index.in")
114
+ remove = yield FileTask(lambda: _write_removal_request(request_filename, target, original, cancelled))
115
+ environment_by_name = {"GIT_INDEX_FILE": index.candidate_filename, GIT_ALTERNATE_OBJECT_ENVIRONMENT_NAME: os.path.join(capture.context.common_git_dir, "objects")}
116
+ private_arguments = [*SUBMODULE_RECURSION_OFF_ARGUMENTS, *EXACT_PATH_CASE_ARGUMENTS, "--git-dir=" + private_dir, "--work-tree=" + os.path.join(operation_dir, "index-worktree"),
117
+ "-c", "core.bare=false", "-c", "core.splitIndex=false", "-c", "core.sparseCheckout=false", "-c", "index.sparse=false"]
118
+ if remove:
119
+ yield GitCommand([*private_arguments, "update-index", "--no-split-index", "--force-remove", "-z", "--stdin"], stdin_filename=request_filename,
120
+ environment_by_name=environment_by_name, profile=LOCAL_WRITE_PROFILE, before_start=lambda: check_cancelled(cancelled))
121
+ # The candidate index and the private repository are deleted with the private folder as soon as this read ends.
122
+ candidate = yield GitCommand([*EXACT_PATH_CASE_ARGUMENTS, "-C", private_dir, *INDEX_LIST_ARGUMENTS], environment_by_name=environment_by_name, profile=READ_PROFILE,
123
+ opens_files_deleted_at_end=True)
124
+ yield FileTask(lambda: verify_candidate_index(target, original, candidate, cancelled))
125
+ return replace(index, candidate_identity=(yield FileTask(lambda: index_digest(index.candidate_filename))))