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,309 @@
|
|
|
1
|
+
"""Serve each rebase edit request from its window until Git gets its answer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from PySide6.QtCore import QObject, Signal
|
|
9
|
+
from shiboken6 import isValid
|
|
10
|
+
|
|
11
|
+
from gitpane.config import LINE_SEPARATOR
|
|
12
|
+
from gitpane.sequencer.commit_message_rule import message_is_empty, read_commit_message_rule
|
|
13
|
+
from gitpane.sequencer.rebase_edit_file import read_edit_file, write_edit_file
|
|
14
|
+
from gitpane.sequencer.rebase_state import read_message_context
|
|
15
|
+
from gitpane.tool.editor import EditorAnswer, EditorRequestKind
|
|
16
|
+
from gitpane.ui.sequencer.rebase_editor_window import EditorValidation, RebaseEditorText, RebaseEditorWindow
|
|
17
|
+
from gitpane.worktree.command_sequence import FileTask
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Generator
|
|
21
|
+
|
|
22
|
+
from PySide6.QtWidgets import QWidget
|
|
23
|
+
|
|
24
|
+
from gitpane.history.repository_value import RepositoryContext
|
|
25
|
+
from gitpane.sequencer.commit_message_rule import CommitMessageRule
|
|
26
|
+
from gitpane.sequencer.rebase_edit_file import EditFileContent
|
|
27
|
+
from gitpane.sequencer.rebase_state import RebaseMessageCommit
|
|
28
|
+
from gitpane.tool.editor import EditorRequest, EditorService, PendingEdit
|
|
29
|
+
from gitpane.worktree.worktree_service import WorktreeService
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class ExpectedEdit:
|
|
33
|
+
"""What one rebase operation needs to show the edits Git asks for: its repository, its Git and how its windows validate."""
|
|
34
|
+
|
|
35
|
+
context: RepositoryContext
|
|
36
|
+
git_filename: str
|
|
37
|
+
validation: EditorValidation
|
|
38
|
+
parent_window: QWidget | None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class EditRead:
|
|
43
|
+
"""What the window of one edit needs: the file Git waits for and, for a commit message, the commits it is about and the repository's rule for an empty message."""
|
|
44
|
+
|
|
45
|
+
content: EditFileContent
|
|
46
|
+
commits: tuple[RebaseMessageCommit, ...] = ()
|
|
47
|
+
rule: CommitMessageRule | None = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Between the short hash of a commit and its subject in the header of a message window.
|
|
51
|
+
COMMIT_LINE_SEPARATOR = " "
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def message_context_lines(repository_dir: str, commits: tuple[RebaseMessageCommit, ...]) -> tuple[str, ...]:
|
|
55
|
+
"""Answer the header of a message window: the repository folder, then the short hash and subject of each commit the message is about."""
|
|
56
|
+
lines = [repository_dir]
|
|
57
|
+
for commit in commits:
|
|
58
|
+
lines.append(commit.short_oid + COMMIT_LINE_SEPARATOR + commit.subject)
|
|
59
|
+
return tuple(lines)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def edit_failure_reason(error: Exception) -> str:
|
|
63
|
+
"""Name why a read or a write failed without quoting the file's content or its path.
|
|
64
|
+
|
|
65
|
+
The system's own reason is kept for a system error; the errors raised by the file module carry fixed texts.
|
|
66
|
+
"""
|
|
67
|
+
if isinstance(error, OSError) and error.strerror:
|
|
68
|
+
return error.strerror
|
|
69
|
+
if isinstance(error, UnicodeError):
|
|
70
|
+
return RebaseEditorText.ENCODING_FAILED.value
|
|
71
|
+
return str(error)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def save_failure_text(error: Exception | None) -> str:
|
|
75
|
+
"""Answer the line a failed save shows: its fixed start, then why the file could not be written, never the text."""
|
|
76
|
+
cause = edit_failure_reason(error) if error is not None else RebaseEditorText.WRITE_GAVE_NO_RESULT.value
|
|
77
|
+
return RebaseEditorText.SAVE_FAILED.value.format(cause=cause)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class RebaseEditorController(QObject):
|
|
81
|
+
"""Turn the edits Git asks for during an expected rebase operation into windows, then their answers into the codes Git receives."""
|
|
82
|
+
|
|
83
|
+
# Tells that the window of an edit Git waits for is shown: the EditorRequest and its RebaseEditorWindow.
|
|
84
|
+
window_shown = Signal(object, object)
|
|
85
|
+
|
|
86
|
+
def __init__(self, *, editor: EditorService, worktree_service: WorktreeService, parent: QObject | None = None) -> None:
|
|
87
|
+
"""Receive the editor service whose requests it serves and the scheduler that reads and writes Git's files off the Qt thread.
|
|
88
|
+
|
|
89
|
+
The rebase coordinator builds it and says which operations it serves; a test builds it on bare services.
|
|
90
|
+
"""
|
|
91
|
+
super().__init__(parent)
|
|
92
|
+
self.editor = editor
|
|
93
|
+
self.worktree_service = worktree_service
|
|
94
|
+
self.expected_by_operation_id: dict[str, ExpectedEdit] = {}
|
|
95
|
+
self.window_by_operation_id: dict[str, RebaseEditorWindow] = {}
|
|
96
|
+
self.pending_by_operation_id: dict[str, PendingEdit] = {}
|
|
97
|
+
self.content_by_operation_id: dict[str, EditFileContent] = {}
|
|
98
|
+
# The repository's rule for an empty commit message, read when Git asked for the message the window shows.
|
|
99
|
+
self.rule_by_operation_id: dict[str, CommitMessageRule] = {}
|
|
100
|
+
# The problem a command was started to fix, shown once in the first window of that command.
|
|
101
|
+
self.error_text_by_operation_id: dict[str, str] = {}
|
|
102
|
+
# The error line a window opened with, such as Git's refusal of the instructions: a successful save shows it
|
|
103
|
+
# again instead of clearing the line.
|
|
104
|
+
self.opening_error_text_by_operation_id: dict[str, str] = {}
|
|
105
|
+
editor.edit_requested.connect(self._edit_requested)
|
|
106
|
+
editor.edit_cancelled.connect(self._edit_cancelled)
|
|
107
|
+
|
|
108
|
+
def expect_edits(self, operation_id: str, context: RepositoryContext, git_filename: str, validation: EditorValidation, parent_window: QWidget | None, error_text: str = "") -> None:
|
|
109
|
+
"""Serve the edits Git asks for during this operation, each in a window parented to the given one.
|
|
110
|
+
|
|
111
|
+
An error text, such as the problem Git found in the instructions, is shown above the text of the first window.
|
|
112
|
+
"""
|
|
113
|
+
self.expected_by_operation_id[operation_id] = ExpectedEdit(context, git_filename, validation, parent_window)
|
|
114
|
+
if error_text:
|
|
115
|
+
self.error_text_by_operation_id[operation_id] = error_text
|
|
116
|
+
else:
|
|
117
|
+
self.error_text_by_operation_id.pop(operation_id, None)
|
|
118
|
+
|
|
119
|
+
def raise_windows(self) -> None:
|
|
120
|
+
"""Bring every editor window still waiting for its answer to the front, so a close that waits for Git sends the user to it."""
|
|
121
|
+
for window in tuple(self.window_by_operation_id.values()):
|
|
122
|
+
if not isValid(window) or window.answered:
|
|
123
|
+
continue
|
|
124
|
+
window.show()
|
|
125
|
+
window.raise_()
|
|
126
|
+
window.activateWindow()
|
|
127
|
+
|
|
128
|
+
def forget(self, operation_id: str) -> None:
|
|
129
|
+
"""Stop serving this operation and close the window left from its last edit."""
|
|
130
|
+
self.expected_by_operation_id.pop(operation_id, None)
|
|
131
|
+
self.pending_by_operation_id.pop(operation_id, None)
|
|
132
|
+
self.content_by_operation_id.pop(operation_id, None)
|
|
133
|
+
self.rule_by_operation_id.pop(operation_id, None)
|
|
134
|
+
self.error_text_by_operation_id.pop(operation_id, None)
|
|
135
|
+
self.opening_error_text_by_operation_id.pop(operation_id, None)
|
|
136
|
+
window = self.window_by_operation_id.pop(operation_id, None)
|
|
137
|
+
if window is not None:
|
|
138
|
+
window.close_after_answer()
|
|
139
|
+
window.deleteLater()
|
|
140
|
+
|
|
141
|
+
def _edit_requested(self, pending: PendingEdit) -> None:
|
|
142
|
+
"""Read the file of an edit this controller expects; an edit of another operation is left to its own owner."""
|
|
143
|
+
request = pending.request
|
|
144
|
+
expected = self.expected_by_operation_id.get(request.operation_id)
|
|
145
|
+
if expected is None:
|
|
146
|
+
return
|
|
147
|
+
self.pending_by_operation_id[request.operation_id] = pending
|
|
148
|
+
self.content_by_operation_id.pop(request.operation_id, None)
|
|
149
|
+
self.rule_by_operation_id.pop(request.operation_id, None)
|
|
150
|
+
self.worktree_service.run(expected.context, expected.git_filename, self._read(request, expected.context.git_dir),
|
|
151
|
+
lambda read, error: self._show(pending, expected, read, error))
|
|
152
|
+
|
|
153
|
+
def _edit_cancelled(self, request: EditorRequest) -> None:
|
|
154
|
+
"""Close the window of a request whose bridge was lost; a click left on it no longer reaches Git."""
|
|
155
|
+
pending = self.pending_by_operation_id.get(request.operation_id)
|
|
156
|
+
if pending is None or pending.request is not request:
|
|
157
|
+
return
|
|
158
|
+
del self.pending_by_operation_id[request.operation_id]
|
|
159
|
+
window = self.window_by_operation_id.get(request.operation_id)
|
|
160
|
+
if window is not None:
|
|
161
|
+
window.close_after_answer()
|
|
162
|
+
|
|
163
|
+
def _read(self, request: EditorRequest, git_dir: str) -> Generator:
|
|
164
|
+
"""Read the file Git waits for off the Qt thread; for a commit message, read first the repository's message settings, then the commits it is about.
|
|
165
|
+
|
|
166
|
+
One git config call gives the comment prefix and cleanup the empty-message rule follows, and the commit encoding
|
|
167
|
+
a message that is not UTF-8 is decoded and written back with.
|
|
168
|
+
"""
|
|
169
|
+
if request.kind != EditorRequestKind.MESSAGE:
|
|
170
|
+
content = yield FileTask(lambda: read_edit_file(request.file_filename, request.file_identity))
|
|
171
|
+
return EditRead(content)
|
|
172
|
+
rule = yield from read_commit_message_rule()
|
|
173
|
+
content = yield FileTask(lambda: read_edit_file(request.file_filename, request.file_identity, fallback_codec=rule.commit_encoding))
|
|
174
|
+
commits = yield FileTask(lambda: read_message_context(git_dir))
|
|
175
|
+
return EditRead(content, commits, rule)
|
|
176
|
+
|
|
177
|
+
def _show(self, pending: PendingEdit, expected: ExpectedEdit, read: EditRead | None, error: Exception | None) -> None:
|
|
178
|
+
"""Open the window of a read edit, read-only with its reason when the file cannot be written back exactly.
|
|
179
|
+
|
|
180
|
+
A message window names above its text the repository and the commits the message is about.
|
|
181
|
+
"""
|
|
182
|
+
operation_id = pending.request.operation_id
|
|
183
|
+
content = read.content if read is not None else None
|
|
184
|
+
if pending.answered or self.pending_by_operation_id.get(operation_id) is not pending:
|
|
185
|
+
return
|
|
186
|
+
previous = self.window_by_operation_id.pop(operation_id, None)
|
|
187
|
+
if previous is not None:
|
|
188
|
+
previous.close_after_answer()
|
|
189
|
+
previous.deleteLater()
|
|
190
|
+
# A message always hands its text to Git: saving it while the rebase stays paused would mean nothing.
|
|
191
|
+
validation = EditorValidation.CONTINUE if pending.request.kind == EditorRequestKind.MESSAGE else expected.validation
|
|
192
|
+
window = RebaseEditorWindow(pending.request.kind, validation, content.text if content is not None else "", expected.context.repository_dir, expected.parent_window)
|
|
193
|
+
if pending.request.kind == EditorRequestKind.MESSAGE:
|
|
194
|
+
window.show_context(message_context_lines(expected.context.repository_dir, read.commits if read is not None else ()))
|
|
195
|
+
if error is not None:
|
|
196
|
+
window.set_read_only(RebaseEditorText.READ_FAILED.value.format(button=validation.label, cause=edit_failure_reason(error)))
|
|
197
|
+
elif content.read_only_cause is not None:
|
|
198
|
+
window.set_read_only(content.read_only_cause.label)
|
|
199
|
+
error_text = self.error_text_by_operation_id.pop(operation_id, "")
|
|
200
|
+
if error_text:
|
|
201
|
+
# The problem the command was started to fix comes first; a reason the text is read-only stays under it.
|
|
202
|
+
error_lines = [error_text]
|
|
203
|
+
if window.error_label.text():
|
|
204
|
+
error_lines.append(window.error_label.text())
|
|
205
|
+
window.show_error(LINE_SEPARATOR.join(error_lines))
|
|
206
|
+
self.opening_error_text_by_operation_id[operation_id] = window.error_label.text()
|
|
207
|
+
if content is not None:
|
|
208
|
+
self.content_by_operation_id[operation_id] = content
|
|
209
|
+
if read is not None and read.rule is not None:
|
|
210
|
+
self.rule_by_operation_id[operation_id] = read.rule
|
|
211
|
+
self.window_by_operation_id[operation_id] = window
|
|
212
|
+
window.validate_requested.connect(lambda: self._validate(operation_id, window))
|
|
213
|
+
window.save_requested.connect(lambda: self._save(operation_id, window))
|
|
214
|
+
window.abort_requested.connect(lambda: self._abort(operation_id, window))
|
|
215
|
+
window.present()
|
|
216
|
+
self.window_shown.emit(pending.request, window)
|
|
217
|
+
|
|
218
|
+
def _served(self, operation_id: str, window: RebaseEditorWindow) -> PendingEdit | None:
|
|
219
|
+
"""Answer the request this window still serves, or nothing once it was answered, lost or replaced."""
|
|
220
|
+
pending = self.pending_by_operation_id.get(operation_id)
|
|
221
|
+
if pending is None or pending.answered or self.window_by_operation_id.get(operation_id) is not window:
|
|
222
|
+
return None
|
|
223
|
+
return pending
|
|
224
|
+
|
|
225
|
+
def _validate(self, operation_id: str, window: RebaseEditorWindow) -> None:
|
|
226
|
+
"""Write the edited text, then hand it to Git; a file that cannot be written back goes to Git unchanged.
|
|
227
|
+
|
|
228
|
+
A commit message Git would find empty under the repository's settings is refused: the window stays open with
|
|
229
|
+
the reason and Git keeps waiting, since Git would abort the commit and pause the rebase.
|
|
230
|
+
"""
|
|
231
|
+
pending = self._served(operation_id, window)
|
|
232
|
+
if pending is None:
|
|
233
|
+
return
|
|
234
|
+
content = self.content_by_operation_id.get(operation_id)
|
|
235
|
+
if content is None or content.read_only_cause is not None:
|
|
236
|
+
self._answer(operation_id, pending, window, EditorAnswer.CONTINUE)
|
|
237
|
+
return
|
|
238
|
+
expected = self.expected_by_operation_id.get(operation_id)
|
|
239
|
+
if expected is None:
|
|
240
|
+
return
|
|
241
|
+
text = window.edited_text()
|
|
242
|
+
rule = self.rule_by_operation_id.get(operation_id)
|
|
243
|
+
if rule is not None and message_is_empty(text, rule):
|
|
244
|
+
window.show_error(RebaseEditorText.EMPTY_MESSAGE.value)
|
|
245
|
+
return
|
|
246
|
+
window.set_busy(True)
|
|
247
|
+
self.worktree_service.run(expected.context, expected.git_filename, self._write(content, text),
|
|
248
|
+
lambda written, error: self._written(operation_id, pending, window, written, error))
|
|
249
|
+
|
|
250
|
+
def _write(self, content: EditFileContent, text: str) -> Generator:
|
|
251
|
+
"""Write the edited text off the Qt thread."""
|
|
252
|
+
return (yield FileTask(lambda: write_edit_file(content, text)))
|
|
253
|
+
|
|
254
|
+
def _written(self, operation_id: str, pending: PendingEdit, window: RebaseEditorWindow, written: EditFileContent | None, error: Exception | None) -> None:
|
|
255
|
+
"""Hand a saved text to Git, or keep the window open with the cause when saving failed: Git never receives an unsaved text."""
|
|
256
|
+
if pending.answered or self.pending_by_operation_id.get(operation_id) is not pending:
|
|
257
|
+
return
|
|
258
|
+
if error is not None or written is None:
|
|
259
|
+
window.show_error(save_failure_text(error))
|
|
260
|
+
window.set_busy(False)
|
|
261
|
+
return
|
|
262
|
+
self.content_by_operation_id[operation_id] = written
|
|
263
|
+
self._answer(operation_id, pending, window, EditorAnswer.CONTINUE)
|
|
264
|
+
|
|
265
|
+
def _save(self, operation_id: str, window: RebaseEditorWindow) -> None:
|
|
266
|
+
"""Write the edited text without answering Git, as Ctrl+S asks: the window stays open and Git keeps waiting.
|
|
267
|
+
|
|
268
|
+
A file shown read-only, or one that could not be read, has nothing to save. The window stays busy during the
|
|
269
|
+
write, so neither a second Ctrl+S nor Continue starts another one.
|
|
270
|
+
"""
|
|
271
|
+
pending = self._served(operation_id, window)
|
|
272
|
+
if pending is None:
|
|
273
|
+
return
|
|
274
|
+
content = self.content_by_operation_id.get(operation_id)
|
|
275
|
+
expected = self.expected_by_operation_id.get(operation_id)
|
|
276
|
+
if content is None or content.read_only_cause is not None or expected is None:
|
|
277
|
+
return
|
|
278
|
+
window.set_busy(True)
|
|
279
|
+
self.worktree_service.run(expected.context, expected.git_filename, self._write(content, window.edited_text()),
|
|
280
|
+
lambda written, error: self._saved(operation_id, pending, window, written, error))
|
|
281
|
+
|
|
282
|
+
def _saved(self, operation_id: str, pending: PendingEdit, window: RebaseEditorWindow, written: EditFileContent | None, error: Exception | None) -> None:
|
|
283
|
+
"""Keep the saved text as the one Git's file now holds, or show why it could not be saved; Git receives no answer either way.
|
|
284
|
+
|
|
285
|
+
A successful save clears only what the window showed itself, a failed save or an empty message, and shows again
|
|
286
|
+
the error the window opened with: Git has not checked the saved text yet, so its refusal still says what to fix.
|
|
287
|
+
"""
|
|
288
|
+
if pending.answered or self.pending_by_operation_id.get(operation_id) is not pending:
|
|
289
|
+
return
|
|
290
|
+
window.set_busy(False)
|
|
291
|
+
if error is not None or written is None:
|
|
292
|
+
window.show_error(save_failure_text(error))
|
|
293
|
+
return
|
|
294
|
+
# The saved text and its new file identity: Continue on the same text writes nothing more, and a later write checks this file.
|
|
295
|
+
self.content_by_operation_id[operation_id] = written
|
|
296
|
+
window.show_error(self.opening_error_text_by_operation_id.get(operation_id, ""))
|
|
297
|
+
|
|
298
|
+
def _abort(self, operation_id: str, window: RebaseEditorWindow) -> None:
|
|
299
|
+
"""Answer Abort to Git for the request this window serves."""
|
|
300
|
+
pending = self._served(operation_id, window)
|
|
301
|
+
if pending is None:
|
|
302
|
+
return
|
|
303
|
+
self._answer(operation_id, pending, window, EditorAnswer.ABORT)
|
|
304
|
+
|
|
305
|
+
def _answer(self, operation_id: str, pending: PendingEdit, window: RebaseEditorWindow, answer: EditorAnswer) -> None:
|
|
306
|
+
"""Give Git its one answer, then close the window; the window stays known until the next edit or the end of the operation."""
|
|
307
|
+
self.pending_by_operation_id.pop(operation_id, None)
|
|
308
|
+
pending.answer(answer)
|
|
309
|
+
window.close_after_answer()
|