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,320 @@
|
|
|
1
|
+
"""Transport history rewrite records without changing their bytes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from gitpane.config import COMMIT_HEADER_LIMIT_BYTES, HISTORY_REWRITE_SPOOL_LIMIT_BYTES, PROCESS_CHUNK_BYTES
|
|
9
|
+
from gitpane.history.repository_value import validate_oid
|
|
10
|
+
from gitpane.history_rewrite.value import ChangeKind, ExportCommit, FileChange, RawCommit, RewriteRefusal, RewriteRefusalKind, RewriteTargetKind, validate_path
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
14
|
+
from typing import BinaryIO
|
|
15
|
+
|
|
16
|
+
from gitpane.history_rewrite.value import ImportCommit, RewriteTarget
|
|
17
|
+
|
|
18
|
+
SIGNATURE_HEADER_SET = frozenset({b"gpgsig", b"gpgsig-sha256"})
|
|
19
|
+
SUPPORTED_HEADER_SET = frozenset({b"tree", b"parent", b"author", b"committer", b"encoding"}) | SIGNATURE_HEADER_SET
|
|
20
|
+
FILE_MODE_SET = frozenset({b"100644", b"100755", b"120000", b"160000"})
|
|
21
|
+
ESCAPE_BY_BYTE = {ord("a"): 7, ord("b"): 8, ord("t"): 9, ord("n"): 10, ord("v"): 11, ord("f"): 12, ord("r"): 13, ord('"'): 34, ord("\\"): 92}
|
|
22
|
+
OCTAL_ESCAPE_LENGTH = 3
|
|
23
|
+
PRINTABLE_BYTE_START = 32
|
|
24
|
+
PRINTABLE_BYTE_END = 127
|
|
25
|
+
MARK_PATTERN = re.compile(r":[1-9][0-9]*")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def check_cancelled(cancelled: Callable[[], bool] | None) -> None:
|
|
29
|
+
"""Stop CPU and file work between bounded blocks when its owner cancels."""
|
|
30
|
+
if cancelled is not None and cancelled():
|
|
31
|
+
raise ValueError("History preparation cancelled; source unchanged")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def read_line(stream: BinaryIO, cancelled: Callable[[], bool] | None = None, limit: int = COMMIT_HEADER_LIMIT_BYTES) -> bytes:
|
|
35
|
+
"""Read a bounded protocol line even when a transport returns short reads."""
|
|
36
|
+
result = bytearray()
|
|
37
|
+
while True:
|
|
38
|
+
check_cancelled(cancelled)
|
|
39
|
+
chunk = stream.readline(limit + 1 - len(result))
|
|
40
|
+
if not chunk:
|
|
41
|
+
if result:
|
|
42
|
+
raise ValueError("Truncated Git protocol line")
|
|
43
|
+
return b""
|
|
44
|
+
result.extend(chunk)
|
|
45
|
+
if len(result) > limit:
|
|
46
|
+
raise ValueError("Git header exceeds the supported size; source unchanged")
|
|
47
|
+
if chunk.endswith(b"\n"):
|
|
48
|
+
return bytes(result)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def copy_bytes(source: BinaryIO, length: int, destination: BinaryIO | None = None, cancelled: Callable[[], bool] | None = None) -> None:
|
|
52
|
+
"""Transfer an exact data block without retaining a large message in memory."""
|
|
53
|
+
while length:
|
|
54
|
+
check_cancelled(cancelled)
|
|
55
|
+
chunk = source.read(min(length, PROCESS_CHUNK_BYTES))
|
|
56
|
+
if not chunk:
|
|
57
|
+
raise ValueError("Truncated Git data record")
|
|
58
|
+
if destination is not None:
|
|
59
|
+
write_bytes(destination, chunk)
|
|
60
|
+
length -= len(chunk)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def write_bytes(destination: BinaryIO, content: bytes) -> None:
|
|
64
|
+
"""Refuse oversized or incomplete writes to a private import spool."""
|
|
65
|
+
if destination.tell() + len(content) > HISTORY_REWRITE_SPOOL_LIMIT_BYTES:
|
|
66
|
+
raise ValueError("History import exceeds its private file size limit")
|
|
67
|
+
if destination.write(content) != len(content):
|
|
68
|
+
raise OSError("Incomplete private import write")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def read_nul_records(stream: BinaryIO, cancelled: Callable[[], bool] | None = None) -> Iterator[bytes]:
|
|
72
|
+
"""Read bounded NUL-framed records without loading a complete batch response."""
|
|
73
|
+
pending = bytearray()
|
|
74
|
+
while chunk := stream.read(PROCESS_CHUNK_BYTES):
|
|
75
|
+
check_cancelled(cancelled)
|
|
76
|
+
pieces = chunk.split(b"\0")
|
|
77
|
+
first_piece = True
|
|
78
|
+
for piece in pieces:
|
|
79
|
+
if not first_piece:
|
|
80
|
+
yield bytes(pending)
|
|
81
|
+
pending.clear()
|
|
82
|
+
pending.extend(piece)
|
|
83
|
+
if len(pending) > COMMIT_HEADER_LIMIT_BYTES:
|
|
84
|
+
raise ValueError("Git path check exceeds the supported record size")
|
|
85
|
+
first_piece = False
|
|
86
|
+
if pending:
|
|
87
|
+
raise ValueError("Truncated Git path check")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _length(value: bytes) -> int:
|
|
91
|
+
if not value or not value.isdigit():
|
|
92
|
+
raise ValueError("Invalid Git data length")
|
|
93
|
+
return int(value)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _oid(value: bytes) -> str:
|
|
97
|
+
return validate_oid(value.decode("ascii"))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _reference(value: bytes) -> str:
|
|
101
|
+
"""Accept an object ID or numeric mark, never an arbitrary revision expression."""
|
|
102
|
+
reference = value.decode("ascii")
|
|
103
|
+
if MARK_PATTERN.fullmatch(reference):
|
|
104
|
+
return reference
|
|
105
|
+
return validate_oid(reference)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def decode_path(value: bytes) -> bytes:
|
|
109
|
+
"""Decode Git's C quoting without interpreting shell syntax or Unicode."""
|
|
110
|
+
if not value.startswith(b'"'):
|
|
111
|
+
return validate_path(value)
|
|
112
|
+
if not value.endswith(b'"') or len(value) < 2:
|
|
113
|
+
raise ValueError("Invalid quoted Git path")
|
|
114
|
+
content = value[1:-1]
|
|
115
|
+
result = bytearray()
|
|
116
|
+
position = 0
|
|
117
|
+
while position < len(content):
|
|
118
|
+
byte = content[position]
|
|
119
|
+
position += 1
|
|
120
|
+
if byte == ord('"'):
|
|
121
|
+
raise ValueError("Unescaped quote in Git path")
|
|
122
|
+
if byte != ord("\\"):
|
|
123
|
+
result.append(byte)
|
|
124
|
+
continue
|
|
125
|
+
if position == len(content):
|
|
126
|
+
raise ValueError("Incomplete Git path escape")
|
|
127
|
+
byte = content[position]
|
|
128
|
+
if byte in ESCAPE_BY_BYTE:
|
|
129
|
+
result.append(ESCAPE_BY_BYTE[byte])
|
|
130
|
+
position += 1
|
|
131
|
+
continue
|
|
132
|
+
octal = content[position:position + OCTAL_ESCAPE_LENGTH]
|
|
133
|
+
if len(octal) != OCTAL_ESCAPE_LENGTH or any(digit not in b"01234567" for digit in octal) or int(octal, 8) > 255:
|
|
134
|
+
raise ValueError("Invalid Git path escape")
|
|
135
|
+
result.append(int(octal, 8))
|
|
136
|
+
position += OCTAL_ESCAPE_LENGTH
|
|
137
|
+
return validate_path(bytes(result))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def encode_path(path: bytes) -> bytes:
|
|
141
|
+
"""Quote every special path byte so it cannot become an import directive."""
|
|
142
|
+
validate_path(path)
|
|
143
|
+
result = bytearray(b'"')
|
|
144
|
+
for byte in path:
|
|
145
|
+
if PRINTABLE_BYTE_START <= byte < PRINTABLE_BYTE_END and byte not in (ord('"'), ord("\\")):
|
|
146
|
+
result.append(byte)
|
|
147
|
+
else:
|
|
148
|
+
result.extend(f"\\{byte:03o}".encode("ascii"))
|
|
149
|
+
result.extend(b'"')
|
|
150
|
+
return bytes(result)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def matches_target(path: bytes, target: RewriteTarget) -> bool:
|
|
154
|
+
"""Match exact bytes or descendants separated by a slash."""
|
|
155
|
+
return path == target.path or (target.kind == RewriteTargetKind.FOLDER and path.startswith(target.path + b"/"))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def read_export(stream: BinaryIO, cancelled: Callable[[], bool] | None = None) -> list[ExportCommit]:
|
|
159
|
+
"""Parse only the closed no-data export grammar, requiring its final done record."""
|
|
160
|
+
if read_line(stream, cancelled) != b"feature done\n":
|
|
161
|
+
raise ValueError("Missing export done feature")
|
|
162
|
+
records: list[ExportCommit] = []
|
|
163
|
+
oid_set: set[str] = set()
|
|
164
|
+
mark_set: set[str] = set()
|
|
165
|
+
line = read_line(stream, cancelled)
|
|
166
|
+
while line != b"done\n":
|
|
167
|
+
check_cancelled(cancelled)
|
|
168
|
+
if line == b"\n":
|
|
169
|
+
line = read_line(stream, cancelled)
|
|
170
|
+
continue
|
|
171
|
+
if line.startswith(b"reset "):
|
|
172
|
+
line = read_line(stream, cancelled)
|
|
173
|
+
if line.startswith(b"from "):
|
|
174
|
+
_reference(line.removeprefix(b"from ").removesuffix(b"\n"))
|
|
175
|
+
line = read_line(stream, cancelled)
|
|
176
|
+
continue
|
|
177
|
+
if not line.startswith(b"commit "):
|
|
178
|
+
raise ValueError("Unknown or incomplete Git export record")
|
|
179
|
+
mark_line = read_line(stream, cancelled)
|
|
180
|
+
if not mark_line.startswith(b"mark "):
|
|
181
|
+
raise ValueError("Missing commit mark")
|
|
182
|
+
mark = _reference(mark_line.removeprefix(b"mark ").removesuffix(b"\n"))
|
|
183
|
+
if not mark.startswith(":") or mark in mark_set:
|
|
184
|
+
raise ValueError("Invalid or duplicate commit mark")
|
|
185
|
+
oid_line = read_line(stream, cancelled)
|
|
186
|
+
if not oid_line.startswith(b"original-oid "):
|
|
187
|
+
raise ValueError("Missing original commit ID")
|
|
188
|
+
oid = _oid(oid_line.removeprefix(b"original-oid ").removesuffix(b"\n"))
|
|
189
|
+
if oid in oid_set:
|
|
190
|
+
raise ValueError("Duplicate exported commit")
|
|
191
|
+
line = read_line(stream, cancelled)
|
|
192
|
+
if line.startswith(b"author "):
|
|
193
|
+
line = read_line(stream, cancelled)
|
|
194
|
+
if not line.startswith(b"committer "):
|
|
195
|
+
raise ValueError("Missing export committer")
|
|
196
|
+
line = read_line(stream, cancelled)
|
|
197
|
+
if line.startswith(b"encoding "):
|
|
198
|
+
line = read_line(stream, cancelled)
|
|
199
|
+
if not line.startswith(b"data "):
|
|
200
|
+
raise ValueError("Missing export message")
|
|
201
|
+
copy_bytes(stream, _length(line.removeprefix(b"data ").removesuffix(b"\n")), cancelled=cancelled)
|
|
202
|
+
line = read_line(stream, cancelled)
|
|
203
|
+
# An empty root has only its commit terminator here; keep the next command as lookahead.
|
|
204
|
+
next_command = False
|
|
205
|
+
if line == b"\n":
|
|
206
|
+
line = read_line(stream, cancelled)
|
|
207
|
+
next_command = line.startswith((b"commit ", b"reset ")) or line == b"done\n"
|
|
208
|
+
parent_refs: list[str] = []
|
|
209
|
+
if line.startswith(b"from "):
|
|
210
|
+
parent_refs.append(_reference(line.removeprefix(b"from ").removesuffix(b"\n")))
|
|
211
|
+
line = read_line(stream, cancelled)
|
|
212
|
+
while line.startswith(b"merge "):
|
|
213
|
+
parent_refs.append(_reference(line.removeprefix(b"merge ").removesuffix(b"\n")))
|
|
214
|
+
line = read_line(stream, cancelled)
|
|
215
|
+
changes: list[FileChange] = []
|
|
216
|
+
while line != b"\n" and not next_command:
|
|
217
|
+
if line.startswith(b"M "):
|
|
218
|
+
_, mode, object_oid, path = line.removesuffix(b"\n").split(b" ", 3)
|
|
219
|
+
if mode not in FILE_MODE_SET:
|
|
220
|
+
raise ValueError("Unsupported Git file mode")
|
|
221
|
+
changes.append(FileChange(ChangeKind.MODIFY, decode_path(path), mode, _oid(object_oid)))
|
|
222
|
+
elif line.startswith(b"D "):
|
|
223
|
+
changes.append(FileChange(ChangeKind.DELETE, decode_path(line.removeprefix(b"D ").removesuffix(b"\n"))))
|
|
224
|
+
else:
|
|
225
|
+
raise ValueError("Unsupported or truncated Git file directive")
|
|
226
|
+
line = read_line(stream, cancelled)
|
|
227
|
+
records.append(ExportCommit(oid, mark, tuple(parent_refs), tuple(changes)))
|
|
228
|
+
oid_set.add(oid)
|
|
229
|
+
mark_set.add(mark)
|
|
230
|
+
if not next_command:
|
|
231
|
+
line = read_line(stream, cancelled)
|
|
232
|
+
if stream.read(1):
|
|
233
|
+
raise ValueError("Unexpected bytes after export done")
|
|
234
|
+
return records
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def read_raw_commits(stream: BinaryIO, cancelled: Callable[[], bool] | None = None) -> dict[str, RawCommit]:
|
|
238
|
+
"""Read raw cat-file commit headers while keeping message bytes in their spool."""
|
|
239
|
+
commit_by_oid: dict[str, RawCommit] = {}
|
|
240
|
+
while line := read_line(stream, cancelled):
|
|
241
|
+
oid_raw, object_type, size_raw = line.removesuffix(b"\n").split(b" ")
|
|
242
|
+
oid = _oid(oid_raw)
|
|
243
|
+
if object_type != b"commit" or oid in commit_by_oid:
|
|
244
|
+
raise ValueError("Expected one complete raw commit per object ID")
|
|
245
|
+
size = _length(size_raw)
|
|
246
|
+
start = stream.tell()
|
|
247
|
+
value_by_header: dict[bytes, bytes] = {}
|
|
248
|
+
header_names: list[bytes] = []
|
|
249
|
+
parent_oids: list[str] = []
|
|
250
|
+
previous_header = b""
|
|
251
|
+
while True:
|
|
252
|
+
line = read_line(stream, cancelled)
|
|
253
|
+
consumed = stream.tell() - start
|
|
254
|
+
if consumed > size or consumed > COMMIT_HEADER_LIMIT_BYTES or not line:
|
|
255
|
+
raise ValueError(f"Invalid or oversized commit header for {oid}")
|
|
256
|
+
if line == b"\n":
|
|
257
|
+
break
|
|
258
|
+
if line.startswith(b" "):
|
|
259
|
+
if not previous_header or previous_header in SUPPORTED_HEADER_SET - SIGNATURE_HEADER_SET:
|
|
260
|
+
raise ValueError(f"Invalid header continuation for {oid}")
|
|
261
|
+
continue
|
|
262
|
+
header, separator, value = line.removesuffix(b"\n").partition(b" ")
|
|
263
|
+
if not separator or not header:
|
|
264
|
+
raise ValueError(f"Invalid raw commit header for {oid}")
|
|
265
|
+
if header == b"parent":
|
|
266
|
+
parent_oids.append(_oid(value))
|
|
267
|
+
elif header in value_by_header and header in SUPPORTED_HEADER_SET:
|
|
268
|
+
raise ValueError(f"Repeated commit header for {oid}")
|
|
269
|
+
value_by_header[header] = value
|
|
270
|
+
header_names.append(header)
|
|
271
|
+
previous_header = header
|
|
272
|
+
for required in (b"tree", b"author", b"committer"):
|
|
273
|
+
if required not in value_by_header:
|
|
274
|
+
raise ValueError(f"Missing commit header for {oid}")
|
|
275
|
+
offset = stream.tell()
|
|
276
|
+
length = size - (offset - start)
|
|
277
|
+
copy_bytes(stream, length, cancelled=cancelled)
|
|
278
|
+
if stream.read(1) != b"\n":
|
|
279
|
+
raise ValueError(f"Truncated raw commit {oid}")
|
|
280
|
+
commit_by_oid[oid] = RawCommit(oid, _oid(value_by_header[b"tree"]), tuple(parent_oids), value_by_header[b"author"], value_by_header[b"committer"],
|
|
281
|
+
value_by_header.get(b"encoding"), offset, length, tuple(header_names), bool(set(header_names) & SIGNATURE_HEADER_SET))
|
|
282
|
+
return commit_by_oid
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def write_import(destination: BinaryIO, commits: Iterable[ImportCommit], raw_source: BinaryIO, private_ref: bytes, cancelled: Callable[[], bool] | None = None) -> None:
|
|
286
|
+
"""Emit only generated private commits, restoring each original raw message."""
|
|
287
|
+
if not re.fullmatch(rb"refs/heads/[A-Za-z0-9_-]+(?:/[A-Za-z0-9_-]+)*", private_ref):
|
|
288
|
+
raise ValueError("Invalid private import reference")
|
|
289
|
+
write_bytes(destination, b"feature done\n")
|
|
290
|
+
for commit in commits:
|
|
291
|
+
check_cancelled(cancelled)
|
|
292
|
+
raw = commit.raw
|
|
293
|
+
for header in raw.header_names:
|
|
294
|
+
if header not in SUPPORTED_HEADER_SET:
|
|
295
|
+
raise RewriteRefusal(RewriteRefusalKind.UNSUPPORTED_METADATA,
|
|
296
|
+
f"Unsupported commit header {header.decode('ascii', 'replace')!r} in {raw.oid}; rewrite refused, use a compatible history editor")
|
|
297
|
+
if not MARK_PATTERN.fullmatch(commit.mark):
|
|
298
|
+
raise ValueError("Invalid import mark")
|
|
299
|
+
write_bytes(destination, b"reset " + private_ref + b"\n\ncommit " + private_ref + b"\nmark " + commit.mark.encode("ascii") + b"\nauthor " + raw.author
|
|
300
|
+
+ b"\ncommitter " + raw.committer + b"\n")
|
|
301
|
+
if raw.encoding is not None:
|
|
302
|
+
write_bytes(destination, b"encoding " + raw.encoding + b"\n")
|
|
303
|
+
write_bytes(destination, b"data " + str(raw.message_length).encode("ascii") + b"\n")
|
|
304
|
+
raw_source.seek(raw.message_offset)
|
|
305
|
+
copy_bytes(raw_source, raw.message_length, destination, cancelled)
|
|
306
|
+
write_bytes(destination, b"\n")
|
|
307
|
+
first_parent = True
|
|
308
|
+
for parent_ref in commit.parent_refs:
|
|
309
|
+
reference = _reference(parent_ref.encode("ascii"))
|
|
310
|
+
write_bytes(destination, (b"from " if first_parent else b"merge ") + reference.encode("ascii") + b"\n")
|
|
311
|
+
first_parent = False
|
|
312
|
+
for change in commit.changes:
|
|
313
|
+
if change.kind == ChangeKind.MODIFY:
|
|
314
|
+
if change.mode not in FILE_MODE_SET:
|
|
315
|
+
raise ValueError("Invalid import mode")
|
|
316
|
+
write_bytes(destination, b"M " + change.mode + b" " + validate_oid(change.oid).encode("ascii") + b" " + encode_path(change.path) + b"\n")
|
|
317
|
+
else:
|
|
318
|
+
write_bytes(destination, b"D " + encode_path(change.path) + b"\n")
|
|
319
|
+
write_bytes(destination, b"\n")
|
|
320
|
+
write_bytes(destination, b"done\n")
|