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,444 @@
1
+ """Publish an authorized private history proposal without writing working files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import stat
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+ from typing import TYPE_CHECKING
10
+
11
+ from gitpane.config import EXACT_PATH_CASE_ARGUMENTS, GIT_MUTATION_TIMEOUT_MS, LOCAL_PREFIX, MUTATION_IDLE_TIMEOUT_MS, SUBMODULE_RECURSION_OFF_ARGUMENTS
12
+ from gitpane.history.file_history import WindowCapability
13
+ from gitpane.history.repository_value import validate_oid
14
+ from gitpane.history_rewrite.guard import capture_rewrite_state, publication_authority, validate_publication_authority, validate_rewrite_capture
15
+ from gitpane.history_rewrite.index import index_digest
16
+ from gitpane.history_rewrite.prepare import OWNER_BASENAME, OWNER_CONTENT, PRIVATE_REF
17
+ from gitpane.history_rewrite.protocol import copy_bytes, read_line
18
+ from gitpane.history_rewrite.recovery import (
19
+ EMPTY_LOCK_DIGEST,
20
+ PRIVATE_REF_PREFIX,
21
+ RECOVERY_SCHEMA_VERSION,
22
+ RewritePublicationState,
23
+ RewriteRecoveryRecord,
24
+ context_identity,
25
+ create_recovery_directory,
26
+ digest_text,
27
+ finish_coherent_recovery,
28
+ lock_identity,
29
+ observe_recovery,
30
+ recover_rewrite,
31
+ release_recovery_lock,
32
+ retain_recovery_failure,
33
+ sync_directory,
34
+ transition_record,
35
+ validate_recovery_authority,
36
+ write_recovery_record,
37
+ )
38
+ from gitpane.operation.diagnostic import Diagnostic
39
+ from gitpane.operation.operation_status import OperationStatus
40
+ from gitpane.operation.repository_operation import OperationAction, OperationIntent, OperationOutcome, WriteScope
41
+ from gitpane.process.process_runner import LOCAL_WRITE_PROFILE
42
+ from gitpane.reference.reference_edit import reference_signature
43
+ from gitpane.reference.reference_snapshot import REFERENCE_FORMAT, parse_reference_read
44
+ from gitpane.worktree.command_sequence import FileTask, GitCommand
45
+ from gitpane.worktree.file_menu_action import FileAction
46
+ from gitpane.worktree.mutation import fill_owned_lock, release_owned_index_lock
47
+ from gitpane.worktree.worktree_file import FILE_OBJECT_IDENTITY_FIELDS, PRIVATE_FILE_MODE
48
+ from gitpane.worktree.worktree_snapshot import read_index
49
+
50
+ if TYPE_CHECKING:
51
+ from collections.abc import Callable, Generator
52
+
53
+ from gitpane.history_rewrite.recovery import RecoveryObservation
54
+ from gitpane.history_rewrite.value import RewritePreparation
55
+ from gitpane.operation.repository_operation import RepositoryOperationService
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class RewritePublicationOutcome(OperationOutcome):
60
+ """Expose the reconciled journal location alongside the common operation status."""
61
+
62
+ record: RewriteRecoveryRecord | None = None
63
+
64
+
65
+ class RewriteProgress(Enum):
66
+ """Name actual publication steps shared by the service and its presentation."""
67
+
68
+ CHECKING = "Checking repository…"
69
+ IMPORTING = "Importing rewritten commits…"
70
+ REFERENCES = "Updating local branches…"
71
+ INDEX = "Updating index…"
72
+ REFRESHING = "Refreshing repository…"
73
+ FINISHING = "Finishing repository update…"
74
+
75
+
76
+ def rewrite_operation(preparation: RewritePreparation, operation_id: str, *, session_id: str | None = None) -> OperationIntent:
77
+ """Capture all local branch authority under the common repository reservation."""
78
+ capture = preparation.capture
79
+ return OperationIntent.capture(operation_id, capture.context, capture.git_filename, generation=capture.generation, session_id=session_id,
80
+ action=OperationAction.REF_WRITE, capability=WindowCapability.MANAGE, operation=FileAction.REMOVE_FROM_HISTORY.label, from_form=True,
81
+ targets=(preparation.operation_dir, preparation.operation_identity), scope=WriteScope.capture(capture.context, (LOCAL_PREFIX + "*",)))
82
+
83
+
84
+ def _validate_preparation(preparation: RewritePreparation, operation: OperationIntent) -> None:
85
+ """Refuse incomplete, replaced or differently authorized proposals before writing source storage."""
86
+ if not preparation.complete or preparation.index is None or not preparation.branches:
87
+ raise ValueError("No complete history changes to publish; build a new preview")
88
+ capture = preparation.capture
89
+ if (operation.context != capture.context or operation.git_filename != capture.git_filename or operation.generation != capture.generation
90
+ or operation.action != OperationAction.REF_WRITE or operation.capability != WindowCapability.MANAGE
91
+ or operation.targets != (preparation.operation_dir, preparation.operation_identity)
92
+ or operation.scope != WriteScope.capture(capture.context, (LOCAL_PREFIX + "*",))):
93
+ raise ValueError("Publication authority differs from the preview; build a new preview")
94
+ information = os.lstat(preparation.operation_dir)
95
+ if not stat.S_ISDIR(information.st_mode) or (information.st_dev, information.st_ino) != preparation.operation_identity:
96
+ raise ValueError("Private preparation was replaced; build a new preview")
97
+ with open(os.path.join(preparation.operation_dir, OWNER_BASENAME), "rb") as stream:
98
+ if stream.read(len(OWNER_CONTENT) + 1) != OWNER_CONTENT:
99
+ raise ValueError("Private preparation ownership changed; build a new preview")
100
+ if index_digest(preparation.index.candidate_filename) != preparation.index.candidate_identity:
101
+ raise ValueError("Private candidate index changed; build a new preview")
102
+
103
+
104
+ def _new_record(preparation: RewritePreparation, operation: OperationIntent, storage_root_dir: str) -> RewriteRecoveryRecord:
105
+ """Consume the preparation token before creating the durable publication evidence."""
106
+ _validate_preparation(preparation, operation)
107
+ # A claim inside the owned preparation also rejects reuse under a different operation ID.
108
+ try:
109
+ with open(os.path.join(preparation.operation_dir, "publication.claim"), "xb") as stream:
110
+ stream.write(operation.operation_id.encode("ascii"))
111
+ stream.flush()
112
+ os.fsync(stream.fileno())
113
+ except FileExistsError as error:
114
+ raise ValueError("This preview has already been submitted; build a new preview") from error
115
+ sync_directory(preparation.operation_dir)
116
+ operation_dir = create_recovery_directory(storage_root_dir, operation.operation_id, operation.context)
117
+ old_oid_by_ref = dict(preparation.capture.local_ref_by_name)
118
+ new_oid_by_ref = dict(old_oid_by_ref)
119
+ private_ref_by_name = {}
120
+ for number, branch in enumerate(preparation.branches):
121
+ if branch.name not in old_oid_by_ref or branch.old_oid != old_oid_by_ref[branch.name] or not branch.name.startswith(LOCAL_PREFIX):
122
+ raise ValueError("A prepared branch no longer matches its capture; build a new preview")
123
+ new_oid_by_ref[branch.name] = validate_oid(branch.new_oid)
124
+ prefix = PRIVATE_REF_PREFIX + operation.operation_id + "/"
125
+ private_ref_by_name[prefix + "old-" + str(number)] = branch.old_oid
126
+ private_ref_by_name[prefix + "new-" + str(number)] = branch.new_oid
127
+ record = RewriteRecoveryRecord(RECOVERY_SCHEMA_VERSION, operation.operation_id, operation_dir, context_identity(operation.context), operation.git_filename,
128
+ preparation.capture.head_ref, old_oid_by_ref, new_oid_by_ref, digest_text(preparation.index.original_identity),
129
+ digest_text(preparation.index.candidate_identity), None, RewritePublicationState.PREPARED, private_ref_by_name)
130
+ _make_source_stream(preparation, record)
131
+ write_recovery_record(record)
132
+ return record
133
+
134
+
135
+ def _make_source_stream(preparation: RewritePreparation, record: RewriteRecoveryRecord) -> None:
136
+ """Copy the closed generated grammar, rebinding only reference directives outside length-delimited messages."""
137
+ import_ref = PRIVATE_REF_PREFIX + record.operation_id + "/import"
138
+ mark_by_oid = {}
139
+ for private_mark, oid in _read_marks(os.path.join(preparation.operation_dir, "marks.out")).items():
140
+ mark_by_oid[oid] = private_mark
141
+ mark = None
142
+ with open(preparation.stream_filename, "rb") as source, open(os.path.join(record.operation_dir, "import.in"), "xb") as destination:
143
+ while line := read_line(source):
144
+ if line == b"done\n":
145
+ if source.read(1):
146
+ raise ValueError("Unexpected data after the prepared import; build a new preview")
147
+ for reference, oid in record.private_ref_by_name.items():
148
+ # New objects in the open pack are addressed by marks until fast-import closes it.
149
+ target = mark_by_oid.get(oid, oid)
150
+ destination.write(b"reset " + reference.encode("ascii") + b"\nfrom " + target.encode("ascii") + b"\n\n")
151
+ destination.write(b"done\n")
152
+ break
153
+ if line.startswith((b"reset ", b"commit ")):
154
+ verb, reference = line.removesuffix(b"\n").split(b" ", 1)
155
+ if reference != PRIVATE_REF:
156
+ raise ValueError("Unexpected reference in prepared import; build a new preview")
157
+ line = verb + b" " + import_ref.encode("ascii") + b"\n"
158
+ elif line.startswith(b"mark "):
159
+ mark = line.removeprefix(b"mark ").strip().decode("ascii")
160
+ elif line not in (b"feature done\n", b"\n") and not line.startswith((b"author ", b"committer ", b"encoding ", b"data ", b"from ", b"merge ", b"M ", b"D ")):
161
+ raise ValueError("Unexpected command in prepared import; build a new preview")
162
+ destination.write(line)
163
+ if line.startswith(b"data "):
164
+ copy_bytes(source, int(line.removeprefix(b"data ").strip()), destination)
165
+ else:
166
+ raise ValueError("Incomplete prepared import; build a new preview")
167
+ destination.flush()
168
+ os.fsync(destination.fileno())
169
+ if mark is not None:
170
+ record.private_ref_by_name[import_ref] = _read_marks(os.path.join(preparation.operation_dir, "marks.out"))[mark]
171
+
172
+
173
+ def _read_marks(filename: str) -> dict[str, str]:
174
+ """Read the import's exact mark mapping without depending on Git's output ordering."""
175
+ oid_by_mark = {}
176
+ with open(filename, "rb") as stream:
177
+ while line := read_line(stream):
178
+ mark, oid = line.strip().split(b" ")
179
+ mark = mark.decode("ascii")
180
+ if mark in oid_by_mark:
181
+ raise ValueError("Duplicate history import mark; inspect the recovery files")
182
+ oid_by_mark[mark] = validate_oid(oid.decode("ascii"))
183
+ return oid_by_mark
184
+
185
+
186
+ def _verify_marks(preparation: RewritePreparation, record: RewriteRecoveryRecord) -> None:
187
+ """Require source import object identities to match the verified private import."""
188
+ if _read_marks(os.path.join(preparation.operation_dir, "marks.out")) != _read_marks(os.path.join(record.operation_dir, "marks.out")):
189
+ raise ValueError("Source import differs from the verified preview; inspect the recovery files")
190
+
191
+
192
+ def rewrite_command(record: RewriteRecoveryRecord, arguments: list[str], *, stdin: bytes | None = None, stdin_filename: str | None = None,
193
+ before_start: Callable[[], None] | None = None) -> GitCommand:
194
+ """Supervise every native write with empty private hooks, submodule recursion disabled and exact path case.
195
+
196
+ The source import must fold no path case, or it would build other commits than the verified private import.
197
+ """
198
+ return GitCommand([*SUBMODULE_RECURSION_OFF_ARGUMENTS, *EXACT_PATH_CASE_ARGUMENTS, "-c", "core.hooksPath=" + os.path.join(record.operation_dir, "hooks"), *arguments],
199
+ stdin=stdin, stdin_filename=stdin_filename, profile=LOCAL_WRITE_PROFILE, timeout_ms=GIT_MUTATION_TIMEOUT_MS,
200
+ idle_timeout_ms=MUTATION_IDLE_TIMEOUT_MS, before_start=before_start)
201
+
202
+
203
+ def reference_transaction(old_oid_by_ref: dict[str, str], new_oid_by_ref: dict[str, str]) -> bytes:
204
+ """Compare every captured reference in one native transaction before moving any affected branch."""
205
+ commands = [b"start\n"]
206
+ for name, old_oid in sorted(old_oid_by_ref.items()):
207
+ new_oid = new_oid_by_ref[name]
208
+ if old_oid == new_oid:
209
+ commands.append(b"verify " + os.fsencode(name) + b" " + old_oid.encode("ascii") + b"\n")
210
+ else:
211
+ commands.append(b"update " + os.fsencode(name) + b" " + new_oid.encode("ascii") + b" " + old_oid.encode("ascii") + b"\n")
212
+ commands.extend((b"prepare\n", b"commit\n"))
213
+ return b"".join(commands)
214
+
215
+
216
+ def _acquire_index(record: RewriteRecoveryRecord, preparation: RewritePreparation) -> RewriteRecoveryRecord:
217
+ """Acquire and fill only an exclusive index lock, recording ownership before content work."""
218
+ index_filename = os.path.join(preparation.capture.context.git_dir, "index")
219
+ lock_filename = index_filename + ".lock"
220
+ try:
221
+ descriptor = os.open(lock_filename, os.O_WRONLY | os.O_CREAT | os.O_EXCL, PRIVATE_FILE_MODE)
222
+ except FileExistsError as error:
223
+ raise ValueError("Another writer owns index.lock; wait for it to finish, then build a new preview") from error
224
+ information = os.fstat(descriptor)
225
+ identity = information.st_dev, information.st_ino, stat.S_IFMT(information.st_mode)
226
+ try:
227
+ record = transition_record(record, record.state, owned_lock_identity=identity, owned_lock_digest=record.candidate_index_identity or EMPTY_LOCK_DIGEST)
228
+ if digest_text(index_digest(index_filename)) != record.original_index_identity:
229
+ raise ValueError("Index changed before locking; build a new preview")
230
+ content = read_index(preparation.index.candidate_filename)
231
+ if digest_text(index_digest(preparation.index.candidate_filename)) != record.candidate_index_identity:
232
+ raise ValueError("Candidate index changed; build a new preview")
233
+ owned_descriptor, descriptor = descriptor, None
234
+ fill_owned_lock(owned_descriptor, content if content is not None else b"")
235
+ return record
236
+ except (OSError, ValueError, RuntimeError):
237
+ if descriptor is not None:
238
+ os.close(descriptor)
239
+ if lock_identity(lock_filename) == identity and digest_text(index_digest(lock_filename)) in (EMPTY_LOCK_DIGEST, record.owned_lock_digest):
240
+ release_owned_index_lock(lock_filename, None, identity[:FILE_OBJECT_IDENTITY_FIELDS])
241
+ raise
242
+
243
+
244
+ def _validate_locked_index(record: RewriteRecoveryRecord, operation: OperationIntent) -> None:
245
+ """Check the exact source bytes and lock identity immediately before publication."""
246
+ index_filename = os.path.join(operation.context.git_dir, "index")
247
+ if lock_identity(index_filename + ".lock") != record.owned_lock_identity:
248
+ raise ValueError("Index lock was replaced by another writer; inspect it before retrying")
249
+ if digest_text(index_digest(index_filename)) != record.original_index_identity:
250
+ raise ValueError("Index changed outside the owned lock; inspect it before retrying")
251
+ if digest_text(index_digest(index_filename + ".lock")) != record.owned_lock_digest:
252
+ raise ValueError("Index lock content changed; inspect it before retrying")
253
+
254
+
255
+ def _read_named_head(record: RewriteRecoveryRecord, operation: OperationIntent) -> Generator[GitCommand | FileTask, object, tuple]:
256
+ """Bind the expected published references to fresh native HEAD evidence before index installation."""
257
+ authority = yield FileTask(lambda: reference_signature(operation.context))
258
+ output = yield GitCommand(["for-each-ref", "--format=" + REFERENCE_FORMAT, LOCAL_PREFIX])
259
+ reference = parse_reference_read(output)
260
+ if reference.object_oid_by_name != record.new_oid_by_ref:
261
+ raise ValueError("Local branches changed after publication; the index was not published. Inspect references and refresh recovery before continuing.")
262
+ if any(entry.symbolic_target is not None for entry in reference.references):
263
+ raise ValueError("A local branch became symbolic; the index was not published. Inspect references and refresh recovery before continuing.")
264
+ head = (yield GitCommand(["symbolic-ref", "--quiet", "HEAD"], accepted_codes=(0, 1))).removesuffix(b"\n")
265
+ if not head:
266
+ raise ValueError("HEAD is detached. The index was not published; inspect recovery and check out the original branch before continuing.")
267
+ if os.fsdecode(head) != record.head_ref:
268
+ raise ValueError("HEAD changed before index publication; inspect the active branch and refresh recovery before continuing")
269
+ if (yield FileTask(lambda: reference_signature(operation.context))) != authority:
270
+ raise ValueError("References or HEAD changed during reading; the index was not published. Refresh recovery before continuing.")
271
+ return authority
272
+
273
+
274
+ def _publish_index(record: RewriteRecoveryRecord, operation: OperationIntent) -> None:
275
+ """Replace only the verified owned index lock, leaving absent indexes absent."""
276
+ _validate_locked_index(record, operation)
277
+ index_filename = os.path.join(operation.context.git_dir, "index")
278
+ if record.candidate_index_identity is None:
279
+ release_recovery_lock(record, operation.context)
280
+ else:
281
+ os.replace(index_filename + ".lock", index_filename)
282
+ sync_directory(operation.context.git_dir)
283
+
284
+
285
+ def cleanup_private_refs(record: RewriteRecoveryRecord, observation: RecoveryObservation, *, before_start: Callable[[], None] | None = None) -> Generator[GitCommand, object]:
286
+ """Delete only operation-owned references whose actual values still match their journal."""
287
+ commands = [b"start\n"]
288
+ for name, oid in observation.private_ref_by_name.items():
289
+ if record.private_ref_by_name.get(name) != oid:
290
+ raise ValueError("A private recovery reference changed; retain recovery data for inspection")
291
+ commands.append(b"delete " + name.encode("ascii") + b" " + oid.encode("ascii") + b"\n")
292
+ if len(commands) == 1:
293
+ return
294
+ commands.extend((b"prepare\n", b"commit\n"))
295
+ yield rewrite_command(record, ["update-ref", "--no-deref", "--stdin"], stdin=b"".join(commands), before_start=before_start)
296
+
297
+
298
+ def _lock_compensation(record: RewriteRecoveryRecord, operation: OperationIntent) -> RewriteRecoveryRecord:
299
+ """Hold the original index stable during the collective inverse reference transaction."""
300
+ index_filename = os.path.join(operation.context.git_dir, "index")
301
+ filename = index_filename + ".lock"
302
+ actual_identity = lock_identity(filename)
303
+ if actual_identity is None:
304
+ descriptor = os.open(filename, os.O_WRONLY | os.O_CREAT | os.O_EXCL, PRIVATE_FILE_MODE)
305
+ information = os.fstat(descriptor)
306
+ identity = information.st_dev, information.st_ino, stat.S_IFMT(information.st_mode)
307
+ os.close(descriptor)
308
+ record = transition_record(record, record.state, owned_lock_identity=identity, owned_lock_digest=EMPTY_LOCK_DIGEST)
309
+ elif actual_identity != record.owned_lock_identity:
310
+ raise ValueError("Another writer owns the index lock; recovery cannot restore references")
311
+ if digest_text(index_digest(filename)) != record.owned_lock_digest:
312
+ raise ValueError("Index lock content changed; inspect recovery without restoring branches")
313
+ if digest_text(index_digest(index_filename)) != record.original_index_identity:
314
+ raise ValueError("Index changed before compensation; inspect recovery without restoring branches")
315
+ return record
316
+
317
+
318
+ def compensate_rewrite(record: RewriteRecoveryRecord, observation: RecoveryObservation, operation: OperationIntent,
319
+ repository_operation: RepositoryOperationService) -> Generator[GitCommand | FileTask, object, RewritePublicationOutcome]:
320
+ """Restore all previous branches together only while a freshly observed incomplete result still matches."""
321
+ if record.state in (RewritePublicationState.COMPLETE, RewritePublicationState.RESTORED):
322
+ raise ValueError("A completed operation cannot be undone by recovery")
323
+ validate_recovery_authority(record, operation, repository_operation)
324
+ current = yield from observe_recovery(record, operation.context)
325
+ if current != observation or not current.restorable:
326
+ raise ValueError("Recovery state changed since confirmation; refresh recovery before restoring branches")
327
+ record = yield FileTask(lambda: _lock_compensation(record, operation))
328
+ record = yield FileTask(lambda: transition_record(record, RewritePublicationState.COMPENSATING))
329
+
330
+ def validate_compensation() -> None:
331
+ """Bind the inverse CAS to the observed worktrees, references, HEAD and original index."""
332
+ validate_recovery_authority(record, operation, repository_operation)
333
+ if reference_signature(operation.context) != current.authority:
334
+ raise ValueError("References or worktrees changed before restoration; refresh recovery before restoring branches")
335
+ # The observation already read symbolic HEAD through Git; its signature covers files and reftable.
336
+ index_filename = os.path.join(operation.context.git_dir, "index")
337
+ if (lock_identity(index_filename + ".lock") != record.owned_lock_identity or digest_text(index_digest(index_filename)) != record.original_index_identity
338
+ or digest_text(index_digest(index_filename + ".lock")) != record.owned_lock_digest):
339
+ raise ValueError("Index or lock changed before compensation; refresh recovery")
340
+
341
+ try:
342
+ yield rewrite_command(record, ["update-ref", "--no-deref", "--stdin"], stdin=reference_transaction(record.new_oid_by_ref, record.old_oid_by_ref),
343
+ before_start=validate_compensation)
344
+ after = yield from observe_recovery(record, operation.context)
345
+ if after.detail or after.ref_oid_by_name != record.old_oid_by_ref or after.index_identity != record.original_index_identity:
346
+ raise ValueError(after.detail or "Compensation could not be verified; retain recovery data")
347
+ record = yield from finish_coherent_recovery(record, after, RewritePublicationState.RESTORED, operation, repository_operation,
348
+ detail="Previous branches restored; the original index was retained")
349
+ except (OSError, ValueError, RuntimeError) as error:
350
+ return (yield from retain_recovery_failure(record, operation, error))
351
+ return RewritePublicationOutcome(operation.operation_id, OperationStatus.FAILED, record=record)
352
+
353
+
354
+ def publish_rewrite(preparation: RewritePreparation, operation: OperationIntent, repository_operation: RepositoryOperationService,
355
+ storage_root_dir: str, *, progress: Callable[[RewriteProgress], None] | None = None) -> Generator[GitCommand | FileTask, object, RewritePublicationOutcome]:
356
+ """Publish once under the caller's reservation, reporting only observed refs and index effects."""
357
+ record = None
358
+ try:
359
+ if progress is not None:
360
+ progress(RewriteProgress.CHECKING)
361
+ repository_operation.validate(operation)
362
+ yield FileTask(lambda: _validate_preparation(preparation, operation))
363
+ import_authority = yield FileTask(lambda: publication_authority(preparation.capture))
364
+ observed = yield from capture_rewrite_state(preparation.capture)
365
+ validate_rewrite_capture(preparation.capture, observed)
366
+ namespace = PRIVATE_REF_PREFIX + operation.operation_id + "/"
367
+ if (yield GitCommand(["for-each-ref", "--format=%(refname)", namespace])):
368
+ raise ValueError("Publication reference namespace already exists; build a new preview")
369
+ record = yield FileTask(lambda: _new_record(preparation, operation, storage_root_dir))
370
+ record = yield FileTask(lambda: transition_record(record, RewritePublicationState.IMPORTING))
371
+ if progress is not None:
372
+ progress(RewriteProgress.IMPORTING)
373
+
374
+ def validate_import() -> None:
375
+ """Reject authority changes between the final preview read and source object import."""
376
+ repository_operation.validate(operation)
377
+ validate_publication_authority(preparation.capture, import_authority)
378
+
379
+ yield rewrite_command(record, ["fast-import", "--quiet", "--export-marks=" + os.path.join(record.operation_dir, "marks.out")],
380
+ stdin_filename=os.path.join(record.operation_dir, "import.in"), before_start=validate_import)
381
+ yield FileTask(lambda: _verify_marks(preparation, record))
382
+ record = yield FileTask(lambda: transition_record(record, RewritePublicationState.IMPORTED))
383
+ record = yield FileTask(lambda: _acquire_index(record, preparation))
384
+ authority = yield FileTask(lambda: publication_authority(preparation.capture))
385
+ observed = yield from capture_rewrite_state(preparation.capture)
386
+ validate_rewrite_capture(preparation.capture, observed)
387
+ record = yield FileTask(lambda: transition_record(record, RewritePublicationState.REFS_PENDING))
388
+ if progress is not None:
389
+ progress(RewriteProgress.REFERENCES)
390
+
391
+ def validate_publication() -> None:
392
+ """Keep authorization, named HEAD and index checks adjacent to the collective CAS."""
393
+ repository_operation.validate(operation)
394
+ validate_publication_authority(preparation.capture, authority)
395
+ # The checked capture includes Git's symbolic HEAD, bound to this unchanged authority.
396
+ _validate_locked_index(record, operation)
397
+
398
+ yield rewrite_command(record, ["update-ref", "--no-deref", "--stdin"], stdin=reference_transaction(record.old_oid_by_ref, record.new_oid_by_ref),
399
+ before_start=validate_publication)
400
+ record = yield FileTask(lambda: transition_record(record, RewritePublicationState.REFS_COMMITTED))
401
+ index_authority = yield from _read_named_head(record, operation)
402
+ record = yield FileTask(lambda: transition_record(record, RewritePublicationState.INDEX_PENDING))
403
+ if progress is not None:
404
+ progress(RewriteProgress.INDEX)
405
+
406
+ def publish_index() -> None:
407
+ """Reject changes while the checked index installation waits for its file worker."""
408
+ repository_operation.validate(operation)
409
+ if reference_signature(operation.context) != index_authority:
410
+ raise ValueError("References or HEAD changed before index publication; refresh recovery before continuing")
411
+ _publish_index(record, operation)
412
+
413
+ yield FileTask(publish_index)
414
+ record = yield FileTask(lambda: transition_record(record, RewritePublicationState.INDEX_PUBLISHED))
415
+ observation = yield from observe_recovery(record, operation.context)
416
+ if observation.detail or observation.ref_oid_by_name != record.new_oid_by_ref or observation.index_identity != record.candidate_index_identity:
417
+ raise ValueError(observation.detail or "Published state changed before verification; inspect the recovery files")
418
+ yield from cleanup_private_refs(record, observation)
419
+ # Readers and external Git programs are not isolated by our reservation. Reobserve after cleanup too.
420
+ observation = yield from observe_recovery(record, operation.context)
421
+ if observation.detail or observation.ref_oid_by_name != record.new_oid_by_ref or observation.index_identity != record.candidate_index_identity:
422
+ raise ValueError(observation.detail or "Published state changed during cleanup; inspect the recovery files")
423
+ record = yield FileTask(lambda: transition_record(record, RewritePublicationState.COMPLETE))
424
+ return RewritePublicationOutcome(operation.operation_id, OperationStatus.SUCCESS, record=record)
425
+ except (OSError, ValueError, RuntimeError) as error:
426
+ detail = str(error)
427
+ status = OperationStatus.REFUSED if record is None else OperationStatus.UNKNOWN
428
+ if record is not None:
429
+ try:
430
+ reconciled = yield from recover_rewrite(record, operation, repository_operation, restore=True)
431
+ record = reconciled.record
432
+ status = reconciled.status
433
+ if reconciled.diagnostic is not None:
434
+ detail += "; " + reconciled.diagnostic.cause
435
+ except (OSError, ValueError, RuntimeError) as recovery_error:
436
+ detail += "; recovery inspection failed: " + str(recovery_error)
437
+ diagnostic = Diagnostic(operation.operation, operation.context.repository_dir, operation.git_filename, [], detail).prepare()
438
+ return RewritePublicationOutcome(operation.operation_id, status, diagnostic=diagnostic, record=record, stale_capture=status == OperationStatus.REFUSED)
439
+ finally:
440
+ if record is not None and record.owned_lock_identity is not None:
441
+ lock_filename = os.path.join(operation.context.git_dir, "index.lock")
442
+ # A replaced lock belongs to another writer; even failure cleanup must leave it alone.
443
+ if lock_identity(lock_filename) == record.owned_lock_identity and digest_text(index_digest(lock_filename)) == record.owned_lock_digest:
444
+ release_recovery_lock(record, operation.context)