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,224 @@
|
|
|
1
|
+
"""Keep the file Git handed to the rebase editor byte-exact from its read to the write of the user's text."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import codecs
|
|
6
|
+
import os
|
|
7
|
+
import stat
|
|
8
|
+
import tempfile
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
12
|
+
from gitpane.config import QT_PARAGRAPH_SEPARATOR
|
|
13
|
+
from gitpane.system.path_identity import link_resolved_path
|
|
14
|
+
from gitpane.worktree.worktree_file import SINGLE_LINK_COUNT
|
|
15
|
+
|
|
16
|
+
# Git writes its instruction list and its messages in UTF-8 unless the repository asks for another commit encoding.
|
|
17
|
+
EDIT_FILE_CODEC = "utf-8"
|
|
18
|
+
EDIT_FILE_TEMPORARY_PREFIX = "gitpane-rebase-edit-"
|
|
19
|
+
# The editor widget turns a carriage return and a paragraph separator into a plain line break, and drops a byte order
|
|
20
|
+
# mark at the start of the document: text holding one of them would come back with bytes the user never changed.
|
|
21
|
+
CARRIAGE_RETURN_CHARACTER = "\r"
|
|
22
|
+
BYTE_ORDER_MARK_CHARACTER = "\ufeff"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ReadOnlyCause(Enum):
|
|
26
|
+
"""Why a file Git waits for can be shown but not written back; each value is the line the editor window shows above it."""
|
|
27
|
+
|
|
28
|
+
UNDECODABLE = "Read-only: the file is in an encoding GitPane cannot read back exactly."
|
|
29
|
+
CARRIAGE_RETURN = "Read-only: the file has carriage returns GitPane cannot keep."
|
|
30
|
+
BYTE_ORDER_MARK = "Read-only: the file starts with a byte order mark GitPane cannot keep."
|
|
31
|
+
PARAGRAPH_SEPARATOR = "Read-only: the file holds a paragraph separator GitPane cannot keep."
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def label(self) -> str:
|
|
35
|
+
"""Answer the line the editor window shows above a file it keeps read-only."""
|
|
36
|
+
return self.value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class EditFileFailure(Enum):
|
|
40
|
+
"""Why the file Git waits for could not be read or written; each value is the fixed start of the cause shown to the user."""
|
|
41
|
+
|
|
42
|
+
CHANGED = "The file Git is waiting for changed."
|
|
43
|
+
# The codec is named, never the character it cannot hold.
|
|
44
|
+
UNENCODABLE = "The text cannot be written in {codec}."
|
|
45
|
+
CREATE = "Cannot create the new file next to the file Git is waiting for:"
|
|
46
|
+
WRITE = "Cannot write the new file:"
|
|
47
|
+
REPLACE = "Cannot replace the file Git is waiting for:"
|
|
48
|
+
# Out of reach in practice, os.name being posix or nt, both in the table of open flags.
|
|
49
|
+
UNKNOWN_SYSTEM = "GitPane does not know how to open the file Git is waiting for on this system."
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class EditFileWriteError(OSError):
|
|
53
|
+
"""A write of the file Git waits for that did not happen; the file is left as it was.
|
|
54
|
+
|
|
55
|
+
Its cause is a readable sentence, never the content of the file, and is also the error's strerror so that code
|
|
56
|
+
reading system errors shows it as is.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, cause: str, error_number: int | None = None) -> None:
|
|
60
|
+
super().__init__(error_number, cause)
|
|
61
|
+
self.cause = cause
|
|
62
|
+
|
|
63
|
+
def __str__(self) -> str:
|
|
64
|
+
return self.cause
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _system_failure(failure: EditFileFailure, error: OSError) -> EditFileWriteError:
|
|
68
|
+
"""Turn the system error of one write step into a write error naming the step, then the system's reason."""
|
|
69
|
+
return EditFileWriteError(failure.value + " " + (error.strerror or str(error)), error.errno)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def read_only_cause(text: str) -> ReadOnlyCause | None:
|
|
73
|
+
"""Answer why the editor could not give this text back unchanged, or None when it can.
|
|
74
|
+
|
|
75
|
+
The editor widget changes a carriage return into a line break, cannot tell a paragraph separator from a line break
|
|
76
|
+
once it holds the text, and drops a byte order mark at the start of the document; any of them would rewrite bytes
|
|
77
|
+
the user never touched.
|
|
78
|
+
"""
|
|
79
|
+
if text.startswith(BYTE_ORDER_MARK_CHARACTER):
|
|
80
|
+
return ReadOnlyCause.BYTE_ORDER_MARK
|
|
81
|
+
if CARRIAGE_RETURN_CHARACTER in text:
|
|
82
|
+
return ReadOnlyCause.CARRIAGE_RETURN
|
|
83
|
+
if QT_PARAGRAPH_SEPARATOR in text:
|
|
84
|
+
return ReadOnlyCause.PARAGRAPH_SEPARATOR
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class EditFileContent:
|
|
90
|
+
"""The text of one file Git waits for, with what writing it back needs: its codec, the identity it was read from and its permission bits."""
|
|
91
|
+
|
|
92
|
+
filename: str
|
|
93
|
+
text: str
|
|
94
|
+
codec: str
|
|
95
|
+
identity: tuple[int, int]
|
|
96
|
+
mode: int
|
|
97
|
+
read_only_cause: ReadOnlyCause | None = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
EDIT_FILE_OPEN_FLAG_NAMES_BY_OS_NAME: dict[str, tuple[str, ...]] = {
|
|
101
|
+
# Binary mode, so the C library of Windows neither turns CR LF into LF nor stops reading at the first Ctrl+Z byte.
|
|
102
|
+
"nt": ("O_RDONLY", "O_BINARY"),
|
|
103
|
+
# The path can become a FIFO after Git wrote it: reading must never wait for a writer, nor follow a link put in place of the file.
|
|
104
|
+
"posix": ("O_RDONLY", "O_NONBLOCK", "O_NOFOLLOW"),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def edit_file_open_flags(os_name: str) -> int:
|
|
109
|
+
"""Answer the flags, for the system named like os.name, that open the file Git waits for byte for byte and without any wait.
|
|
110
|
+
|
|
111
|
+
The file is read in binary mode on Windows, and without waiting on a FIFO or following a replacement link on a
|
|
112
|
+
POSIX system. The flag names come from the table by system and are resolved on the os module of the running Python,
|
|
113
|
+
at the call: the function is meant to be called with os.name, and the name of another system can miss one of its
|
|
114
|
+
flags there (AttributeError), which is why the tests put the Windows flag in place. A system the table does not
|
|
115
|
+
know raises OSError.
|
|
116
|
+
"""
|
|
117
|
+
flag_names = EDIT_FILE_OPEN_FLAG_NAMES_BY_OS_NAME.get(os_name)
|
|
118
|
+
if flag_names is None:
|
|
119
|
+
raise OSError(EditFileFailure.UNKNOWN_SYSTEM.value)
|
|
120
|
+
# Each flag only exists in the os module of its own system, and the function is called with os.name; reading them
|
|
121
|
+
# at the call, rather than at import, also lets a test put the Windows flag in place.
|
|
122
|
+
flags = 0
|
|
123
|
+
for flag_name in flag_names:
|
|
124
|
+
flags |= getattr(os, flag_name)
|
|
125
|
+
return flags
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _same_single_file(status: os.stat_result, identity: tuple[int, int] | None) -> bool:
|
|
129
|
+
"""Tell whether a status is the regular file with one link that the bridge validated."""
|
|
130
|
+
return stat.S_ISREG(status.st_mode) and status.st_nlink == SINGLE_LINK_COUNT and identity == (status.st_dev, status.st_ino)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _decoded(data: bytes, fallback_codec: str) -> tuple[str, str] | None:
|
|
134
|
+
"""Decode the bytes in strict UTF-8, else in the fallback text codec, answering the text and the codec's normalized name.
|
|
135
|
+
|
|
136
|
+
None means neither decodes: no known text codec is given, or the bytes are not valid in it.
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
return data.decode(EDIT_FILE_CODEC), codecs.lookup(EDIT_FILE_CODEC).name
|
|
140
|
+
except UnicodeDecodeError:
|
|
141
|
+
pass
|
|
142
|
+
if not fallback_codec:
|
|
143
|
+
return None
|
|
144
|
+
try:
|
|
145
|
+
codec = codecs.lookup(fallback_codec).name
|
|
146
|
+
# bytes.decode refuses a codec that is not a text encoding (rot13, base64) with LookupError.
|
|
147
|
+
return data.decode(codec), codec
|
|
148
|
+
except (LookupError, UnicodeDecodeError):
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def read_edit_file(filename: str, identity: tuple[int, int] | None, fallback_codec: str = "") -> EditFileContent:
|
|
153
|
+
"""Read the file Git waits for, refusing any other file than the one the bridge validated.
|
|
154
|
+
|
|
155
|
+
The bytes are decoded in strict UTF-8, else with the fallback codec (the repository's commit encoding, in which Git
|
|
156
|
+
writes a commit message). A file neither decodes is still shown, decoded with replacement characters, and a text the
|
|
157
|
+
editor could not give back unchanged is shown too, but both are marked read-only: writing them back would change
|
|
158
|
+
bytes the user never touched.
|
|
159
|
+
"""
|
|
160
|
+
descriptor = os.open(filename, edit_file_open_flags(os.name))
|
|
161
|
+
with os.fdopen(descriptor, "rb") as stream:
|
|
162
|
+
opened = os.fstat(stream.fileno())
|
|
163
|
+
if not _same_single_file(opened, identity):
|
|
164
|
+
raise OSError(EditFileFailure.CHANGED.value)
|
|
165
|
+
data = stream.read()
|
|
166
|
+
mode = stat.S_IMODE(opened.st_mode)
|
|
167
|
+
file_identity = (opened.st_dev, opened.st_ino)
|
|
168
|
+
decoded = _decoded(data, fallback_codec)
|
|
169
|
+
if decoded is None:
|
|
170
|
+
return EditFileContent(filename, data.decode(EDIT_FILE_CODEC, errors="replace"), EDIT_FILE_CODEC, file_identity, mode, ReadOnlyCause.UNDECODABLE)
|
|
171
|
+
text, codec = decoded
|
|
172
|
+
return EditFileContent(filename, text, codec, file_identity, mode, read_only_cause(text))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _verify_unchanged(content: EditFileContent) -> None:
|
|
176
|
+
"""Refuse to replace the file when its path no longer names the single regular file that was read: a link, another file, a second link or no file at all."""
|
|
177
|
+
try:
|
|
178
|
+
current = os.lstat(content.filename)
|
|
179
|
+
except OSError:
|
|
180
|
+
raise EditFileWriteError(EditFileFailure.CHANGED.value) from None
|
|
181
|
+
if not _same_single_file(current, content.identity) or link_resolved_path(content.filename) != os.path.normcase(content.filename):
|
|
182
|
+
raise EditFileWriteError(EditFileFailure.CHANGED.value)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def write_edit_file(content: EditFileContent, text: str) -> EditFileContent:
|
|
186
|
+
"""Replace the file Git waits for with the user's text in one step, or leave it untouched when the text did not change.
|
|
187
|
+
|
|
188
|
+
An unchanged text writes nothing, so Git reads back exactly the bytes it wrote. A changed text goes to a private
|
|
189
|
+
file of the same folder, which takes the permission bits of the original and replaces it only once it is complete
|
|
190
|
+
on disk and the original is proven to be the file that was read. Any failure raises EditFileWriteError with a
|
|
191
|
+
readable cause, leaves the original as it was and removes the private file. A read-only content raises ValueError.
|
|
192
|
+
"""
|
|
193
|
+
if text == content.text:
|
|
194
|
+
return content
|
|
195
|
+
if content.read_only_cause is not None:
|
|
196
|
+
raise ValueError(content.read_only_cause.label)
|
|
197
|
+
try:
|
|
198
|
+
data = text.encode(content.codec)
|
|
199
|
+
except UnicodeEncodeError:
|
|
200
|
+
# The encoding error holds the whole text; dropping it keeps the user's text out of any diagnostic.
|
|
201
|
+
raise EditFileWriteError(EditFileFailure.UNENCODABLE.value.format(codec=content.codec)) from None
|
|
202
|
+
try:
|
|
203
|
+
descriptor, candidate_filename = tempfile.mkstemp(prefix=EDIT_FILE_TEMPORARY_PREFIX, dir=os.path.dirname(content.filename))
|
|
204
|
+
except OSError as error:
|
|
205
|
+
raise _system_failure(EditFileFailure.CREATE, error) from error
|
|
206
|
+
try:
|
|
207
|
+
try:
|
|
208
|
+
with os.fdopen(descriptor, "wb") as stream:
|
|
209
|
+
os.fchmod(stream.fileno(), content.mode)
|
|
210
|
+
stream.write(data)
|
|
211
|
+
stream.flush()
|
|
212
|
+
os.fsync(stream.fileno())
|
|
213
|
+
written = os.fstat(stream.fileno())
|
|
214
|
+
except OSError as error:
|
|
215
|
+
raise _system_failure(EditFileFailure.WRITE, error) from error
|
|
216
|
+
_verify_unchanged(content)
|
|
217
|
+
try:
|
|
218
|
+
os.replace(candidate_filename, content.filename)
|
|
219
|
+
except OSError as error:
|
|
220
|
+
raise _system_failure(EditFileFailure.REPLACE, error) from error
|
|
221
|
+
finally:
|
|
222
|
+
if os.path.lexists(candidate_filename):
|
|
223
|
+
os.unlink(candidate_filename)
|
|
224
|
+
return EditFileContent(content.filename, text, content.codec, (written.st_dev, written.st_ino), content.mode)
|