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.
- gitpane/__init__.py +0 -0
- gitpane/account/__init__.py +0 -0
- gitpane/account/account_choice.py +297 -0
- gitpane/account/address_owner.py +67 -0
- gitpane/account/authentication.py +1811 -0
- gitpane/account/authentication_worker.py +836 -0
- gitpane/account/credential_manager.py +480 -0
- gitpane/account/credential_manager_install.py +171 -0
- gitpane/account/keyring_reader.py +135 -0
- gitpane/account/keyring_state.py +158 -0
- gitpane/account/ssh.py +312 -0
- gitpane/account/ssh_askpass.py +216 -0
- gitpane/account/ssh_prompt.py +222 -0
- gitpane/application.py +684 -0
- gitpane/commit/__init__.py +0 -0
- gitpane/commit/commit_metadata.py +88 -0
- gitpane/commit/commit_push.py +276 -0
- gitpane/commit/commit_write.py +830 -0
- gitpane/config.py +1066 -0
- gitpane/controller/__init__.py +0 -0
- gitpane/controller/account_controller.py +677 -0
- gitpane/controller/clone_controller.py +776 -0
- gitpane/controller/close_readiness.py +212 -0
- gitpane/controller/commit_timing_controller.py +566 -0
- gitpane/controller/conflict_controller.py +885 -0
- gitpane/controller/console_registry.py +38 -0
- gitpane/controller/emoji_window_controller.py +95 -0
- gitpane/controller/file_action_controller.py +479 -0
- gitpane/controller/history_read_controller.py +671 -0
- gitpane/controller/history_rewrite_controller.py +739 -0
- gitpane/controller/navigation_controller.py +658 -0
- gitpane/controller/opening_controller.py +375 -0
- gitpane/controller/operation_policy.py +76 -0
- gitpane/controller/rebase_controller.py +797 -0
- gitpane/controller/rebase_editor_controller.py +309 -0
- gitpane/controller/reference_form_controller.py +1677 -0
- gitpane/controller/reference_read_controller.py +242 -0
- gitpane/controller/refresh_controller.py +477 -0
- gitpane/controller/replay_controller.py +805 -0
- gitpane/controller/session_registry.py +337 -0
- gitpane/controller/setting_controller.py +592 -0
- gitpane/controller/shutdown_controller.py +345 -0
- gitpane/controller/staging_controller.py +199 -0
- gitpane/controller/staging_session_registry.py +47 -0
- gitpane/controller/stash_controller.py +367 -0
- gitpane/controller/transport_controller.py +813 -0
- gitpane/controller/welcome_controller.py +102 -0
- gitpane/controller/window_activation.py +116 -0
- gitpane/controller/worktree_state_controller.py +169 -0
- gitpane/emoji/__init__.py +0 -0
- gitpane/emoji/emoji_catalogue.py +156 -0
- gitpane/git_config/__init__.py +0 -0
- gitpane/git_config/git_setting.py +441 -0
- gitpane/history/__init__.py +0 -0
- gitpane/history/avatar.py +241 -0
- gitpane/history/file_history.py +359 -0
- gitpane/history/graph.py +145 -0
- gitpane/history/history_filter.py +31 -0
- gitpane/history/history_parse.py +605 -0
- gitpane/history/repository.py +1600 -0
- gitpane/history/repository_value.py +341 -0
- gitpane/history/revision.py +180 -0
- gitpane/history/revision_file.py +182 -0
- gitpane/history_rewrite/__init__.py +0 -0
- gitpane/history_rewrite/graph.py +210 -0
- gitpane/history_rewrite/guard.py +288 -0
- gitpane/history_rewrite/index.py +125 -0
- gitpane/history_rewrite/prepare.py +378 -0
- gitpane/history_rewrite/protocol.py +320 -0
- gitpane/history_rewrite/publish.py +444 -0
- gitpane/history_rewrite/recovery.py +481 -0
- gitpane/history_rewrite/value.py +229 -0
- gitpane/operation/__init__.py +0 -0
- gitpane/operation/console_line.py +313 -0
- gitpane/operation/console_tone.py +76 -0
- gitpane/operation/diagnostic.py +954 -0
- gitpane/operation/launch_signature.py +254 -0
- gitpane/operation/operation_status.py +87 -0
- gitpane/operation/operation_trace.py +766 -0
- gitpane/operation/repository_operation.py +781 -0
- gitpane/preference/__init__.py +0 -0
- gitpane/preference/preference_location.py +40 -0
- gitpane/preference/preference_store.py +590 -0
- gitpane/process/__init__.py +0 -0
- gitpane/process/exact_arguments.py +77 -0
- gitpane/process/git_output_text.py +20 -0
- gitpane/process/process_group.py +851 -0
- gitpane/process/process_log.py +114 -0
- gitpane/process/process_runner.py +1033 -0
- gitpane/process/released_child_watch.py +138 -0
- gitpane/recent/__init__.py +0 -0
- gitpane/recent/mount_table.py +422 -0
- gitpane/recent/recent_availability.py +414 -0
- gitpane/recent/recent_name.py +11 -0
- gitpane/recent/recent_search.py +114 -0
- gitpane/reference/__init__.py +0 -0
- gitpane/reference/reference_edit.py +2030 -0
- gitpane/reference/reference_snapshot.py +196 -0
- gitpane/reference/stash.py +730 -0
- gitpane/reference/stash_list.py +164 -0
- gitpane/sequencer/__init__.py +0 -0
- gitpane/sequencer/commit_message_rule.py +154 -0
- gitpane/sequencer/conflict.py +506 -0
- gitpane/sequencer/merge_recovery.py +163 -0
- gitpane/sequencer/merge_state.py +166 -0
- gitpane/sequencer/mergetool.py +856 -0
- gitpane/sequencer/rebase.py +750 -0
- gitpane/sequencer/rebase_edit_file.py +224 -0
- gitpane/sequencer/rebase_state.py +523 -0
- gitpane/sequencer/replay.py +1435 -0
- gitpane/sequencer/replay_legacy.py +253 -0
- gitpane/sequencer/replay_state.py +747 -0
- gitpane/submodule/__init__.py +0 -0
- gitpane/submodule/checkout_refusal.py +102 -0
- gitpane/submodule/checkout_refusal_notice.py +111 -0
- gitpane/submodule/cloned_without_checkout.py +65 -0
- gitpane/submodule/submodule_discovery.py +1180 -0
- gitpane/submodule/submodule_loss.py +218 -0
- gitpane/system/__init__.py +0 -0
- gitpane/system/appearance_portal.py +125 -0
- gitpane/system/application_identity.py +48 -0
- gitpane/system/command_line.py +36 -0
- gitpane/system/desktop_icon_fact.py +128 -0
- gitpane/system/desktop_icon_support.py +270 -0
- gitpane/system/desktop_shortcut.py +536 -0
- gitpane/system/icns_image.py +35 -0
- gitpane/system/installed_launcher.py +136 -0
- gitpane/system/linux_distribution.py +151 -0
- gitpane/system/network_folder.py +46 -0
- gitpane/system/path_identity.py +95 -0
- gitpane/system/private_tree_removal.py +47 -0
- gitpane/system/process_program.py +63 -0
- gitpane/system/qt_folder.py +38 -0
- gitpane/system/ready_channel_watch.py +67 -0
- gitpane/system/startup_activation.py +65 -0
- gitpane/system/terminal_detach.py +384 -0
- gitpane/system/xdg_session.py +85 -0
- gitpane/text/__init__.py +0 -0
- gitpane/text/text_query.py +97 -0
- gitpane/text/typed_path.py +75 -0
- gitpane/text/typed_text.py +17 -0
- gitpane/thread/__init__.py +0 -0
- gitpane/thread/weak_signal.py +93 -0
- gitpane/tool/__init__.py +0 -0
- gitpane/tool/app_bundle.py +164 -0
- gitpane/tool/desktop_text_editor.py +156 -0
- gitpane/tool/difftool.py +429 -0
- gitpane/tool/editor.py +445 -0
- gitpane/tool/editor_bridge.py +193 -0
- gitpane/tool/editor_recipe.py +481 -0
- gitpane/tool/file_reveal.py +94 -0
- gitpane/tool/git_executable.py +264 -0
- gitpane/tool/git_version.py +62 -0
- gitpane/tool/kdiff3.py +41 -0
- gitpane/tool/kdiff3_install.py +162 -0
- gitpane/tool/launch_services_text_editor.py +104 -0
- gitpane/tool/mergetool_recipe.py +84 -0
- gitpane/tool/system_editor_default.py +80 -0
- gitpane/tool/system_text_editor.py +44 -0
- gitpane/tool/system_tool.py +816 -0
- gitpane/tool/terminal_recipe.py +390 -0
- gitpane/transport/__init__.py +0 -0
- gitpane/transport/branch_exchange.py +1690 -0
- gitpane/transport/clone.py +1273 -0
- gitpane/transport/credential_read_retry.py +35 -0
- gitpane/transport/git_refusal.py +207 -0
- gitpane/transport/push_tracking.py +121 -0
- gitpane/transport/refusal_notice.py +225 -0
- gitpane/transport/remote.py +747 -0
- gitpane/transport/remote_config.py +544 -0
- gitpane/ui/__init__.py +0 -0
- gitpane/ui/console/__init__.py +0 -0
- gitpane/ui/console/error_dialog.py +332 -0
- gitpane/ui/console/operation_dialog.py +408 -0
- gitpane/ui/console/operation_output.py +91 -0
- gitpane/ui/console/operation_output_view.py +373 -0
- gitpane/ui/emoji/__init__.py +0 -0
- gitpane/ui/emoji/emoji_font.py +257 -0
- gitpane/ui/emoji/emoji_grid.py +125 -0
- gitpane/ui/emoji/emoji_window.py +154 -0
- gitpane/ui/emoji/file_action_icon.py +131 -0
- gitpane/ui/emoji/opentype_family_name.py +182 -0
- gitpane/ui/history_rewrite/__init__.py +0 -0
- gitpane/ui/history_rewrite/details.py +78 -0
- gitpane/ui/history_rewrite/preview.py +565 -0
- gitpane/ui/history_rewrite/preview_model.py +291 -0
- gitpane/ui/history_rewrite/question.py +87 -0
- gitpane/ui/history_rewrite/recovery_dialog.py +262 -0
- gitpane/ui/look/__init__.py +0 -0
- gitpane/ui/look/action_icon.py +504 -0
- gitpane/ui/look/application_icon.py +78 -0
- gitpane/ui/look/application_style.py +317 -0
- gitpane/ui/look/avatar_label.py +53 -0
- gitpane/ui/look/color_contrast.py +43 -0
- gitpane/ui/look/desktop_appearance_follower.py +118 -0
- gitpane/ui/look/fixed_font.py +21 -0
- gitpane/ui/look/gtk_settings.py +116 -0
- gitpane/ui/look/icon_density.py +16 -0
- gitpane/ui/look/reference_font.py +19 -0
- gitpane/ui/look/theme_color.py +63 -0
- gitpane/ui/look/theme_look.py +95 -0
- gitpane/ui/look/theme_palette.py +173 -0
- gitpane/ui/look/warning_icon.py +86 -0
- gitpane/ui/look/windows_control_look.py +250 -0
- gitpane/ui/reader/__init__.py +0 -0
- gitpane/ui/reader/commit_message.py +129 -0
- gitpane/ui/reader/commit_panel.py +378 -0
- gitpane/ui/reader/diff_panel.py +626 -0
- gitpane/ui/reader/file_action.py +138 -0
- gitpane/ui/reader/file_tree_panel.py +235 -0
- gitpane/ui/reader/reader_toolbar.py +56 -0
- gitpane/ui/reader/revision_tree.py +80 -0
- gitpane/ui/reader/search_result.py +165 -0
- gitpane/ui/reader/source_text.py +134 -0
- gitpane/ui/reader/source_view.py +151 -0
- gitpane/ui/reader/syntax_span.py +67 -0
- gitpane/ui/reference/__init__.py +0 -0
- gitpane/ui/reference/branch_selector.py +106 -0
- gitpane/ui/reference/checkout_dialog.py +419 -0
- gitpane/ui/reference/checkout_revision_dialog.py +182 -0
- gitpane/ui/reference/commit_summary.py +339 -0
- gitpane/ui/reference/create_branch_dialog.py +346 -0
- gitpane/ui/reference/create_tag_dialog.py +283 -0
- gitpane/ui/reference/delete_local_branch_dialog.py +323 -0
- gitpane/ui/reference/delete_tag_dialog.py +394 -0
- gitpane/ui/reference/form_opening.py +53 -0
- gitpane/ui/reference/reference_dialog.py +417 -0
- gitpane/ui/reference/reference_feedback.py +62 -0
- gitpane/ui/reference/reference_form.py +139 -0
- gitpane/ui/reference/rename_branch_dialog.py +179 -0
- gitpane/ui/reference/reset_branch_dialog.py +430 -0
- gitpane/ui/reference/reset_dialog.py +263 -0
- gitpane/ui/reference/revision_dialog.py +121 -0
- gitpane/ui/reference/revision_picker.py +227 -0
- gitpane/ui/reference/select_branches_dialog.py +199 -0
- gitpane/ui/reference/stash_dialog.py +171 -0
- gitpane/ui/repository/__init__.py +0 -0
- gitpane/ui/repository/graph_delegate.py +298 -0
- gitpane/ui/repository/history_avatar.py +130 -0
- gitpane/ui/repository/history_model.py +179 -0
- gitpane/ui/repository/repository_list_popup.py +319 -0
- gitpane/ui/repository/repository_window.py +2580 -0
- gitpane/ui/resource/ArrowDown.png +0 -0
- gitpane/ui/resource/ArrowUp.png +0 -0
- gitpane/ui/resource/BranchCheckout.png +0 -0
- gitpane/ui/resource/BranchCreate.png +0 -0
- gitpane/ui/resource/BranchDelete.png +0 -0
- gitpane/ui/resource/CopyToClipboard.png +0 -0
- gitpane/ui/resource/GitPane-16.png +0 -0
- gitpane/ui/resource/GitPane-24.png +0 -0
- gitpane/ui/resource/GitPane-256.png +0 -0
- gitpane/ui/resource/GitPane-32.png +0 -0
- gitpane/ui/resource/GitPane-48.png +0 -0
- gitpane/ui/resource/GitPane-64.png +0 -0
- gitpane/ui/resource/GitPane.ico +0 -0
- gitpane/ui/resource/NotoColorEmoji-OFL.txt +92 -0
- gitpane/ui/resource/NotoColorEmoji_WindowsCompatible.ttf +0 -0
- gitpane/ui/resource/Renamed.png +0 -0
- gitpane/ui/resource/ResetAnotherBranchToHere.png +0 -0
- gitpane/ui/resource/ResetCurrentBranchToHere.png +0 -0
- gitpane/ui/resource/Stash.png +0 -0
- gitpane/ui/resource/Tag.png +0 -0
- gitpane/ui/resource/TagCreate.png +0 -0
- gitpane/ui/resource/TagDelete.png +0 -0
- gitpane/ui/resource/Unmerged.png +0 -0
- gitpane/ui/resource/checkout.png +0 -0
- gitpane/ui/sequencer/__init__.py +0 -0
- gitpane/ui/sequencer/conflict_dialog.py +532 -0
- gitpane/ui/sequencer/merge_recovery_dialog.py +67 -0
- gitpane/ui/sequencer/rebase_dialog.py +461 -0
- gitpane/ui/sequencer/rebase_editor_window.py +220 -0
- gitpane/ui/sequencer/replay_conflict_prompt.py +105 -0
- gitpane/ui/sequencer/replay_dialog.py +501 -0
- gitpane/ui/setting/__init__.py +0 -0
- gitpane/ui/setting/account_page.py +724 -0
- gitpane/ui/setting/add_account_dialog.py +1005 -0
- gitpane/ui/setting/credential_manager_dialog.py +151 -0
- gitpane/ui/setting/git_setting_widget.py +388 -0
- gitpane/ui/setting/setting_block.py +19 -0
- gitpane/ui/setting/setting_dialog.py +750 -0
- gitpane/ui/setting/ssh_widget.py +232 -0
- gitpane/ui/setting/utility_page.py +240 -0
- gitpane/ui/staging/__init__.py +0 -0
- gitpane/ui/staging/staging_action.py +356 -0
- gitpane/ui/staging/staging_window.py +745 -0
- gitpane/ui/transport/__init__.py +0 -0
- gitpane/ui/transport/clone_dialog.py +879 -0
- gitpane/ui/transport/push_dialog.py +580 -0
- gitpane/ui/transport/push_rejection_prompt.py +100 -0
- gitpane/ui/transport/push_tracking_question.py +88 -0
- gitpane/ui/transport/remote_dialog.py +943 -0
- gitpane/ui/welcome/__init__.py +0 -0
- gitpane/ui/welcome/invalid_repository_prompt.py +114 -0
- gitpane/ui/welcome/welcome_window.py +1137 -0
- gitpane/ui/widget/__init__.py +0 -0
- gitpane/ui/widget/busy_indicator.py +106 -0
- gitpane/ui/widget/commit_date_text.py +55 -0
- gitpane/ui/widget/content_growth.py +262 -0
- gitpane/ui/widget/content_measure.py +99 -0
- gitpane/ui/widget/dialog_layout.py +668 -0
- gitpane/ui/widget/drop_down_wheel_filter.py +32 -0
- gitpane/ui/widget/flow_layout.py +110 -0
- gitpane/ui/widget/long_text.py +542 -0
- gitpane/ui/widget/mnemonic.py +13 -0
- gitpane/ui/widget/numbered_text_edit.py +127 -0
- gitpane/ui/widget/plain_label.py +15 -0
- gitpane/ui/widget/plain_tooltip.py +19 -0
- gitpane/ui/widget/refresh_advice.py +25 -0
- gitpane/ui/widget/result_label.py +227 -0
- gitpane/ui/widget/result_status_bar.py +53 -0
- gitpane/ui/widget/scrolled_text.py +69 -0
- gitpane/ui/widget/shortcut_text.py +31 -0
- gitpane/ui/widget/standard_icon_label.py +55 -0
- gitpane/ui/widget/system_shortcut.py +75 -0
- gitpane/ui/widget/unavailable_reason.py +29 -0
- gitpane/ui/widget/wheel_safe_drop_down.py +24 -0
- gitpane/ui/window_control/__init__.py +0 -0
- gitpane/ui/window_control/button_focus.py +42 -0
- gitpane/ui/window_control/compositor_answer.py +152 -0
- gitpane/ui/window_control/compositor_growth_cap.py +73 -0
- gitpane/ui/window_control/desktop_activation.py +203 -0
- gitpane/ui/window_control/dialog_modality.py +122 -0
- gitpane/ui/window_control/item_activation.py +42 -0
- gitpane/ui/window_control/modal_focus_relay.py +124 -0
- gitpane/ui/window_control/wayland_activation.py +695 -0
- gitpane/ui/window_control/window_placement.py +305 -0
- gitpane/vendor/DISTLIB-LICENSE.txt +284 -0
- gitpane/vendor/w32.exe +0 -0
- gitpane/vendor/w64-arm.exe +0 -0
- gitpane/vendor/w64.exe +0 -0
- gitpane/worktree/__init__.py +0 -0
- gitpane/worktree/command_sequence.py +110 -0
- gitpane/worktree/file_action_target.py +61 -0
- gitpane/worktree/file_menu_action.py +52 -0
- gitpane/worktree/historical_restore.py +361 -0
- gitpane/worktree/ignore_rule.py +266 -0
- gitpane/worktree/mutation.py +1539 -0
- gitpane/worktree/partial_patch.py +435 -0
- gitpane/worktree/staged_reset.py +447 -0
- gitpane/worktree/staging.py +1418 -0
- gitpane/worktree/trash.py +302 -0
- gitpane/worktree/worktree_file.py +406 -0
- gitpane/worktree/worktree_service.py +584 -0
- gitpane/worktree/worktree_snapshot.py +630 -0
- gitpane-1.0.0.dist-info/METADATA +141 -0
- gitpane-1.0.0.dist-info/RECORD +353 -0
- gitpane-1.0.0.dist-info/WHEEL +5 -0
- gitpane-1.0.0.dist-info/entry_points.txt +2 -0
- gitpane-1.0.0.dist-info/licenses/LICENSE +21 -0
- gitpane-1.0.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +109 -0
- gitpane-1.0.0.dist-info/licenses/src/gitpane/ui/resource/NotoColorEmoji-OFL.txt +92 -0
- gitpane-1.0.0.dist-info/licenses/src/gitpane/vendor/DISTLIB-LICENSE.txt +284 -0
- 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)
|