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