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,345 @@
|
|
|
1
|
+
"""Close GitPane only once no write or process it started still runs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import QEvent, QObject, Qt
|
|
8
|
+
from PySide6.QtWidgets import QApplication, QDialog
|
|
9
|
+
from shiboken6 import isValid
|
|
10
|
+
|
|
11
|
+
from gitpane.controller.session_registry import NavigationKind
|
|
12
|
+
from gitpane.operation.operation_status import OperationStatus
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
|
|
17
|
+
from PySide6.QtWidgets import QWidget
|
|
18
|
+
|
|
19
|
+
from gitpane.account.authentication import AuthenticationService
|
|
20
|
+
from gitpane.account.ssh import SshService
|
|
21
|
+
from gitpane.controller.account_controller import AccountController
|
|
22
|
+
from gitpane.controller.close_readiness import CloseReadiness
|
|
23
|
+
from gitpane.controller.conflict_controller import ConflictController
|
|
24
|
+
from gitpane.controller.emoji_window_controller import EmojiWindowController
|
|
25
|
+
from gitpane.controller.history_read_controller import HistoryReadController
|
|
26
|
+
from gitpane.controller.navigation_controller import NavigationController
|
|
27
|
+
from gitpane.controller.rebase_controller import RebaseController
|
|
28
|
+
from gitpane.controller.reference_form_controller import ReferenceFormController
|
|
29
|
+
from gitpane.controller.reference_read_controller import ReferenceReadController
|
|
30
|
+
from gitpane.controller.refresh_controller import RefreshController
|
|
31
|
+
from gitpane.controller.replay_controller import ReplayController
|
|
32
|
+
from gitpane.controller.session_registry import SessionRegistry
|
|
33
|
+
from gitpane.controller.staging_session_registry import StagingSessionRegistry
|
|
34
|
+
from gitpane.controller.stash_controller import StashController
|
|
35
|
+
from gitpane.controller.transport_controller import TransportController
|
|
36
|
+
from gitpane.controller.window_activation import WindowActivationFilter
|
|
37
|
+
from gitpane.history.repository import GitRepository
|
|
38
|
+
from gitpane.operation.repository_operation import RepositoryOperationService
|
|
39
|
+
from gitpane.sequencer.mergetool import MergetoolService
|
|
40
|
+
from gitpane.sequencer.rebase import RebaseService
|
|
41
|
+
from gitpane.sequencer.replay import ReplayService
|
|
42
|
+
from gitpane.submodule.submodule_discovery import SubmoduleService
|
|
43
|
+
from gitpane.tool.difftool import DifftoolService
|
|
44
|
+
from gitpane.tool.editor import EditorService
|
|
45
|
+
from gitpane.tool.system_tool import SystemToolService
|
|
46
|
+
from gitpane.transport.branch_exchange import TransportService
|
|
47
|
+
from gitpane.transport.clone import CloneService
|
|
48
|
+
from gitpane.worktree.mutation import MutationService
|
|
49
|
+
from gitpane.worktree.worktree_service import WorktreeService
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def holds_running_work(widget: QWidget) -> bool:
|
|
53
|
+
"""Tell whether a window is making Git work right now, so quitting waits for it instead of closing it.
|
|
54
|
+
|
|
55
|
+
Operation consoles say so with ``running`` and forms with ``busy``; closing one of them would stop a write halfway
|
|
56
|
+
(a running console treats a close as Abort) instead of letting it finish. Any other window has neither attribute.
|
|
57
|
+
"""
|
|
58
|
+
return bool(getattr(widget, "running", False) or getattr(widget, "busy", False))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ShutdownController(QObject):
|
|
62
|
+
"""The end of GitPane and of each repository window: it keeps hold of Quit and of the close of a repository window while Git works, closes the windows of every domain in their order, and leaves the event loop once everything has drained."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, *, session_registry_controller: SessionRegistry, staging_session_registry_controller: StagingSessionRegistry, window_activation_controller: WindowActivationFilter, reference_read_controller: ReferenceReadController,
|
|
65
|
+
refresh_controller: RefreshController, history_read_controller: HistoryReadController, account_controller: AccountController, emoji_window_controller: EmojiWindowController,
|
|
66
|
+
transport_controller: TransportController, reference_form_controller: ReferenceFormController, replay_controller: ReplayController, rebase_controller: RebaseController, stash_controller: StashController,
|
|
67
|
+
conflict_controller: ConflictController, close_readiness_controller: CloseReadiness, navigation_controller: NavigationController, application: QApplication, leave_event_loop: Callable[[], None],
|
|
68
|
+
repository: GitRepository, difftool: DifftoolService, worktree_service: WorktreeService, editor: EditorService, repository_operation: RepositoryOperationService, rebase: RebaseService, replay: ReplayService,
|
|
69
|
+
mutation: MutationService, authentication: AuthenticationService, ssh: SshService, transport: TransportService, clone: CloneService, mergetool: MergetoolService, system_tool: SystemToolService,
|
|
70
|
+
submodule: SubmoduleService, parent: QObject | None = None) -> None:
|
|
71
|
+
"""Receive the services and the lower controllers whose work and windows a close waits for; the wiring builds it last and installs it on the application before the placement filter."""
|
|
72
|
+
super().__init__(parent)
|
|
73
|
+
self.session_registry_controller = session_registry_controller
|
|
74
|
+
self.staging_session_registry_controller = staging_session_registry_controller
|
|
75
|
+
self.window_activation_controller = window_activation_controller
|
|
76
|
+
self.reference_read_controller = reference_read_controller
|
|
77
|
+
self.refresh_controller = refresh_controller
|
|
78
|
+
self.history_read_controller = history_read_controller
|
|
79
|
+
self.account_controller = account_controller
|
|
80
|
+
self.emoji_window_controller = emoji_window_controller
|
|
81
|
+
self.transport_controller = transport_controller
|
|
82
|
+
self.reference_form_controller = reference_form_controller
|
|
83
|
+
self.replay_controller = replay_controller
|
|
84
|
+
self.rebase_controller = rebase_controller
|
|
85
|
+
self.stash_controller = stash_controller
|
|
86
|
+
self.conflict_controller = conflict_controller
|
|
87
|
+
self.close_readiness_controller = close_readiness_controller
|
|
88
|
+
self.navigation_controller = navigation_controller
|
|
89
|
+
self.application = application
|
|
90
|
+
self.leave_event_loop = leave_event_loop
|
|
91
|
+
self.repository = repository
|
|
92
|
+
self.difftool = difftool
|
|
93
|
+
self.worktree_service = worktree_service
|
|
94
|
+
self.editor = editor
|
|
95
|
+
self.repository_operation = repository_operation
|
|
96
|
+
self.rebase = rebase
|
|
97
|
+
self.replay = replay
|
|
98
|
+
self.mutation = mutation
|
|
99
|
+
self.authentication = authentication
|
|
100
|
+
self.ssh = ssh
|
|
101
|
+
self.transport = transport
|
|
102
|
+
self.clone = clone
|
|
103
|
+
self.mergetool = mergetool
|
|
104
|
+
self.system_tool = system_tool
|
|
105
|
+
self.submodule = submodule
|
|
106
|
+
self.application_quit_requested = False
|
|
107
|
+
|
|
108
|
+
def eventFilter(self, watched: QObject, event: QEvent) -> bool:
|
|
109
|
+
"""Keep hold of the Quit of the application and of the Close of a repository window while Git work still runs.
|
|
110
|
+
|
|
111
|
+
Quit becomes a shutdown request that waits for every write and process; the Close of a repository window waits the
|
|
112
|
+
same way when its repository is still busy.
|
|
113
|
+
"""
|
|
114
|
+
if watched is self.application and event.type() == QEvent.Type.Quit and not self.session_registry_controller.shutdown_requested:
|
|
115
|
+
self.request_shutdown()
|
|
116
|
+
event.ignore()
|
|
117
|
+
return True
|
|
118
|
+
if event.type() == QEvent.Type.Close:
|
|
119
|
+
for session_id, window in tuple(self.session_registry_controller.window_by_session.items()):
|
|
120
|
+
if watched is window and self.close_readiness_controller.defer_repository_close(session_id):
|
|
121
|
+
event.ignore()
|
|
122
|
+
return True
|
|
123
|
+
return super().eventFilter(watched, event)
|
|
124
|
+
|
|
125
|
+
def request_shutdown(self) -> None:
|
|
126
|
+
"""Keep application ownership until all writes, descendants and file work finish."""
|
|
127
|
+
if self.session_registry_controller.shutdown_requested:
|
|
128
|
+
return
|
|
129
|
+
self._reject_idle_modal_windows()
|
|
130
|
+
self.session_registry_controller.quit_pending = True
|
|
131
|
+
if not self.close_readiness_controller.close_ready():
|
|
132
|
+
self.close_readiness_controller.show_close_wait()
|
|
133
|
+
self.close_readiness_controller.close_timer.start()
|
|
134
|
+
return
|
|
135
|
+
self.session_registry_controller.quit_pending = False
|
|
136
|
+
self.session_registry_controller.shutdown_requested = True
|
|
137
|
+
self.close_readiness_controller.close_timer.stop()
|
|
138
|
+
self.rebase.close()
|
|
139
|
+
self.replay.close()
|
|
140
|
+
self.editor.close()
|
|
141
|
+
# A window that refuses to close keeps Qt from quitting, so Settings closes here without asking about unsaved changes.
|
|
142
|
+
if self.session_registry_controller.setting_dialog is not None and isValid(self.session_registry_controller.setting_dialog):
|
|
143
|
+
self.session_registry_controller.setting_dialog.discard_and_close()
|
|
144
|
+
# A window modal for the whole application left open would keep the other windows from closing.
|
|
145
|
+
if self.account_controller.credential_manager_dialog is not None and isValid(self.account_controller.credential_manager_dialog):
|
|
146
|
+
self.account_controller.credential_manager_dialog.close()
|
|
147
|
+
# The keyboard belongs to no repository window, so closing those would leave it open; closing it saves its geometry.
|
|
148
|
+
if self.emoji_window_controller.emoji_window is not None and isValid(self.emoji_window_controller.emoji_window) and self.emoji_window_controller.emoji_window.isVisible():
|
|
149
|
+
self.emoji_window_controller.emoji_window.close()
|
|
150
|
+
for window in tuple(self.session_registry_controller.window_by_session.values()):
|
|
151
|
+
window.close()
|
|
152
|
+
self.transport.close()
|
|
153
|
+
self.clone.close()
|
|
154
|
+
for run in tuple(self.submodule.run_by_id.values()):
|
|
155
|
+
self.submodule.cancel(run.operation_id)
|
|
156
|
+
self.ssh.close()
|
|
157
|
+
self.repository_operation.close()
|
|
158
|
+
self.authentication.close()
|
|
159
|
+
self.difftool.close()
|
|
160
|
+
self.mergetool.close()
|
|
161
|
+
self.system_tool.close()
|
|
162
|
+
for staging in self.staging_session_registry_controller.staging_by_key.values():
|
|
163
|
+
staging.timer.stop()
|
|
164
|
+
staging.refresh_queued = False
|
|
165
|
+
staging.queued_refresh_priority = None
|
|
166
|
+
self.maybe_quit()
|
|
167
|
+
|
|
168
|
+
def _reject_idle_modal_windows(self) -> None:
|
|
169
|
+
"""Answer Cancel in every open modal window that makes no Git work, so a window left open never keeps GitPane from quitting.
|
|
170
|
+
|
|
171
|
+
On macOS every modal window of GitPane blocks the whole application, and a question asked with exec() runs its
|
|
172
|
+
own event loop until answered. A console or form still making Git work is left open: quitting waits for it, and
|
|
173
|
+
the next retry of the pending quit closes it once it is idle. Settings and the Git Credential Manager window keep
|
|
174
|
+
their own closing in request_shutdown.
|
|
175
|
+
"""
|
|
176
|
+
kept_window_set = {self.session_registry_controller.setting_dialog, self.account_controller.credential_manager_dialog}
|
|
177
|
+
for widget in tuple(QApplication.topLevelWidgets()):
|
|
178
|
+
if not isinstance(widget, QDialog) or not isValid(widget) or widget in kept_window_set:
|
|
179
|
+
continue
|
|
180
|
+
if not widget.isVisible() or widget.windowModality() == Qt.WindowModality.NonModal or holds_running_work(widget):
|
|
181
|
+
continue
|
|
182
|
+
widget.reject()
|
|
183
|
+
|
|
184
|
+
def resume_pending_close(self) -> None:
|
|
185
|
+
"""Honor original close requests only after owners have published native results."""
|
|
186
|
+
if self.session_registry_controller.quit_pending:
|
|
187
|
+
self.request_shutdown()
|
|
188
|
+
return
|
|
189
|
+
for session_id in tuple(self.session_registry_controller.close_session_set):
|
|
190
|
+
if not self.close_readiness_controller.close_ready(session_id):
|
|
191
|
+
self.close_readiness_controller.show_close_wait(session_id)
|
|
192
|
+
continue
|
|
193
|
+
self.session_registry_controller.close_session_set.discard(session_id)
|
|
194
|
+
window = self.session_registry_controller.window_by_session.get(session_id)
|
|
195
|
+
if window is not None and isValid(window):
|
|
196
|
+
window.close()
|
|
197
|
+
for session_id, (session, repository_dir, kind) in tuple(self.session_registry_controller.navigation_request_by_session.items()):
|
|
198
|
+
if self.session_registry_controller.session_by_id.get(session_id) is not session:
|
|
199
|
+
self.session_registry_controller.navigation_request_by_session.pop(session_id, None)
|
|
200
|
+
continue
|
|
201
|
+
if not self.close_readiness_controller.navigation_ready(session_id):
|
|
202
|
+
continue
|
|
203
|
+
self.session_registry_controller.navigation_request_by_session.pop(session_id, None)
|
|
204
|
+
# A repository chosen in the switch menu is outside the family, which the submodule path refuses.
|
|
205
|
+
if kind is NavigationKind.SWITCH:
|
|
206
|
+
self.navigation_controller.switch_repository(session_id, repository_dir)
|
|
207
|
+
else:
|
|
208
|
+
self.navigation_controller.navigate_repository(session_id, repository_dir)
|
|
209
|
+
# The resumed change checks everything again and may stop before reading anything (folder gone, window
|
|
210
|
+
# opened meanwhile): its wait message goes with it. A change that waits again or started reading keeps it.
|
|
211
|
+
if session_id not in self.session_registry_controller.navigation_request_by_session and session_id not in self.navigation_controller.navigation_target_by_session:
|
|
212
|
+
self.close_readiness_controller.clear_navigation_wait(session_id)
|
|
213
|
+
for target_id, snapshot in tuple(self.navigation_controller.navigation_resume_by_target.items()):
|
|
214
|
+
owner_id = self.session_registry_controller.navigation_owner_by_id.get(target_id)
|
|
215
|
+
if owner_id is not None and not self.close_readiness_controller.navigation_ready(owner_id):
|
|
216
|
+
continue
|
|
217
|
+
self.navigation_controller.navigation_resume_by_target.pop(target_id, None)
|
|
218
|
+
self.navigation_controller.publish_repository(target_id, snapshot)
|
|
219
|
+
if not self.session_registry_controller.close_session_set and not self.session_registry_controller.navigation_request_by_session and not self.navigation_controller.navigation_resume_by_target:
|
|
220
|
+
self.close_readiness_controller.close_timer.stop()
|
|
221
|
+
|
|
222
|
+
def maybe_quit(self, *arguments: object) -> None:
|
|
223
|
+
"""Exit only after results have been retained and every native child has drained."""
|
|
224
|
+
if (self.application_quit_requested or not self.session_registry_controller.shutdown_requested
|
|
225
|
+
or self.repository_operation.active_by_id or self.mutation.active_key_set or self.mutation.configuration_busy or self.authentication.active_by_id
|
|
226
|
+
or self.editor.active_by_id or self.mergetool.active_by_id or self.mergetool.series_by_id or self.worktree_service.file_task_by_id or self.worktree_service.continuation_by_id):
|
|
227
|
+
return
|
|
228
|
+
if not self.repository.runner.closed:
|
|
229
|
+
self.repository.close()
|
|
230
|
+
if (not self.application_quit_requested and not self.system_tool.export_by_id and not self.difftool.active_by_id and not self.mergetool.active_by_id and not self.repository.runner.active_by_id
|
|
231
|
+
and not self.repository.runner.pending_requests and not self.worktree_service.file_task_by_id and not self.worktree_service.continuation_by_id):
|
|
232
|
+
self.application_quit_requested = True
|
|
233
|
+
self.leave_event_loop()
|
|
234
|
+
|
|
235
|
+
def close_repository(self, session_id: str, *, keep_navigation: bool = False) -> None:
|
|
236
|
+
"""Close one session and preserve all other windows and pending requests."""
|
|
237
|
+
if not keep_navigation and self.close_readiness_controller.defer_repository_close(session_id):
|
|
238
|
+
return
|
|
239
|
+
self.session_registry_controller.navigation_request_by_session.pop(session_id, None)
|
|
240
|
+
if not keep_navigation:
|
|
241
|
+
for run in tuple(self.submodule.run_by_id.values()):
|
|
242
|
+
if run.session_id == session_id:
|
|
243
|
+
self.submodule.cancel(run.operation_id)
|
|
244
|
+
for target_id, owner_id in tuple(self.session_registry_controller.navigation_owner_by_id.items()):
|
|
245
|
+
if owner_id != session_id:
|
|
246
|
+
continue
|
|
247
|
+
self.session_registry_controller.navigation_owner_by_id.pop(target_id, None)
|
|
248
|
+
self.navigation_controller.navigation_kind_by_id.pop(target_id, None)
|
|
249
|
+
self.navigation_controller.navigation_family_by_id.pop(target_id, None)
|
|
250
|
+
self.navigation_controller.navigation_resume_by_target.pop(target_id, None)
|
|
251
|
+
run = self.navigation_controller.submodule_run_by_navigation.pop(target_id, None)
|
|
252
|
+
if run is not None:
|
|
253
|
+
run.open_status = OperationStatus.CANCELLED.value
|
|
254
|
+
self.navigation_controller.navigation_context_by_id.pop(target_id, None)
|
|
255
|
+
self.navigation_controller.navigation_snapshot_by_id.pop(target_id, None)
|
|
256
|
+
self.navigation_controller.navigation_commits_by_id.pop(target_id, None)
|
|
257
|
+
self.navigation_controller.navigation_streamed_history_by_id.pop(target_id, None)
|
|
258
|
+
self.repository.cancel(target_id)
|
|
259
|
+
self.repository.context_by_session.pop(target_id, None)
|
|
260
|
+
self.navigation_controller.navigation_target_by_session.pop(session_id, None)
|
|
261
|
+
self.refresh_controller.submodule_snapshot_by_session.pop(session_id, None)
|
|
262
|
+
self.navigation_controller.anchor_by_window.pop(session_id, None)
|
|
263
|
+
self.mergetool.invalidate_session(session_id)
|
|
264
|
+
self.refresh_controller.leave_full_refresh_read(session_id)
|
|
265
|
+
self.replay_controller.replay_refresh_by_session.pop(session_id, None)
|
|
266
|
+
self.session_registry_controller.replay_pending_resolve_set.discard(session_id)
|
|
267
|
+
for key, request in tuple(self.replay_controller.replay_auto_continue_by_key.items()):
|
|
268
|
+
if request.session_id == session_id:
|
|
269
|
+
self.replay_controller.replay_auto_continue_by_key.pop(key, None)
|
|
270
|
+
for key, resume_session_id in tuple(self.replay_controller.replay_resume_session_by_key.items()):
|
|
271
|
+
if resume_session_id == session_id:
|
|
272
|
+
self.replay_controller.replay_resume_session_by_key.pop(key, None)
|
|
273
|
+
for dialog_by_session in (self.replay_controller.replay_dialog_by_session, self.replay_controller.replay_prompt_by_session,
|
|
274
|
+
self.rebase_controller.rebase_dialog_by_session, self.rebase_controller.rebase_confirmation_by_session, self.stash_controller.stash_dialog_by_session,
|
|
275
|
+
self.conflict_controller.conflict_dialog_by_session, self.conflict_controller.conflict_confirmation_by_session, self.conflict_controller.conflict_prompt_by_session,
|
|
276
|
+
self.navigation_controller.invalid_repository_prompt_by_session):
|
|
277
|
+
dialog = dialog_by_session.pop(session_id, None)
|
|
278
|
+
if dialog is not None and isValid(dialog):
|
|
279
|
+
dialog.close()
|
|
280
|
+
recovery_dialog = self.conflict_controller.merge_recovery_dialog_by_session.pop(session_id, None)
|
|
281
|
+
if recovery_dialog is not None and isValid(recovery_dialog):
|
|
282
|
+
recovery_dialog.close()
|
|
283
|
+
for operation_id, run in tuple(self.transport.run_by_id.items()):
|
|
284
|
+
if run.request.operation.session_id == session_id:
|
|
285
|
+
self.transport.cancel(operation_id)
|
|
286
|
+
if operation_id in self.transport.outcome_by_id:
|
|
287
|
+
run.trace.close()
|
|
288
|
+
checkout_dialog = self.reference_form_controller.checkout_dialog_by_session.pop(session_id, None)
|
|
289
|
+
checkout_revision_dialog = self.reference_form_controller.checkout_revision_dialog_by_session.pop(session_id, None)
|
|
290
|
+
branch_dialog = self.reference_form_controller.branch_dialog_by_session.pop(session_id, None)
|
|
291
|
+
delete_dialog = self.reference_form_controller.delete_dialog_by_session.pop(session_id, None)
|
|
292
|
+
if delete_dialog is not None and isValid(delete_dialog):
|
|
293
|
+
delete_dialog.reject()
|
|
294
|
+
local_branch_delete_dialog = self.reference_form_controller.local_branch_delete_dialog_by_session.pop(session_id, None)
|
|
295
|
+
if local_branch_delete_dialog is not None and isValid(local_branch_delete_dialog):
|
|
296
|
+
local_branch_delete_dialog.reject()
|
|
297
|
+
remote_dialog = self.transport_controller.remote_dialog_by_session.pop(session_id, None)
|
|
298
|
+
push_dialog = self.transport_controller.push_dialog_by_session.pop(session_id, None)
|
|
299
|
+
if push_dialog is not None and isValid(push_dialog):
|
|
300
|
+
push_dialog.reject()
|
|
301
|
+
push_rejection_prompt = self.transport_controller.push_rejection_prompt_by_session.pop(session_id, None)
|
|
302
|
+
if push_rejection_prompt is not None and isValid(push_rejection_prompt):
|
|
303
|
+
push_rejection_prompt.reject()
|
|
304
|
+
# A question parented on the Push window outlives its rejection, since Qt hides no child window then.
|
|
305
|
+
push_tracking_question = self.transport_controller.push_tracking_question_by_session.pop(session_id, None)
|
|
306
|
+
if push_tracking_question is not None and isValid(push_tracking_question):
|
|
307
|
+
push_tracking_question.reject()
|
|
308
|
+
if remote_dialog is not None and isValid(remote_dialog):
|
|
309
|
+
remote_dialog.reject()
|
|
310
|
+
tag_dialog = self.reference_form_controller.tag_dialog_by_session.pop(session_id, None)
|
|
311
|
+
if tag_dialog is not None and isValid(tag_dialog):
|
|
312
|
+
tag_dialog.reject()
|
|
313
|
+
reset_dialog = self.reference_form_controller.reset_dialog_by_session.pop(session_id, None)
|
|
314
|
+
# A form may still wait for its opening check and never have been shown, and close() then emits no finished.
|
|
315
|
+
for form in (reset_dialog, branch_dialog, checkout_dialog, checkout_revision_dialog):
|
|
316
|
+
if form is not None and isValid(form):
|
|
317
|
+
form.done(QDialog.DialogCode.Rejected)
|
|
318
|
+
self.reference_read_controller.reference_snapshot_by_session.pop(session_id, None)
|
|
319
|
+
self.reference_read_controller.reference_read_generation_by_session.pop(session_id, None)
|
|
320
|
+
self.reference_read_controller.logged_stash_cause_by_session.pop(session_id, None)
|
|
321
|
+
self.reference_read_controller.reference_continuation_by_session.pop(session_id, None)
|
|
322
|
+
self.refresh_controller.checkout_refresh_session_set.discard(session_id)
|
|
323
|
+
# The window id outlives its session when the window moves to another repository: what the previous repository
|
|
324
|
+
# still had to do (scroll to the top, offer to resolve its conflicts, read its remotes for Fetch) must never
|
|
325
|
+
# happen in the repository the same window shows next.
|
|
326
|
+
self.history_read_controller.history_top_session_set.discard(session_id)
|
|
327
|
+
self.session_registry_controller.conflict_pending_session_set.discard(session_id)
|
|
328
|
+
self.transport_controller.fetch_read_by_session.pop(session_id, None)
|
|
329
|
+
self.rebase_controller.rebase_final_oid_by_session.pop(session_id, None)
|
|
330
|
+
self.rebase_controller.rebase_end_notice_session_set.discard(session_id)
|
|
331
|
+
for identity, owner_id in tuple(self.session_registry_controller.export_owner_by_identity.items()):
|
|
332
|
+
if owner_id == session_id:
|
|
333
|
+
self.session_registry_controller.export_owner_by_identity.pop(identity, None)
|
|
334
|
+
self.system_tool.cancel_export(identity)
|
|
335
|
+
export_id = identity.request_identity.session_id
|
|
336
|
+
self.repository.cancel(export_id)
|
|
337
|
+
self.repository.context_by_session.pop(export_id, None)
|
|
338
|
+
self.session_registry_controller.window_by_session.pop(session_id, None)
|
|
339
|
+
self.session_registry_controller.identity_by_session.pop(session_id, None)
|
|
340
|
+
session = self.session_registry_controller.session_by_id.pop(session_id, None)
|
|
341
|
+
if session is not None:
|
|
342
|
+
session.refresh_queued = None
|
|
343
|
+
session.close()
|
|
344
|
+
# A worktree that has just lost its last window must stop refreshing itself, and no activation event says so.
|
|
345
|
+
self.window_activation_controller.schedule_activation_update()
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Coordinate the Prepare changes window of each worktree with the rest of GitPane."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from shiboken6 import isValid
|
|
9
|
+
|
|
10
|
+
from gitpane.history.file_history import WindowCapability
|
|
11
|
+
from gitpane.history.repository_value import RequestIdentity
|
|
12
|
+
from gitpane.operation.diagnostic import Diagnostic
|
|
13
|
+
from gitpane.operation.operation_status import OperationStatus
|
|
14
|
+
from gitpane.system.path_identity import canonical_path
|
|
15
|
+
from gitpane.tool.system_tool import text_editor_unavailable_reason
|
|
16
|
+
from gitpane.transport.remote_config import GitConfigScope
|
|
17
|
+
from gitpane.ui.repository.repository_window import COMMIT_NOTICE_KIND
|
|
18
|
+
from gitpane.worktree.worktree_snapshot import WorktreeKey
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from gitpane.commit.commit_write import CommitOutcome
|
|
22
|
+
from gitpane.controller.emoji_window_controller import EmojiWindowController
|
|
23
|
+
from gitpane.controller.file_action_controller import FileActionController
|
|
24
|
+
from gitpane.controller.history_read_controller import HistoryReadController
|
|
25
|
+
from gitpane.controller.opening_controller import OpeningController
|
|
26
|
+
from gitpane.controller.operation_policy import OperationPolicy
|
|
27
|
+
from gitpane.controller.refresh_controller import RefreshController
|
|
28
|
+
from gitpane.controller.session_registry import SessionRegistry
|
|
29
|
+
from gitpane.controller.staging_session_registry import StagingSessionRegistry
|
|
30
|
+
from gitpane.controller.window_activation import WindowActivationFilter
|
|
31
|
+
from gitpane.controller.worktree_state_controller import WorktreeStateController
|
|
32
|
+
from gitpane.history.file_history import FileHistoryAnchor
|
|
33
|
+
from gitpane.history.repository import GitRepository
|
|
34
|
+
from gitpane.history.repository_value import RepositoryContext
|
|
35
|
+
from gitpane.operation.diagnostic import DiagnosticLog
|
|
36
|
+
from gitpane.preference.preference_store import PreferenceStore
|
|
37
|
+
from gitpane.ui.repository.repository_window import RepositoryWindow
|
|
38
|
+
from gitpane.ui.staging.staging_window import StagingWindow
|
|
39
|
+
from gitpane.worktree.mutation import MutationOutcome
|
|
40
|
+
from gitpane.worktree.staging import StagingSession
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class StagingController:
|
|
44
|
+
"""Prepare changes of every worktree: its opening from a repository window, its window bound to the shared preferences, and the reads that its commits, its ignore changes and its index writes ask from the other worktrees and windows."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, *, session_registry_controller: SessionRegistry, operation_policy_controller: OperationPolicy, staging_session_registry_controller: StagingSessionRegistry, window_activation_controller: WindowActivationFilter,
|
|
47
|
+
refresh_controller: RefreshController, worktree_state_controller: WorktreeStateController, history_read_controller: HistoryReadController, opening_controller: OpeningController, emoji_window_controller: EmojiWindowController,
|
|
48
|
+
file_action_controller: FileActionController, preference: PreferenceStore, repository: GitRepository, diagnostic_log: DiagnosticLog | None) -> None:
|
|
49
|
+
"""Receive the services and the lower controllers Prepare changes works with; the wiring builds it after the file action controller, and the repository windows and the preparation sessions reach it through the wiring."""
|
|
50
|
+
self.session_registry_controller = session_registry_controller
|
|
51
|
+
self.operation_policy_controller = operation_policy_controller
|
|
52
|
+
self.staging_session_registry_controller = staging_session_registry_controller
|
|
53
|
+
self.window_activation_controller = window_activation_controller
|
|
54
|
+
self.refresh_controller = refresh_controller
|
|
55
|
+
self.worktree_state_controller = worktree_state_controller
|
|
56
|
+
self.history_read_controller = history_read_controller
|
|
57
|
+
self.opening_controller = opening_controller
|
|
58
|
+
self.emoji_window_controller = emoji_window_controller
|
|
59
|
+
self.file_action_controller = file_action_controller
|
|
60
|
+
self.preference = preference
|
|
61
|
+
self.repository = repository
|
|
62
|
+
self.diagnostic_log = diagnostic_log
|
|
63
|
+
|
|
64
|
+
def open_staging(self, session_id: str) -> StagingWindow | None:
|
|
65
|
+
"""Open shared preparation only from a live management window with a working tree."""
|
|
66
|
+
if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
|
|
67
|
+
return None
|
|
68
|
+
session = self.session_registry_controller.session_by_id[session_id]
|
|
69
|
+
if session.repository_context.is_bare or session.repository_context.worktree_dir is None:
|
|
70
|
+
return None
|
|
71
|
+
staging = self.staging_session_registry_controller.staging_session(session.repository_context)
|
|
72
|
+
staging.commit_push_session_id = session_id
|
|
73
|
+
return staging.open()
|
|
74
|
+
|
|
75
|
+
def log_mutation_result(self, outcome: MutationOutcome) -> None:
|
|
76
|
+
"""Include prepared write failures in the optional application diagnostic file."""
|
|
77
|
+
if outcome.diagnostic is not None and self.diagnostic_log is not None:
|
|
78
|
+
self.diagnostic_log.write(outcome.diagnostic)
|
|
79
|
+
staging = self.staging_session_registry_controller.staging_by_key.get(outcome.key)
|
|
80
|
+
if staging is not None:
|
|
81
|
+
self.refresh_submodule_family(staging.context)
|
|
82
|
+
|
|
83
|
+
def refresh_submodule_family(self, context: RepositoryContext) -> None:
|
|
84
|
+
"""Refresh declared relatives across separate Git directories after a local write."""
|
|
85
|
+
directory = canonical_path(context.repository_dir)
|
|
86
|
+
related_dir_set: set[str] = set()
|
|
87
|
+
for session_id, family in tuple(self.refresh_controller.submodule_snapshot_by_session.items()):
|
|
88
|
+
family_dir_set = {canonical_path(entry.repository_dir) for entry in family.entries if entry.repository_dir}
|
|
89
|
+
if directory not in family_dir_set:
|
|
90
|
+
continue
|
|
91
|
+
session = self.session_registry_controller.session_by_id.get(session_id)
|
|
92
|
+
if session is None or session.is_closed or canonical_path(session.repository_context.repository_dir) not in family_dir_set:
|
|
93
|
+
continue
|
|
94
|
+
related_dir_set.update(family_dir_set)
|
|
95
|
+
self.refresh_controller.read_submodules(session_id)
|
|
96
|
+
for staging in tuple(self.staging_session_registry_controller.staging_by_key.values()):
|
|
97
|
+
if canonical_path(staging.context.repository_dir) in related_dir_set and WorktreeKey.from_context(staging.context) != WorktreeKey.from_context(context):
|
|
98
|
+
staging.refresh()
|
|
99
|
+
|
|
100
|
+
def git_setting_changed(self, result: object) -> None:
|
|
101
|
+
"""Refresh every affected worktree after conversion changes shared across windows."""
|
|
102
|
+
context, results = result
|
|
103
|
+
changed_scope_set: set[GitConfigScope] = set()
|
|
104
|
+
for outcome in results:
|
|
105
|
+
if outcome.saved and outcome.change.key == "core.autocrlf":
|
|
106
|
+
changed_scope_set.add(outcome.change.scope)
|
|
107
|
+
if not changed_scope_set:
|
|
108
|
+
return
|
|
109
|
+
for staging in self.staging_session_registry_controller.staging_by_key.values():
|
|
110
|
+
if GitConfigScope.GLOBAL in changed_scope_set or (context is not None and canonical_path(staging.context.common_git_dir) == canonical_path(context.common_git_dir)):
|
|
111
|
+
staging.invalidate_conversion()
|
|
112
|
+
|
|
113
|
+
def staging_window_opened(self, window: StagingWindow) -> None:
|
|
114
|
+
"""Bind a new preparation view to shared persisted proportions and its current screen, and open it over the repository window that asked for it."""
|
|
115
|
+
window.installEventFilter(self.window_activation_controller)
|
|
116
|
+
window.closed.connect(self.window_activation_controller.schedule_activation_update)
|
|
117
|
+
for staging in self.staging_session_registry_controller.staging_by_key.values():
|
|
118
|
+
if staging.window is not window:
|
|
119
|
+
continue
|
|
120
|
+
if staging.commit_push_session_id is not None:
|
|
121
|
+
window.placement_anchor = self.session_registry_controller.window_by_session.get(staging.commit_push_session_id)
|
|
122
|
+
context = staging.context
|
|
123
|
+
window.text_editor_reason = lambda entry, context=context: text_editor_unavailable_reason(context, entry.path, self.file_action_controller.text_editor_command(context))
|
|
124
|
+
if context.repository_dir not in self.file_action_controller.git_editor_setting_by_repository_dir:
|
|
125
|
+
self.file_action_controller.read_git_editor_setting(context)
|
|
126
|
+
window.apply_preference(self.preference.value_by_key)
|
|
127
|
+
window.preference_requested.connect(lambda values: self._save_staging_preference(window, values))
|
|
128
|
+
self.emoji_window_controller.report_emoji_font()
|
|
129
|
+
|
|
130
|
+
def _save_staging_preference(self, window: StagingWindow, value_by_key: dict[str, object]) -> None:
|
|
131
|
+
"""Restore accepted pane weights if storage refuses a change, preserving the message."""
|
|
132
|
+
if not isValid(window):
|
|
133
|
+
return
|
|
134
|
+
try:
|
|
135
|
+
self.preference.update(value_by_key)
|
|
136
|
+
except (OSError, ValueError, TypeError) as error:
|
|
137
|
+
window.apply_preference(self.preference.value_by_key)
|
|
138
|
+
diagnostic = Diagnostic("Save staging preferences", window.context.repository_dir, "", [], str(error)).prepare()
|
|
139
|
+
window.status_label.setText(diagnostic.cause)
|
|
140
|
+
if self.diagnostic_log is not None:
|
|
141
|
+
self.diagnostic_log.write(diagnostic)
|
|
142
|
+
|
|
143
|
+
def open_staging_history(self, staging: StagingSession, anchor: FileHistoryAnchor) -> RepositoryWindow | None:
|
|
144
|
+
"""Open the existing limited history view for a locally resolved committed blob."""
|
|
145
|
+
if self.staging_session_registry_controller.staging_by_key.get(staging.key) is not staging or staging.window is None or staging.window.close_requested or staging.busy:
|
|
146
|
+
return None
|
|
147
|
+
if staging.snapshot is None or not staging.snapshot.fresh or staging.snapshot.head_oid != anchor.commit_oid:
|
|
148
|
+
return None
|
|
149
|
+
identity = RequestIdentity(uuid.uuid4().hex, 1)
|
|
150
|
+
self.session_registry_controller.identity_by_session[identity.session_id] = identity
|
|
151
|
+
self.repository.context_by_session[identity.session_id] = staging.context
|
|
152
|
+
self.opening_controller.opened(identity, staging.context, None, anchor, staging.window)
|
|
153
|
+
return self.session_registry_controller.window_by_session[identity.session_id]
|
|
154
|
+
|
|
155
|
+
def ignore_changed(self, context: RepositoryContext, *, refreshed_key: WorktreeKey | None = None) -> None:
|
|
156
|
+
"""Reread every worktree sharing the repository's exclude file, except the one named as already reading.
|
|
157
|
+
|
|
158
|
+
That key names the session whose staging asked for the read itself; it is not derived from the context, which
|
|
159
|
+
can belong to a linked worktree that has not read yet. Without a key, every sharing worktree reads.
|
|
160
|
+
"""
|
|
161
|
+
common_dir = canonical_path(context.common_git_dir)
|
|
162
|
+
for staging in self.staging_session_registry_controller.staging_by_key.values():
|
|
163
|
+
if staging.key == refreshed_key:
|
|
164
|
+
continue
|
|
165
|
+
if canonical_path(staging.context.common_git_dir) == common_dir:
|
|
166
|
+
staging.refresh()
|
|
167
|
+
|
|
168
|
+
def commit_finished(self, staging: StagingSession, outcome: CommitOutcome) -> None:
|
|
169
|
+
"""Refresh shared history after native writes without sharing linked-worktree drafts.
|
|
170
|
+
|
|
171
|
+
The committing worktree shows the complete read the commit made after writing; its preparation session reads
|
|
172
|
+
again on its own when that read cannot serve. Every other worktree of the repository reads once for all its windows.
|
|
173
|
+
|
|
174
|
+
A successful Commit or Amend puts its commit at the top of the graph, so every repository window of the committing
|
|
175
|
+
worktree goes back to its top once that read is shown. Other worktrees, whose branch did not move, and a refused
|
|
176
|
+
or failed commit leave the graph where it was.
|
|
177
|
+
|
|
178
|
+
A successful commit around which a hook or another program changed files writes those details to the --log
|
|
179
|
+
file. The neutral notice of the commit, or its absence, replaces the previous commit's notice in every
|
|
180
|
+
repository window of the worktree, next to any other notice they show.
|
|
181
|
+
"""
|
|
182
|
+
self.log_mutation_result(outcome)
|
|
183
|
+
if outcome.log_diagnostic is not None and self.diagnostic_log is not None:
|
|
184
|
+
self.diagnostic_log.write(outcome.log_diagnostic)
|
|
185
|
+
for _session_id, window in self.worktree_state_controller.managed_windows_of_worktree(staging.key):
|
|
186
|
+
window.show_status_notice(staging.key, COMMIT_NOTICE_KIND, outcome.notice)
|
|
187
|
+
common_dir = canonical_path(staging.context.common_git_dir)
|
|
188
|
+
if outcome.status == OperationStatus.SUCCESS:
|
|
189
|
+
for session_id, session in self.session_registry_controller.session_by_id.items():
|
|
190
|
+
if not session.is_closed and session.mode == "repository" and WorktreeKey.from_context(session.repository_context) == staging.key:
|
|
191
|
+
self.history_read_controller.history_top_session_set.add(session_id)
|
|
192
|
+
shared_by_key = {staging.key: self.refresh_controller.begin_full_refresh_read(staging.key, published_staging=staging)}
|
|
193
|
+
for session_id, session in tuple(self.session_registry_controller.session_by_id.items()):
|
|
194
|
+
if not session.is_closed and canonical_path(session.repository_context.common_git_dir) == common_dir:
|
|
195
|
+
self.refresh_controller.refresh(session_id, full_refresh_read=self.refresh_controller.full_refresh_read_for(session, shared_by_key))
|
|
196
|
+
refreshed_key_set = self.refresh_controller.observe_full_refresh_pauses(shared_by_key)
|
|
197
|
+
for other in self.staging_session_registry_controller.staging_by_key.values():
|
|
198
|
+
if other is not staging and other.key not in refreshed_key_set and canonical_path(other.context.common_git_dir) == common_dir:
|
|
199
|
+
other.refresh()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Reuse one preparation session per worktree across the windows that show it."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import QObject
|
|
8
|
+
|
|
9
|
+
from gitpane.worktree.staging import StagingSession
|
|
10
|
+
from gitpane.worktree.worktree_snapshot import WorktreeKey
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
|
|
15
|
+
from PySide6.QtWidgets import QWidget
|
|
16
|
+
|
|
17
|
+
from gitpane.history.repository import GitRepository
|
|
18
|
+
from gitpane.history.repository_value import RepositoryContext
|
|
19
|
+
from gitpane.sequencer.rebase import RebaseService
|
|
20
|
+
from gitpane.worktree.mutation import MutationService
|
|
21
|
+
from gitpane.worktree.worktree_service import WorktreeService
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class StagingSessionRegistry(QObject):
|
|
25
|
+
"""The preparation session of each worktree, created once, owned here and shared by every window of that worktree."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, *, repository: GitRepository, worktree_service: WorktreeService, rebase: RebaseService, mutation: MutationService,
|
|
28
|
+
wire_staging_session: Callable[[WorktreeKey, StagingSession], None], window_factory: Callable[[RepositoryContext], QWidget], parent: QObject | None = None) -> None:
|
|
29
|
+
"""Receive the services a session works with, the function of the wiring that connects a new session to the controllers above this registry, and the
|
|
30
|
+
factory every session builds its Prepare changes window with."""
|
|
31
|
+
super().__init__(parent)
|
|
32
|
+
self.repository = repository
|
|
33
|
+
self.worktree_service = worktree_service
|
|
34
|
+
self.rebase = rebase
|
|
35
|
+
self.mutation = mutation
|
|
36
|
+
self.wire_staging_session = wire_staging_session
|
|
37
|
+
self.window_factory = window_factory
|
|
38
|
+
self.staging_by_key: dict[WorktreeKey, StagingSession] = {}
|
|
39
|
+
|
|
40
|
+
def staging_session(self, context: RepositoryContext) -> StagingSession:
|
|
41
|
+
"""Reuse one index owner and in-memory draft across repository windows."""
|
|
42
|
+
key = WorktreeKey.from_context(context)
|
|
43
|
+
if key not in self.staging_by_key:
|
|
44
|
+
staging = StagingSession(context, self.repository.git_filename, self.worktree_service, self.mutation, self, rebase_service=self.rebase, window_factory=self.window_factory)
|
|
45
|
+
self.staging_by_key[key] = staging
|
|
46
|
+
self.wire_staging_session(key, staging)
|
|
47
|
+
return self.staging_by_key[key]
|