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
|
File without changes
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Read Git's refusal to check out a path of a submodule.
|
|
2
|
+
|
|
3
|
+
Git writes two whole lines when the checkout of a submodule refuses a path: the refused path, then the submodule it
|
|
4
|
+
could not check out. Only those whole lines count, never the same words inside another sentence, so the output of a
|
|
5
|
+
server or of a hook cannot make GitPane believe a checkout was refused.
|
|
6
|
+
|
|
7
|
+
Git writes the same fatal line for any checkout of a submodule that fails, on a locked index, a name too long for the
|
|
8
|
+
file system or a full disk; such a fatal line counts only after the start of a line that cites a refused path. A failure
|
|
9
|
+
of another kind adds no folder to the refused ones and stays a failure that Retry submodules takes up again.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
from gitpane.config import GIT_PATH_SEPARATOR
|
|
19
|
+
from gitpane.system.path_identity import canonical_path
|
|
20
|
+
|
|
21
|
+
# Git's own two lines when the checkout of a submodule refuses a path: the refused path, then the submodule that could
|
|
22
|
+
# not be checked out. A fatal line counts only after the start of a refused path's line, since Git writes it for any
|
|
23
|
+
# failed checkout of a submodule. Only a whole line counts, so a server's sentence that quotes these words does not. A
|
|
24
|
+
# file name that holds a line break can still make these two lines; it adds at most one folder GitPane leaves unwritten,
|
|
25
|
+
# so nothing is ever written because of them. Git writes the line break of a refused path as it is, which cuts the first
|
|
26
|
+
# line in two: that path gets no sentence of GitPane's, but the start of its line stays at the start of a line, and its
|
|
27
|
+
# submodule is still recognised by the fatal line that follows, which stays whole. The path is matched up to the last
|
|
28
|
+
# quote, since a file name may itself hold a quote.
|
|
29
|
+
INVALID_PATH_START = "error: invalid path '"
|
|
30
|
+
INVALID_PATH_PATTERN = re.compile("^" + re.escape(INVALID_PATH_START) + r"(?P<path>.*)'$")
|
|
31
|
+
SUBMODULE_CHECKOUT_FAILED_PATTERN = re.compile(r"^fatal: Unable to checkout '(?P<oid>[0-9a-f]+)' in submodule path '(?P<submodule>.*)'$")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _git_lines(stderr_text: str) -> list[str]:
|
|
35
|
+
"""Split Git's error output into lines, Windows line ends and the carriage returns of progress lines becoming plain line ends."""
|
|
36
|
+
return stderr_text.replace("\r\n", "\n").replace("\r", "\n").split("\n")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class CheckoutRefusal:
|
|
41
|
+
"""Name a submodule Git could not check out and the paths it refused there, in the order Git printed them."""
|
|
42
|
+
|
|
43
|
+
submodule_path: str
|
|
44
|
+
refused_paths: tuple[str, ...]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def checkout_refusals(stderr_text: str) -> tuple[CheckoutRefusal, ...]:
|
|
48
|
+
"""Pair every refused path Git printed with the submodule checkout failure that follows it.
|
|
49
|
+
|
|
50
|
+
Windows line ends and the carriage returns that end Git's progress lines become plain line ends first, so each line
|
|
51
|
+
is matched whole on every system. A checkout failure with no refused path before it says nothing about a file name
|
|
52
|
+
and gives no refusal.
|
|
53
|
+
"""
|
|
54
|
+
refusals: list[CheckoutRefusal] = []
|
|
55
|
+
pending_paths: list[str] = []
|
|
56
|
+
for line in _git_lines(stderr_text):
|
|
57
|
+
invalid = INVALID_PATH_PATTERN.match(line)
|
|
58
|
+
if invalid is not None:
|
|
59
|
+
pending_paths.append(invalid.group("path"))
|
|
60
|
+
continue
|
|
61
|
+
failed = SUBMODULE_CHECKOUT_FAILED_PATTERN.match(line)
|
|
62
|
+
if failed is not None and pending_paths:
|
|
63
|
+
refusals.append(CheckoutRefusal(failed.group("submodule"), tuple(pending_paths)))
|
|
64
|
+
pending_paths = []
|
|
65
|
+
return tuple(refusals)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def refused_checkout_submodule_paths(stderr_text: str) -> tuple[str, ...]:
|
|
69
|
+
"""Return the path of every submodule whose checkout Git refused on a file name, in the order Git printed them.
|
|
70
|
+
|
|
71
|
+
A submodule counts when a line that starts as Git's refused path line comes before its whole fatal line, even when a
|
|
72
|
+
line break in the refused path cut that line in two: its submodule must then be left as Git left it. The fatal line
|
|
73
|
+
alone is not enough, since Git writes it for every checkout of a submodule that fails, on a locked index, a name too
|
|
74
|
+
long for the file system or a full disk; such a submodule is not refused and stays to be checked out again. Each
|
|
75
|
+
fatal line ends what was read for its submodule, so a refusal never carries over to the next one.
|
|
76
|
+
"""
|
|
77
|
+
submodule_paths: list[str] = []
|
|
78
|
+
refusal_started = False
|
|
79
|
+
for line in _git_lines(stderr_text):
|
|
80
|
+
if line.startswith(INVALID_PATH_START):
|
|
81
|
+
refusal_started = True
|
|
82
|
+
continue
|
|
83
|
+
failed = SUBMODULE_CHECKOUT_FAILED_PATTERN.match(line)
|
|
84
|
+
if failed is None:
|
|
85
|
+
continue
|
|
86
|
+
if refusal_started:
|
|
87
|
+
submodule_paths.append(failed.group("submodule"))
|
|
88
|
+
refusal_started = False
|
|
89
|
+
return tuple(submodule_paths)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def refused_submodule_dirs(submodule_paths: tuple[str, ...], base_dir: str) -> list[str]:
|
|
93
|
+
"""Return the folders of the submodules whose checkout Git refused, so a later checkout of the others can leave them alone.
|
|
94
|
+
|
|
95
|
+
``submodule_paths`` are the submodules as Git names them in its refusal, by their path from the repository it ran in,
|
|
96
|
+
nested submodules included, so ``base_dir`` is the folder of that repository. The folders are canonical, to compare
|
|
97
|
+
with the folders a submodule read gives.
|
|
98
|
+
"""
|
|
99
|
+
directories = []
|
|
100
|
+
for submodule_path in submodule_paths:
|
|
101
|
+
directories.append(canonical_path(os.path.join(base_dir, *submodule_path.split(GIT_PATH_SEPARATOR))))
|
|
102
|
+
return directories
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Say in plain words why Git refused to check out a path of a submodule.
|
|
2
|
+
|
|
3
|
+
A submodule whose checkout Git refused is explained from the lines Git wrote, naming only the paths Git cited, with no
|
|
4
|
+
file name rule of GitPane's own: the rules of Windows are read by Python's ntpath, and only to pick which reason a path
|
|
5
|
+
Git already refused gets.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ntpath
|
|
11
|
+
import unicodedata
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from gitpane.config import CONTROL_CHARACTER_CATEGORY, GIT_PATH_SEPARATOR, WINDOWS_PLATFORM, SharedText
|
|
15
|
+
from gitpane.process.git_output_text import GIT_ERROR_DECODE_ERRORS
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from gitpane.submodule.checkout_refusal import CheckoutRefusal
|
|
19
|
+
|
|
20
|
+
# A control character left in a path Git cited is written as \x and two hexadecimal digits, so it can neither move the
|
|
21
|
+
# console's cursor nor vanish from the text. Every control character of Unicode fits in two digits.
|
|
22
|
+
CONTROL_CHARACTER_ESCAPE_FORMAT = r"\x{code:02x}"
|
|
23
|
+
# A byte that was not UTF-8 in Git's error output is kept as one character from U+DC80 to U+DCFF, which no console or
|
|
24
|
+
# log could write: it is written back as the byte it stands for, in the same \x form.
|
|
25
|
+
UNDECODED_BYTE_FIRST_CODE = 0xDC80
|
|
26
|
+
UNDECODED_BYTE_LAST_CODE = 0xDCFF
|
|
27
|
+
UNDECODED_BYTE_INDEX = 0
|
|
28
|
+
# The folder names Git never writes inside a repository, on every system, compared without case as Git does. git~1 is
|
|
29
|
+
# the short name Windows can give a .git folder.
|
|
30
|
+
PROTECTED_FOLDER_NAME_SET = frozenset({".git", "git~1"})
|
|
31
|
+
# The file names Git refuses to write as a symbolic link, on every system. Git refuses them only as a link, so a refusal
|
|
32
|
+
# of one of these names means a link.
|
|
33
|
+
PROTECTED_LINK_NAME_SET = frozenset({".gitmodules", ".gitattributes", ".gitignore", ".mailmap"})
|
|
34
|
+
# The characters Windows drops from the end of a name, so ".git." and ".git " name the .git folder there; Git compares
|
|
35
|
+
# without them on every system.
|
|
36
|
+
WINDOWS_DROPPED_NAME_END = ". "
|
|
37
|
+
LAST_COMPONENT_INDEX = -1
|
|
38
|
+
# What Git writes in its error lines for each control character of a path, except the tab and the line break.
|
|
39
|
+
GIT_CONTROL_CHARACTER_MARK = "?"
|
|
40
|
+
CHECKOUT_REFUSAL_PART_SEPARATOR = " "
|
|
41
|
+
CHECKOUT_REFUSAL_LINE_SEPARATOR = "\n"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def displayed_git_path(path_text: str) -> str:
|
|
45
|
+
r"""Return a path Git cited as plain text that shows every character, for the console and the forms.
|
|
46
|
+
|
|
47
|
+
Each control character, and each byte that was not UTF-8 and stayed undecoded, is written as \x and two hexadecimal
|
|
48
|
+
digits. Everything else is kept, including a \xNN text the path really holds and the replacement character a
|
|
49
|
+
repository name may really hold.
|
|
50
|
+
"""
|
|
51
|
+
characters: list[str] = []
|
|
52
|
+
for character in path_text:
|
|
53
|
+
if UNDECODED_BYTE_FIRST_CODE <= ord(character) <= UNDECODED_BYTE_LAST_CODE:
|
|
54
|
+
byte = character.encode("utf-8", GIT_ERROR_DECODE_ERRORS)[UNDECODED_BYTE_INDEX]
|
|
55
|
+
characters.append(CONTROL_CHARACTER_ESCAPE_FORMAT.format(code=byte))
|
|
56
|
+
continue
|
|
57
|
+
if unicodedata.category(character) == CONTROL_CHARACTER_CATEGORY:
|
|
58
|
+
characters.append(CONTROL_CHARACTER_ESCAPE_FORMAT.format(code=ord(character)))
|
|
59
|
+
continue
|
|
60
|
+
characters.append(character)
|
|
61
|
+
return "".join(characters)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _protected_name(component: str) -> str:
|
|
65
|
+
"""Return a folder name as Git compares it with the folder names it protects: without case, without the dots and spaces Windows drops at its end."""
|
|
66
|
+
return component.rstrip(WINDOWS_DROPPED_NAME_END).lower()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def checkout_refusal_reason(path_text: str, platform: str) -> SharedText:
|
|
70
|
+
"""Say why Git refused to write a path on the given system, named by the caller as Python names the running one.
|
|
71
|
+
|
|
72
|
+
A .git component, or a name Windows reads as one, is refused by Git itself on every system, so it is never put on
|
|
73
|
+
the file system. Under Windows, a path with a part Windows refuses is then put on Windows, even when it ends with a
|
|
74
|
+
name Git protects. A path whose last part is exactly .gitmodules, .gitattributes, .gitignore or .mailmap, without
|
|
75
|
+
case, is refused by Git too, as a symbolic link. Otherwise Windows is named only on Windows, where its file name
|
|
76
|
+
rules are the usual cause; elsewhere the reason stays neutral.
|
|
77
|
+
"""
|
|
78
|
+
components = path_text.split(GIT_PATH_SEPARATOR)
|
|
79
|
+
for component in components:
|
|
80
|
+
if _protected_name(component) in PROTECTED_FOLDER_NAME_SET:
|
|
81
|
+
return SharedText.CHECKOUT_REFUSED_PROTECTED_PATH
|
|
82
|
+
# Python's own reading of the Windows name rules: dots and spaces at the end of a part, forbidden characters such as
|
|
83
|
+
# : and ?, device names such as CON. ntpath reads a start like "x:" as a drive and a backslash as a separator, so those
|
|
84
|
+
# two forms keep the reason of the link; no repository has a reason to hold them.
|
|
85
|
+
if platform == WINDOWS_PLATFORM and ntpath.isreserved(path_text):
|
|
86
|
+
return SharedText.CHECKOUT_REFUSED_WINDOWS_NAME
|
|
87
|
+
if components[LAST_COMPONENT_INDEX].lower() in PROTECTED_LINK_NAME_SET:
|
|
88
|
+
return SharedText.CHECKOUT_REFUSED_PROTECTED_LINK
|
|
89
|
+
if platform == WINDOWS_PLATFORM:
|
|
90
|
+
return SharedText.CHECKOUT_REFUSED_WINDOWS_NAME
|
|
91
|
+
return SharedText.CHECKOUT_REFUSED_OTHER_NAME
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def checkout_refusal_text(refusals: tuple[CheckoutRefusal, ...], platform: str) -> str:
|
|
95
|
+
"""Return the sentence that explains each submodule Git could not check out, one line per submodule, or an empty text.
|
|
96
|
+
|
|
97
|
+
Each line names the submodule, gives one reason per path Git cited, in Git's order, and ends by saying that other
|
|
98
|
+
files may be in the same case and that the rest of the clone is kept. Only what Git wrote is named. Git replaces
|
|
99
|
+
each control character of its error lines by ?, except the tab and the line break, on every system: a cited path
|
|
100
|
+
that holds ? gets a sentence that says so after its reason, and no other path does.
|
|
101
|
+
"""
|
|
102
|
+
lines: list[str] = []
|
|
103
|
+
for refusal in refusals:
|
|
104
|
+
reasons: list[str] = []
|
|
105
|
+
for path in refusal.refused_paths:
|
|
106
|
+
reasons.append(checkout_refusal_reason(path, platform).value.format(path=displayed_git_path(path)))
|
|
107
|
+
if GIT_CONTROL_CHARACTER_MARK in path:
|
|
108
|
+
reasons.append(SharedText.CHECKOUT_REFUSED_QUESTION_MARK.value)
|
|
109
|
+
start = SharedText.SUBMODULE_CHECKOUT_REFUSED_START.value.format(submodule=displayed_git_path(refusal.submodule_path))
|
|
110
|
+
lines.append(start + CHECKOUT_REFUSAL_PART_SEPARATOR.join(reasons) + CHECKOUT_REFUSAL_PART_SEPARATOR + SharedText.SUBMODULE_CHECKOUT_REFUSED_END.value)
|
|
111
|
+
return CHECKOUT_REFUSAL_LINE_SEPARATOR.join(lines)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Tell from the disk alone whether Git cloned a submodule but never checked out its files.
|
|
2
|
+
|
|
3
|
+
Git clones every submodule of a recursive clone first, then checks them out one by one, and stops at the first
|
|
4
|
+
checkout it cannot finish: the submodules after that one keep their Git folder, with the history and a HEAD, but no
|
|
5
|
+
index, and their folder holds nothing but the ``.git`` file that points to that Git folder. Only that exact state is
|
|
6
|
+
recognised here, so a write GitPane starts on such a submodule can never reach a file the user put there. No Git
|
|
7
|
+
process is started; any doubt answers no.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import stat
|
|
14
|
+
|
|
15
|
+
from gitpane.sequencer.replay_state import HEAD_BASENAME, LIVE_INDEX_BASENAME
|
|
16
|
+
from gitpane.submodule.submodule_loss import GIT_ENTRY_BASENAME
|
|
17
|
+
|
|
18
|
+
# What Git writes at the start of the .git file of a submodule whose Git folder lives in its parent's Git folder.
|
|
19
|
+
GIT_FILE_PREFIX = "gitdir:"
|
|
20
|
+
# A .git file holds one short line; anything larger is not one Git wrote, and is never read whole.
|
|
21
|
+
GIT_FILE_SIZE_LIMIT_BYTES = 4096
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def linked_git_dir(git_filename: str) -> str:
|
|
25
|
+
"""Return the Git folder a submodule's ``.git`` file names, made absolute, or an empty text when it is not such a file."""
|
|
26
|
+
metadata = os.lstat(git_filename)
|
|
27
|
+
if not stat.S_ISREG(metadata.st_mode) or metadata.st_size > GIT_FILE_SIZE_LIMIT_BYTES:
|
|
28
|
+
return ""
|
|
29
|
+
with open(git_filename, "rb") as stream:
|
|
30
|
+
line = stream.read(GIT_FILE_SIZE_LIMIT_BYTES).decode("utf-8").strip()
|
|
31
|
+
if not line.startswith(GIT_FILE_PREFIX):
|
|
32
|
+
return ""
|
|
33
|
+
git_dir = line.removeprefix(GIT_FILE_PREFIX).strip()
|
|
34
|
+
if not git_dir:
|
|
35
|
+
return ""
|
|
36
|
+
if not os.path.isabs(git_dir):
|
|
37
|
+
git_dir = os.path.join(os.path.dirname(git_filename), git_dir)
|
|
38
|
+
return os.path.normpath(git_dir)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def is_cloned_without_checkout(submodule_dir: str) -> bool:
|
|
42
|
+
"""Tell whether Git cloned this submodule but never wrote its files, so checking it out cannot overwrite anything.
|
|
43
|
+
|
|
44
|
+
True only when the folder is a real folder (not a link or a junction) whose only entry is a ``.git`` file, and the
|
|
45
|
+
Git folder that file names has a HEAD and no index. A folder that holds any other file, a ``.git`` folder, a Git
|
|
46
|
+
folder with an index, or anything that cannot be read answers False.
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
metadata = os.lstat(submodule_dir)
|
|
50
|
+
if not stat.S_ISDIR(metadata.st_mode) or getattr(metadata, "st_reparse_tag", 0):
|
|
51
|
+
return False
|
|
52
|
+
basenames = []
|
|
53
|
+
with os.scandir(submodule_dir) as entries:
|
|
54
|
+
for entry in entries:
|
|
55
|
+
basenames.append(entry.name)
|
|
56
|
+
if entry.name != GIT_ENTRY_BASENAME:
|
|
57
|
+
return False
|
|
58
|
+
if basenames != [GIT_ENTRY_BASENAME]:
|
|
59
|
+
return False
|
|
60
|
+
git_dir = linked_git_dir(os.path.join(submodule_dir, GIT_ENTRY_BASENAME))
|
|
61
|
+
if not git_dir:
|
|
62
|
+
return False
|
|
63
|
+
return os.path.isfile(os.path.join(git_dir, HEAD_BASENAME)) and not os.path.lexists(os.path.join(git_dir, LIVE_INDEX_BASENAME))
|
|
64
|
+
except (OSError, UnicodeDecodeError, ValueError):
|
|
65
|
+
return False
|