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,658 @@
|
|
|
1
|
+
"""Move a repository window to another repository, a submodule or its parent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import uuid
|
|
7
|
+
from dataclasses import dataclass, replace
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from PySide6.QtCore import QObject, QSignalBlocker, Qt, QTimer, Signal
|
|
11
|
+
from PySide6.QtWidgets import QDialog, QMessageBox
|
|
12
|
+
from shiboken6 import isValid
|
|
13
|
+
|
|
14
|
+
from gitpane.account.authentication import transfer_account
|
|
15
|
+
from gitpane.config import ConsoleOperation
|
|
16
|
+
from gitpane.controller.session_registry import NavigationKind, RepositorySession, keep_streamed_commits
|
|
17
|
+
from gitpane.history.file_history import WindowCapability
|
|
18
|
+
from gitpane.history.history_filter import HISTORY_FILTER_INITIAL_MODE
|
|
19
|
+
from gitpane.history.repository_value import RequestIdentity
|
|
20
|
+
from gitpane.operation.diagnostic import Diagnostic
|
|
21
|
+
from gitpane.operation.operation_status import OperationStatus
|
|
22
|
+
from gitpane.preference.preference_store import saved_recent_identity
|
|
23
|
+
from gitpane.submodule.submodule_discovery import OPEN_AGAIN_RETRY_SENTENCE, declared_directory, initialization_command
|
|
24
|
+
from gitpane.system.path_identity import canonical_path
|
|
25
|
+
from gitpane.transport.git_refusal import refused_https_url
|
|
26
|
+
from gitpane.transport.refusal_notice import SUBMODULE_NOTICE_SUBJECT_TEXT, account_notice
|
|
27
|
+
from gitpane.ui.console.error_dialog import ErrorDialog
|
|
28
|
+
from gitpane.ui.console.operation_dialog import OperationDialog
|
|
29
|
+
from gitpane.ui.welcome.invalid_repository_prompt import InvalidRepositoryPrompt
|
|
30
|
+
from gitpane.ui.window_control.dialog_modality import open_window_modal, prepare_message_box
|
|
31
|
+
from gitpane.worktree.worktree_snapshot import WorktreeKey
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from gitpane.account.authentication import AuthenticationService
|
|
35
|
+
from gitpane.controller.account_controller import AccountController
|
|
36
|
+
from gitpane.controller.close_readiness import CloseReadiness
|
|
37
|
+
from gitpane.controller.history_read_controller import HistoryReadController
|
|
38
|
+
from gitpane.controller.opening_controller import OpeningController
|
|
39
|
+
from gitpane.controller.operation_policy import OperationPolicy
|
|
40
|
+
from gitpane.controller.rebase_controller import RebaseController
|
|
41
|
+
from gitpane.controller.reference_read_controller import ReferenceReadController
|
|
42
|
+
from gitpane.controller.refresh_controller import RefreshController
|
|
43
|
+
from gitpane.controller.replay_controller import ReplayController
|
|
44
|
+
from gitpane.controller.session_registry import SessionRegistry, StreamedHistory
|
|
45
|
+
from gitpane.controller.staging_session_registry import StagingSessionRegistry
|
|
46
|
+
from gitpane.controller.transport_controller import TransportController
|
|
47
|
+
from gitpane.history.repository import GitRepository
|
|
48
|
+
from gitpane.history.repository_value import Commit, HistorySnapshot, RepositoryContext, RepositoryFailure
|
|
49
|
+
from gitpane.operation.diagnostic import DiagnosticLog
|
|
50
|
+
from gitpane.preference.preference_store import PreferenceStore
|
|
51
|
+
from gitpane.reference.reference_edit import ReferenceService
|
|
52
|
+
from gitpane.reference.reference_snapshot import ReferenceSnapshot
|
|
53
|
+
from gitpane.submodule.submodule_discovery import SubmoduleEntry, SubmoduleInitRun, SubmoduleService, SubmoduleSnapshot
|
|
54
|
+
from gitpane.ui.repository.repository_window import ViewAnchor
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class NavigationFamily:
|
|
59
|
+
"""The family one walk into another repository read, bound to the session, identity and family read it started from.
|
|
60
|
+
|
|
61
|
+
It only spares the new window a second read of the same family for display. It is never a proof for a write:
|
|
62
|
+
every child write reads that child again.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
session: RepositorySession
|
|
66
|
+
identity: RequestIdentity
|
|
67
|
+
generation: int
|
|
68
|
+
family: SubmoduleSnapshot | None = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
SWITCH_OPENING_STATUS = "Opening {directory}…"
|
|
72
|
+
SWITCH_PENDING_STATUS = "Already opening {directory}. Choose again once it is open."
|
|
73
|
+
SWITCH_WAITING_STATUS = "{directory} opens here once Git has finished in the current repository."
|
|
74
|
+
SWITCH_MISSING_STATUS = "The folder {directory} no longer exists. The current repository is still open."
|
|
75
|
+
FORGET_OPERATION = "Save recent repositories"
|
|
76
|
+
SWITCH_REFUSED_TITLE = "Change repository"
|
|
77
|
+
SWITCH_REFUSED_TEXT = "The window \"{title}\" is still open on this repository. Close it, then choose the repository again."
|
|
78
|
+
# Names a dialog of the window that has no title of its own when the change of repository is refused because of it.
|
|
79
|
+
UNTITLED_RELATED_WINDOW_TITLE = "Untitled dialog"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class NavigationController(QObject):
|
|
83
|
+
"""The changes of repository of every repository window: a walk into a submodule, a repository chosen in the switch menu, the reads they own until the window shows the new repository."""
|
|
84
|
+
|
|
85
|
+
# Asks for the closing of the session a published change of repository leaves behind, with the id of the window that
|
|
86
|
+
# keeps going on the new repository. Closing a session belongs to the shutdown, which sits above this controller.
|
|
87
|
+
repository_close_requested = Signal(str)
|
|
88
|
+
|
|
89
|
+
def __init__(self, *, session_registry_controller: SessionRegistry, operation_policy_controller: OperationPolicy, staging_session_registry_controller: StagingSessionRegistry, reference_read_controller: ReferenceReadController,
|
|
90
|
+
refresh_controller: RefreshController, history_read_controller: HistoryReadController, account_controller: AccountController, opening_controller: OpeningController, transport_controller: TransportController,
|
|
91
|
+
replay_controller: ReplayController, rebase_controller: RebaseController, close_readiness_controller: CloseReadiness, preference: PreferenceStore, repository: GitRepository, reference: ReferenceService,
|
|
92
|
+
authentication: AuthenticationService, diagnostic_log: DiagnosticLog | None, submodule: SubmoduleService, parent: QObject | None = None) -> None:
|
|
93
|
+
"""Receive the services and the lower controllers a change of repository works with; the wiring builds it after the close readiness, and closes the session a published change leaves behind when this controller asks it through a signal."""
|
|
94
|
+
super().__init__(parent)
|
|
95
|
+
self.session_registry_controller = session_registry_controller
|
|
96
|
+
self.operation_policy_controller = operation_policy_controller
|
|
97
|
+
self.staging_session_registry_controller = staging_session_registry_controller
|
|
98
|
+
self.reference_read_controller = reference_read_controller
|
|
99
|
+
self.refresh_controller = refresh_controller
|
|
100
|
+
self.history_read_controller = history_read_controller
|
|
101
|
+
self.account_controller = account_controller
|
|
102
|
+
self.opening_controller = opening_controller
|
|
103
|
+
self.transport_controller = transport_controller
|
|
104
|
+
self.replay_controller = replay_controller
|
|
105
|
+
self.rebase_controller = rebase_controller
|
|
106
|
+
self.close_readiness_controller = close_readiness_controller
|
|
107
|
+
self.preference = preference
|
|
108
|
+
self.repository = repository
|
|
109
|
+
self.reference = reference
|
|
110
|
+
self.authentication = authentication
|
|
111
|
+
self.diagnostic_log = diagnostic_log
|
|
112
|
+
self.submodule = submodule
|
|
113
|
+
# The prompt offering to remove a recent repository whose folder is gone, at most one per window.
|
|
114
|
+
self.invalid_repository_prompt_by_session: dict[str, InvalidRepositoryPrompt] = {}
|
|
115
|
+
self.submodule_output_by_id: dict[str, OperationDialog] = {}
|
|
116
|
+
self.submodule_run_by_navigation: dict[str, SubmoduleInitRun] = {}
|
|
117
|
+
self.navigation_target_by_session: dict[str, str] = {}
|
|
118
|
+
self.navigation_context_by_id: dict[str, tuple[RepositoryContext, Commit | None]] = {}
|
|
119
|
+
self.navigation_snapshot_by_id: dict[str, HistorySnapshot] = {}
|
|
120
|
+
self.navigation_commits_by_id: dict[str, list[Commit]] = {}
|
|
121
|
+
self.navigation_streamed_history_by_id: dict[str, StreamedHistory] = {}
|
|
122
|
+
self.navigation_family_by_id: dict[str, NavigationFamily] = {}
|
|
123
|
+
self.navigation_kind_by_id: dict[str, NavigationKind] = {}
|
|
124
|
+
self.anchor_by_window: dict[str, dict[WorktreeKey, ViewAnchor]] = {}
|
|
125
|
+
self.navigation_resume_by_target: dict[str, HistorySnapshot] = {}
|
|
126
|
+
|
|
127
|
+
def navigation_opened(self, identity: RequestIdentity, context: RepositoryContext, commit: Commit | None) -> None:
|
|
128
|
+
"""Read the references of a repository a navigation opened, then its history, while that navigation still owns it.
|
|
129
|
+
|
|
130
|
+
Connected before the opening's own slot, which ignores the identities a navigation owns, so the two never act on
|
|
131
|
+
the same read. A repository other than the one the navigation asked for ends the navigation.
|
|
132
|
+
"""
|
|
133
|
+
owner_id = self.session_registry_controller.navigation_owner_by_id.get(identity.session_id)
|
|
134
|
+
if owner_id is None:
|
|
135
|
+
return
|
|
136
|
+
target_dir = self.navigation_target_by_session.get(owner_id)
|
|
137
|
+
if target_dir is None or canonical_path(context.repository_dir) != canonical_path(target_dir):
|
|
138
|
+
kind = self.navigation_kind_by_id.get(identity.session_id, NavigationKind.SUBMODULE)
|
|
139
|
+
self._navigation_failed(identity.session_id, kind.target_mismatch_text)
|
|
140
|
+
return
|
|
141
|
+
self.navigation_context_by_id[identity.session_id] = (context, commit)
|
|
142
|
+
target_id = identity.session_id
|
|
143
|
+
|
|
144
|
+
def target_references_read(snapshot: ReferenceSnapshot | None, error: Exception | None) -> None:
|
|
145
|
+
"""Build the target's history from its own capture, only while this very walk still owns the target."""
|
|
146
|
+
if self.session_registry_controller.navigation_owner_by_id.get(target_id) != owner_id or self.repository.identity_by_session.get(target_id) != identity:
|
|
147
|
+
return
|
|
148
|
+
self.repository.read_snapshot(identity, reference_snapshot=snapshot if error is None else None)
|
|
149
|
+
|
|
150
|
+
self.reference.read(context, self.repository.git_filename, identity.generation, target_references_read)
|
|
151
|
+
|
|
152
|
+
def navigation_snapshot_read(self, identity: RequestIdentity, snapshot: HistorySnapshot) -> None:
|
|
153
|
+
"""Keep the capture of a repository a navigation reads, then find the commits its window will show.
|
|
154
|
+
|
|
155
|
+
Connected before the history read's own slot, which ignores the identities a navigation owns: the two never act
|
|
156
|
+
on the same capture.
|
|
157
|
+
"""
|
|
158
|
+
if identity.session_id not in self.session_registry_controller.navigation_owner_by_id:
|
|
159
|
+
return
|
|
160
|
+
if snapshot.error:
|
|
161
|
+
self._navigation_failed(identity.session_id, snapshot.error)
|
|
162
|
+
elif snapshot.complete and identity.session_id not in self.navigation_snapshot_by_id:
|
|
163
|
+
self.navigation_snapshot_by_id[identity.session_id] = snapshot
|
|
164
|
+
self.navigation_commits_by_id[identity.session_id] = []
|
|
165
|
+
owner_id = self.session_registry_controller.navigation_owner_by_id[identity.session_id]
|
|
166
|
+
context, _commit = self.navigation_context_by_id[identity.session_id]
|
|
167
|
+
# A repository chosen in the switch menu opens like a new window: no remembered filter applies to it.
|
|
168
|
+
anchor = None
|
|
169
|
+
if self.navigation_kind_by_id.get(identity.session_id) is NavigationKind.SUBMODULE:
|
|
170
|
+
anchor = self.anchor_by_window.get(owner_id, {}).get(WorktreeKey.from_context(context))
|
|
171
|
+
filter_text = anchor.filter_text if anchor else ""
|
|
172
|
+
filter_mode = anchor.filter_mode if anchor else HISTORY_FILTER_INITIAL_MODE
|
|
173
|
+
streamed_history = self.navigation_streamed_history_by_id.get(identity.session_id)
|
|
174
|
+
commits = streamed_history.complete_commits(snapshot) if streamed_history is not None and not filter_text else None
|
|
175
|
+
if commits is not None:
|
|
176
|
+
# Tied to the owner's history list: a window closed before the next turn takes the answer with it,
|
|
177
|
+
# instead of publishing into a list that no longer exists.
|
|
178
|
+
QTimer.singleShot(0, self.session_registry_controller.window_by_session[owner_id].commit_list, lambda: self.navigation_matching_read(identity, commits, len(commits), True))
|
|
179
|
+
return
|
|
180
|
+
self.repository.read_filter_matches(snapshot, filter_text, filter_mode, identity)
|
|
181
|
+
|
|
182
|
+
def navigate_repository(self, session_id: str, repository_dir: str) -> None:
|
|
183
|
+
"""Revalidate a declared target and explain refusals while retaining the current view."""
|
|
184
|
+
if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE) or session_id in self.navigation_target_by_session:
|
|
185
|
+
return
|
|
186
|
+
session = self.session_registry_controller.session_by_id[session_id]
|
|
187
|
+
if canonical_path(repository_dir) == canonical_path(session.repository_context.repository_dir):
|
|
188
|
+
return
|
|
189
|
+
snapshot = self.refresh_controller.submodule_snapshot_by_session.get(session_id)
|
|
190
|
+
if snapshot is None or not any(entry.repository_dir and canonical_path(entry.repository_dir) == canonical_path(repository_dir) for entry in snapshot.entries):
|
|
191
|
+
return
|
|
192
|
+
showing_window = self.opening_controller.window_showing_folder(repository_dir, other_than=session_id)
|
|
193
|
+
if showing_window is not None:
|
|
194
|
+
self.opening_controller.bring_forward(showing_window)
|
|
195
|
+
return
|
|
196
|
+
if not self.close_readiness_controller.navigation_ready(session_id):
|
|
197
|
+
self.session_registry_controller.navigation_request_by_session[session_id] = (session, repository_dir, NavigationKind.SUBMODULE)
|
|
198
|
+
self.close_readiness_controller.show_close_wait(session_id, navigation=True)
|
|
199
|
+
self.close_readiness_controller.close_timer.start()
|
|
200
|
+
return
|
|
201
|
+
self.anchor_by_window.setdefault(session_id, {})[WorktreeKey.from_context(session.repository_context)] = self.session_registry_controller.window_by_session[session_id].capture_anchor()
|
|
202
|
+
target_id = uuid.uuid4().hex
|
|
203
|
+
self.session_registry_controller.navigation_owner_by_id[target_id] = session_id
|
|
204
|
+
self.navigation_target_by_session[session_id] = repository_dir
|
|
205
|
+
self.navigation_kind_by_id[target_id] = NavigationKind.SUBMODULE
|
|
206
|
+
observation = NavigationFamily(session, self.session_registry_controller.identity_by_session[session_id], self.refresh_controller.submodule_read_generation_by_session.get(session_id, 0))
|
|
207
|
+
self.navigation_family_by_id[target_id] = observation
|
|
208
|
+
self.session_registry_controller.window_by_session[session_id].statusBar().showMessage("Reading submodules…")
|
|
209
|
+
|
|
210
|
+
def prepared(family: SubmoduleSnapshot | None, error: Exception | None) -> None:
|
|
211
|
+
"""Recheck declarations and path components before reading the target repository."""
|
|
212
|
+
if self.session_registry_controller.navigation_owner_by_id.get(target_id) != session_id:
|
|
213
|
+
return
|
|
214
|
+
if self.session_registry_controller.session_by_id.get(session_id) is not observation.session:
|
|
215
|
+
self._navigation_failed(target_id, "The requesting window changed repository. Open the submodule again.")
|
|
216
|
+
return
|
|
217
|
+
if error is not None or family is None:
|
|
218
|
+
self._navigation_failed(target_id, str(error))
|
|
219
|
+
return
|
|
220
|
+
# A newer family read or a refresh of the source window started after this walk: the new window reads its family again instead.
|
|
221
|
+
if self.session_registry_controller.identity_by_session.get(session_id) == observation.identity and self.refresh_controller.submodule_read_generation_by_session.get(session_id, 0) == observation.generation:
|
|
222
|
+
observation.family = family
|
|
223
|
+
for entry in family.entries:
|
|
224
|
+
if not entry.repository_dir or canonical_path(entry.repository_dir) != canonical_path(repository_dir):
|
|
225
|
+
continue
|
|
226
|
+
try:
|
|
227
|
+
if entry.parent_dir is not None:
|
|
228
|
+
declared_directory(entry.parent_dir, entry.path)
|
|
229
|
+
except (OSError, ValueError) as failure:
|
|
230
|
+
self._navigation_failed(target_id, str(failure))
|
|
231
|
+
return
|
|
232
|
+
self.refresh_controller.submodule_snapshot_by_session[session_id] = family
|
|
233
|
+
partial = self.submodule.has_incomplete_initialization(entry.repository_dir)
|
|
234
|
+
if entry.availability == "not initialized" or entry.availability == "initialized" and partial:
|
|
235
|
+
self._initialize_submodule(target_id, entry)
|
|
236
|
+
return
|
|
237
|
+
if entry.availability != "initialized":
|
|
238
|
+
command = ""
|
|
239
|
+
hint = ""
|
|
240
|
+
if entry.availability == "occupied" and entry.parent_dir is not None:
|
|
241
|
+
powershell = os.name == "nt"
|
|
242
|
+
command = initialization_command(self.repository.git_filename, entry.parent_dir, entry.path, powershell=powershell)
|
|
243
|
+
hint = "After clearing the folder, run this command in " + ("PowerShell:" if powershell else "your terminal:")
|
|
244
|
+
self._navigation_failed(target_id, entry.cause or "The submodule path is not available.", recovery_command=command, recovery_hint=hint)
|
|
245
|
+
return
|
|
246
|
+
self.repository.open_repository(repository_dir, RequestIdentity(target_id, session.generation + 1))
|
|
247
|
+
return
|
|
248
|
+
self._navigation_failed(target_id, "The declared repository is no longer available. Refresh the repository to retry.")
|
|
249
|
+
|
|
250
|
+
self.submodule.read(session.repository_context, self.repository.git_filename, prepared, snapshot.root_dir)
|
|
251
|
+
|
|
252
|
+
def switch_repository(self, session_id: str, repository_dir: str) -> None:
|
|
253
|
+
"""Open a repository chosen in the switch menu in the same window.
|
|
254
|
+
|
|
255
|
+
The current repository stays displayed and usable until the chosen one has been read; only then does the window
|
|
256
|
+
change, through the same publication as a walk into a submodule. Choosing the displayed repository does nothing.
|
|
257
|
+
While the chosen repository is being read, another choice is refused, since that read cannot be taken back.
|
|
258
|
+
While the change still waits for Git in the current repository, nothing has started yet: the last choice
|
|
259
|
+
replaces the waiting one, and the status bar names the repository that will open.
|
|
260
|
+
"""
|
|
261
|
+
if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
|
|
262
|
+
return
|
|
263
|
+
session = self.session_registry_controller.session_by_id[session_id]
|
|
264
|
+
current_dir = session.repository_context.repository_dir
|
|
265
|
+
# Menu entries are saved resolved, so the text alone recognizes the current repository without touching the disk.
|
|
266
|
+
if saved_recent_identity(repository_dir) == saved_recent_identity(current_dir):
|
|
267
|
+
return
|
|
268
|
+
window = self.session_registry_controller.window_by_session[session_id]
|
|
269
|
+
opening_dir = self.navigation_target_by_session.get(session_id)
|
|
270
|
+
if opening_dir is not None:
|
|
271
|
+
window.statusBar().showMessage(SWITCH_PENDING_STATUS.format(directory=opening_dir))
|
|
272
|
+
return
|
|
273
|
+
# A folder that no longer exists is not handed to Git: the current repository stays, and the entry may be removed.
|
|
274
|
+
# It is checked before any path is resolved, since resolving a folder on an unreachable drive freezes the window.
|
|
275
|
+
if not os.path.isdir(repository_dir):
|
|
276
|
+
self._offer_invalid_repository_removal(session_id, repository_dir)
|
|
277
|
+
return
|
|
278
|
+
if canonical_path(repository_dir) == canonical_path(current_dir):
|
|
279
|
+
return
|
|
280
|
+
showing_window = self.opening_controller.window_showing_folder(repository_dir, other_than=session_id)
|
|
281
|
+
if showing_window is not None:
|
|
282
|
+
self.opening_controller.bring_forward(showing_window)
|
|
283
|
+
return
|
|
284
|
+
# Changing the repository closes every window tied to the current one, with what the user typed in it: the user closes them instead.
|
|
285
|
+
related_title = self._open_related_window_title(session_id)
|
|
286
|
+
if related_title:
|
|
287
|
+
self._refuse_switch(session_id, related_title)
|
|
288
|
+
return
|
|
289
|
+
# Git may still be writing in the current repository: the change waits, then resumes through this same method.
|
|
290
|
+
if not self.close_readiness_controller.navigation_ready(session_id):
|
|
291
|
+
self.session_registry_controller.navigation_request_by_session[session_id] = (session, repository_dir, NavigationKind.SWITCH)
|
|
292
|
+
self.close_readiness_controller.show_close_wait(session_id, navigation=True)
|
|
293
|
+
window.statusBar().showMessage(SWITCH_WAITING_STATUS.format(directory=repository_dir))
|
|
294
|
+
self.close_readiness_controller.close_timer.start()
|
|
295
|
+
return
|
|
296
|
+
target_id = uuid.uuid4().hex
|
|
297
|
+
self.session_registry_controller.navigation_owner_by_id[target_id] = session_id
|
|
298
|
+
self.navigation_target_by_session[session_id] = repository_dir
|
|
299
|
+
self.navigation_kind_by_id[target_id] = NavigationKind.SWITCH
|
|
300
|
+
window.statusBar().showMessage(SWITCH_OPENING_STATUS.format(directory=repository_dir))
|
|
301
|
+
self.repository.open_repository(repository_dir, RequestIdentity(target_id, session.generation + 1))
|
|
302
|
+
|
|
303
|
+
def _offer_invalid_repository_removal(self, session_id: str, repository_dir: str) -> None:
|
|
304
|
+
"""Say that the chosen repository has no folder any more, and offer to remove it from the recent history.
|
|
305
|
+
|
|
306
|
+
As in Git Extensions, every other recent entry whose folder is gone can be removed at once. The other entries are
|
|
307
|
+
checked only now that the chosen one turned out to be missing, never when the menu opens, so a disconnected
|
|
308
|
+
drive in the history cannot slow down the menu. They are the entries the menu listed, compared as saved text:
|
|
309
|
+
one folder check each, with no path resolved again and no rereading of the stored preferences.
|
|
310
|
+
"""
|
|
311
|
+
if session_id in self.invalid_repository_prompt_by_session:
|
|
312
|
+
return
|
|
313
|
+
window = self.session_registry_controller.window_by_session[session_id]
|
|
314
|
+
window.statusBar().showMessage(SWITCH_MISSING_STATUS.format(directory=repository_dir))
|
|
315
|
+
chosen_identity = saved_recent_identity(repository_dir)
|
|
316
|
+
absent_dirs = [repository_dir]
|
|
317
|
+
for recent_dir in window.recent_dirs:
|
|
318
|
+
if saved_recent_identity(recent_dir) == chosen_identity or os.path.isdir(recent_dir):
|
|
319
|
+
continue
|
|
320
|
+
absent_dirs.append(recent_dir)
|
|
321
|
+
prompt = InvalidRepositoryPrompt(repository_dir, len(absent_dirs), window)
|
|
322
|
+
self.invalid_repository_prompt_by_session[session_id] = prompt
|
|
323
|
+
prompt.remove_selected_requested.connect(lambda: self._forget_repositories(session_id, [repository_dir]))
|
|
324
|
+
prompt.remove_all_requested.connect(lambda: self._forget_repositories(session_id, absent_dirs))
|
|
325
|
+
|
|
326
|
+
def finish() -> None:
|
|
327
|
+
"""Forget the prompt once answered, unless a newer one already replaced it."""
|
|
328
|
+
if self.invalid_repository_prompt_by_session.get(session_id) is prompt:
|
|
329
|
+
self.invalid_repository_prompt_by_session.pop(session_id, None)
|
|
330
|
+
prompt.deleteLater()
|
|
331
|
+
|
|
332
|
+
prompt.finished.connect(finish)
|
|
333
|
+
open_window_modal(prompt)
|
|
334
|
+
|
|
335
|
+
def _forget_repositories(self, session_id: str, repository_dirs: list[str]) -> None:
|
|
336
|
+
"""Remove these entries from the recent history, never any folder.
|
|
337
|
+
|
|
338
|
+
The menus of every window and the open chooser follow through the usual preference change relay. A refused save
|
|
339
|
+
is explained in the status bar of the window and written to the log.
|
|
340
|
+
"""
|
|
341
|
+
try:
|
|
342
|
+
self.preference.forget(repository_dirs)
|
|
343
|
+
except OSError as error:
|
|
344
|
+
diagnostic = Diagnostic(FORGET_OPERATION, repository_dirs[0] if repository_dirs else "", self.repository.git_filename, [], str(error)).prepare()
|
|
345
|
+
window = self.session_registry_controller.window_by_session.get(session_id)
|
|
346
|
+
if window is not None and isValid(window):
|
|
347
|
+
window.statusBar().showMessage(diagnostic.cause)
|
|
348
|
+
if self.diagnostic_log is not None:
|
|
349
|
+
self.diagnostic_log.write(diagnostic)
|
|
350
|
+
|
|
351
|
+
def _open_related_window_title(self, session_id: str) -> str:
|
|
352
|
+
"""Name a window still open on the repository of this view, which has to be closed before the view changes repository.
|
|
353
|
+
|
|
354
|
+
Every dialog and console of the window has the window as parent, finished consoles and confirmations included;
|
|
355
|
+
the Staging window is separate but opened from this view. Nothing is closed on the user's behalf, since these
|
|
356
|
+
windows hold what the user typed or still has to read. Settings is not checked: it is shared by every window and
|
|
357
|
+
has no Qt parent, so it stays on the repository it was opened for, whose path its local section shows. File
|
|
358
|
+
history windows are separate views with their own repository. An empty result means nothing is in the way.
|
|
359
|
+
"""
|
|
360
|
+
window = self.session_registry_controller.window_by_session.get(session_id)
|
|
361
|
+
if window is None or not isValid(window):
|
|
362
|
+
return ""
|
|
363
|
+
for dialog in window.findChildren(QDialog):
|
|
364
|
+
if dialog.isVisible():
|
|
365
|
+
return dialog.windowTitle() or UNTITLED_RELATED_WINDOW_TITLE
|
|
366
|
+
session = self.session_registry_controller.session_by_id.get(session_id)
|
|
367
|
+
if session is None:
|
|
368
|
+
return ""
|
|
369
|
+
staging = self.staging_session_registry_controller.staging_by_key.get(WorktreeKey.from_context(session.repository_context))
|
|
370
|
+
if staging is None or staging.commit_push_session_id != session_id:
|
|
371
|
+
return ""
|
|
372
|
+
staging_window = staging.window
|
|
373
|
+
if staging_window is None or not isValid(staging_window) or not staging_window.isVisible():
|
|
374
|
+
return ""
|
|
375
|
+
return staging_window.windowTitle()
|
|
376
|
+
|
|
377
|
+
def _refuse_switch(self, session_id: str, title: str) -> None:
|
|
378
|
+
"""Explain that the repository stays because a window tied to it is still open, in the status bar and in a message."""
|
|
379
|
+
window = self.session_registry_controller.window_by_session[session_id]
|
|
380
|
+
text = SWITCH_REFUSED_TEXT.format(title=title)
|
|
381
|
+
window.statusBar().showMessage(text)
|
|
382
|
+
message = QMessageBox(QMessageBox.Icon.Information, SWITCH_REFUSED_TITLE, text, QMessageBox.StandardButton.Ok, window)
|
|
383
|
+
prepare_message_box(message, SWITCH_REFUSED_TITLE)
|
|
384
|
+
message.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
|
|
385
|
+
open_window_modal(message)
|
|
386
|
+
|
|
387
|
+
def _initialize_submodule(self, target_id: str, entry: SubmoduleEntry) -> None:
|
|
388
|
+
"""Keep the source window intact while displaying supervised initialization output."""
|
|
389
|
+
session_id = self.session_registry_controller.navigation_owner_by_id[target_id]
|
|
390
|
+
session = self.session_registry_controller.session_by_id[session_id]
|
|
391
|
+
window = self.session_registry_controller.window_by_session[session_id]
|
|
392
|
+
window.statusBar().showMessage("Initializing…")
|
|
393
|
+
window.show_submodule_initializing(entry.repository_dir)
|
|
394
|
+
|
|
395
|
+
def current() -> bool:
|
|
396
|
+
"""Reject results after closure or a replacement repository session."""
|
|
397
|
+
return self.session_registry_controller.navigation_owner_by_id.get(target_id) == session_id and self.session_registry_controller.session_by_id.get(session_id) is session
|
|
398
|
+
|
|
399
|
+
def complete(run: SubmoduleInitRun) -> None:
|
|
400
|
+
"""Open only after complete recursive validation, retaining partial failures for retry."""
|
|
401
|
+
output = self.submodule_output_by_id.get(run.operation_id)
|
|
402
|
+
if output is not None and isValid(output):
|
|
403
|
+
output.show_result(OperationStatus(run.status))
|
|
404
|
+
if not current():
|
|
405
|
+
return
|
|
406
|
+
if run.status != OperationStatus.SUCCESS.value or run.validation_status != OperationStatus.SUCCESS.value:
|
|
407
|
+
window.show_submodules(self.refresh_controller.submodule_snapshot_by_session[session_id])
|
|
408
|
+
# The console of a failure never closes by itself and already shows Git's lines and the cause: an error
|
|
409
|
+
# window opened after it would only cover it. Without a visible console, that window is the only place
|
|
410
|
+
# where the failure can be read.
|
|
411
|
+
console_shown = output is not None and isValid(output) and output.isVisible()
|
|
412
|
+
# A refused credential manager opens the install window, which already says what to do while the console
|
|
413
|
+
# keeps its red line: no error window comes on top of it.
|
|
414
|
+
install_window_shown = run.credential_manager_refusal is not None and self.account_controller.offer_credential_manager_install(run.credential_manager_refusal, window)
|
|
415
|
+
self._navigation_failed(target_id, run.diagnostic.detail if run.diagnostic else "Submodule initialization did not complete. " + OPEN_AGAIN_RETRY_SENTENCE,
|
|
416
|
+
show_window=not console_shown and not install_window_shown)
|
|
417
|
+
self._offer_account_after_submodule_refusal(session_id, entry, run)
|
|
418
|
+
return
|
|
419
|
+
self.refresh_controller.submodule_snapshot_by_session[session_id] = run.snapshot
|
|
420
|
+
observation = self.navigation_family_by_id.get(target_id)
|
|
421
|
+
if observation is not None:
|
|
422
|
+
# The family read before initialization showed the child as missing; the one validated at its end replaces it.
|
|
423
|
+
observation.family = run.snapshot
|
|
424
|
+
self.repository.open_repository(entry.repository_dir, RequestIdentity(target_id, session.generation + 1))
|
|
425
|
+
|
|
426
|
+
run = self.submodule.initialize(session.repository_context, self.repository.git_filename, entry, complete,
|
|
427
|
+
session_id=session_id, generation=session.generation, is_current=current)
|
|
428
|
+
self.submodule_run_by_navigation[target_id] = run
|
|
429
|
+
output = OperationDialog(run.trace, "Initialize submodule", window, preference=self.preference, console_operation=ConsoleOperation.SUBMODULE_INIT)
|
|
430
|
+
self.submodule_output_by_id[run.operation_id] = output
|
|
431
|
+
output.abort_requested.connect(lambda: self.submodule.cancel(run.operation_id))
|
|
432
|
+
output.finished.connect(run.trace.close)
|
|
433
|
+
output.set_running()
|
|
434
|
+
open_window_modal(output, keeps_window_buttons=True)
|
|
435
|
+
|
|
436
|
+
def _offer_account_after_submodule_refusal(self, session_id: str, entry: SubmoduleEntry, run: SubmoduleInitRun) -> None:
|
|
437
|
+
"""Open Settings on Accounts when a submodule initialization was refused for an HTTPS account.
|
|
438
|
+
|
|
439
|
+
The run keeps no URL, so the address comes from Git's own refusal line; the account named is the one this
|
|
440
|
+
address would use now, or none when the account list cannot be read.
|
|
441
|
+
"""
|
|
442
|
+
if run.diagnostic is None:
|
|
443
|
+
return
|
|
444
|
+
url = refused_https_url(run.diagnostic.stderr)
|
|
445
|
+
if not url:
|
|
446
|
+
return
|
|
447
|
+
try:
|
|
448
|
+
account = transfer_account(self.authentication.account_for_endpoint(url), url)
|
|
449
|
+
except (OSError, ValueError, TypeError, KeyError):
|
|
450
|
+
account = transfer_account(None, url)
|
|
451
|
+
notice = account_notice(run.diagnostic, account, SUBMODULE_NOTICE_SUBJECT_TEXT.format(path=entry.path))
|
|
452
|
+
if notice is not None:
|
|
453
|
+
self.account_controller.offer_account_after_refusal(session_id, notice)
|
|
454
|
+
|
|
455
|
+
def _abandon_navigation(self, target_id: str) -> tuple[str | None, str, NavigationKind | None]:
|
|
456
|
+
"""Drop everything an unpublished change of repository read or planned, leaving the source window as it is.
|
|
457
|
+
|
|
458
|
+
Returns the window that asked for the change, the folder it was going to and the kind of change, so the caller
|
|
459
|
+
can explain why it stopped; the window is None when it no longer waits for this change. A wait message the
|
|
460
|
+
change left in the window is replaced by the real rebase state, since the repository stays.
|
|
461
|
+
"""
|
|
462
|
+
run = self.submodule_run_by_navigation.pop(target_id, None)
|
|
463
|
+
if run is not None:
|
|
464
|
+
run.open_status = OperationStatus.FAILED.value
|
|
465
|
+
owner_id = self.session_registry_controller.navigation_owner_by_id.pop(target_id, None)
|
|
466
|
+
kind = self.navigation_kind_by_id.pop(target_id, None)
|
|
467
|
+
self.navigation_family_by_id.pop(target_id, None)
|
|
468
|
+
self.navigation_context_by_id.pop(target_id, None)
|
|
469
|
+
self.navigation_snapshot_by_id.pop(target_id, None)
|
|
470
|
+
self.navigation_commits_by_id.pop(target_id, None)
|
|
471
|
+
self.navigation_streamed_history_by_id.pop(target_id, None)
|
|
472
|
+
self.navigation_resume_by_target.pop(target_id, None)
|
|
473
|
+
self.repository.cancel(target_id)
|
|
474
|
+
self.repository.context_by_session.pop(target_id, None)
|
|
475
|
+
if owner_id is None:
|
|
476
|
+
return None, "", kind
|
|
477
|
+
directory = self.navigation_target_by_session.pop(owner_id, "")
|
|
478
|
+
self.close_readiness_controller.clear_navigation_wait(owner_id)
|
|
479
|
+
return owner_id, directory, kind
|
|
480
|
+
|
|
481
|
+
def _navigation_failed(self, target_id: str, cause: str, *, recovery_command: str = "", recovery_hint: str = "", show_window: bool = True) -> None:
|
|
482
|
+
"""Discard unpublished reads and explain recovery while retaining the source repository.
|
|
483
|
+
|
|
484
|
+
The status bar always says the change of repository failed. With ``show_window`` false the caller's own console
|
|
485
|
+
already shows the failure and has written it to the --log file, so no error window opens over it and the
|
|
486
|
+
diagnostic is not written a second time.
|
|
487
|
+
"""
|
|
488
|
+
owner_id, directory, kind = self._abandon_navigation(target_id)
|
|
489
|
+
if owner_id is None:
|
|
490
|
+
return
|
|
491
|
+
if kind is None:
|
|
492
|
+
kind = NavigationKind.SUBMODULE
|
|
493
|
+
window = self.session_registry_controller.window_by_session.get(owner_id)
|
|
494
|
+
if window is not None:
|
|
495
|
+
window.statusBar().showMessage(kind.failure_status.format(directory=directory))
|
|
496
|
+
if not show_window:
|
|
497
|
+
return
|
|
498
|
+
diagnostic = Diagnostic(kind.operation_label, directory, "", [], cause).prepare()
|
|
499
|
+
dialog = ErrorDialog(diagnostic, window, recovery_command=recovery_command, recovery_hint=recovery_hint)
|
|
500
|
+
self.session_registry_controller.dialogs.append(dialog)
|
|
501
|
+
dialog.finished.connect(lambda: self.session_registry_controller.dialogs.remove(dialog) if dialog in self.session_registry_controller.dialogs else None)
|
|
502
|
+
dialog.finished.connect(dialog.deleteLater)
|
|
503
|
+
open_window_modal(dialog)
|
|
504
|
+
if self.diagnostic_log is not None:
|
|
505
|
+
self.diagnostic_log.write(diagnostic)
|
|
506
|
+
|
|
507
|
+
def publish_repository(self, target_id: str, snapshot: HistorySnapshot) -> None:
|
|
508
|
+
"""Replace all repository state together after target validation succeeds."""
|
|
509
|
+
owner_id = self.session_registry_controller.navigation_owner_by_id.get(target_id)
|
|
510
|
+
if owner_id not in self.session_registry_controller.window_by_session or target_id not in self.navigation_context_by_id:
|
|
511
|
+
self._navigation_failed(target_id, "The requesting window is closed.")
|
|
512
|
+
return
|
|
513
|
+
if snapshot.error:
|
|
514
|
+
self._navigation_failed(target_id, snapshot.error)
|
|
515
|
+
return
|
|
516
|
+
if not self.close_readiness_controller.navigation_ready(owner_id):
|
|
517
|
+
self.navigation_resume_by_target[target_id] = snapshot
|
|
518
|
+
self.close_readiness_controller.show_close_wait(owner_id, navigation=True)
|
|
519
|
+
self.close_readiness_controller.close_timer.start()
|
|
520
|
+
return
|
|
521
|
+
if self.navigation_kind_by_id.get(target_id) is NavigationKind.SWITCH:
|
|
522
|
+
# The current repository stayed usable while the chosen one was read, so a dialog may have opened on it
|
|
523
|
+
# meanwhile; publishing now would close it with its content. The change gives up instead, remembering nothing.
|
|
524
|
+
related_title = self._open_related_window_title(owner_id)
|
|
525
|
+
if related_title:
|
|
526
|
+
self._abandon_navigation(target_id)
|
|
527
|
+
self._refuse_switch(owner_id, related_title)
|
|
528
|
+
return
|
|
529
|
+
run = self.submodule_run_by_navigation.get(target_id)
|
|
530
|
+
if run is not None and snapshot.head_oid != run.intent.expected_oid:
|
|
531
|
+
self._navigation_failed(target_id, "The submodule checkout changed before opening. The current repository is still open. Refresh and retry.")
|
|
532
|
+
return
|
|
533
|
+
run = self.submodule_run_by_navigation.pop(target_id, None)
|
|
534
|
+
if run is not None:
|
|
535
|
+
run.open_status = OperationStatus.SUCCESS.value
|
|
536
|
+
observation = self.navigation_family_by_id.pop(target_id, None)
|
|
537
|
+
# The family this walk read still describes the source window only if nothing replaced that window's session,
|
|
538
|
+
# identity or family read since the walk started; otherwise the new window reads its family again.
|
|
539
|
+
family = None
|
|
540
|
+
if (observation is not None and observation.session is self.session_registry_controller.session_by_id.get(owner_id) and observation.identity == self.session_registry_controller.identity_by_session.get(owner_id)
|
|
541
|
+
and observation.generation == self.refresh_controller.submodule_read_generation_by_session.get(owner_id, 0)):
|
|
542
|
+
family = observation.family
|
|
543
|
+
context, commit = self.navigation_context_by_id.pop(target_id)
|
|
544
|
+
commits = self.navigation_commits_by_id.pop(target_id)
|
|
545
|
+
streamed_history = self.navigation_streamed_history_by_id.pop(target_id, None)
|
|
546
|
+
self.navigation_snapshot_by_id.pop(target_id, None)
|
|
547
|
+
self.session_registry_controller.navigation_owner_by_id.pop(target_id)
|
|
548
|
+
kind = self.navigation_kind_by_id.pop(target_id, NavigationKind.SUBMODULE)
|
|
549
|
+
self.navigation_target_by_session.pop(owner_id, None)
|
|
550
|
+
window = self.session_registry_controller.window_by_session[owner_id]
|
|
551
|
+
previous = self.session_registry_controller.session_by_id[owner_id]
|
|
552
|
+
generation = previous.generation + 1
|
|
553
|
+
self.repository_close_requested.emit(owner_id)
|
|
554
|
+
self.repository.cancel(target_id)
|
|
555
|
+
self.repository.context_by_session.pop(target_id, None)
|
|
556
|
+
identity = RequestIdentity(owner_id, generation)
|
|
557
|
+
session = RepositorySession(owner_id, context, self.repository)
|
|
558
|
+
session.generation = generation
|
|
559
|
+
self.session_registry_controller.session_by_id[owner_id] = session
|
|
560
|
+
self.session_registry_controller.window_by_session[owner_id] = window
|
|
561
|
+
self.session_registry_controller.identity_by_session[owner_id] = identity
|
|
562
|
+
self.session_registry_controller.generation_by_session[owner_id] = generation
|
|
563
|
+
self.repository.context_by_session[owner_id] = context
|
|
564
|
+
self.repository.identity_by_session[owner_id] = identity
|
|
565
|
+
self.repository.executable_by_session[owner_id] = self.repository.git_filename
|
|
566
|
+
session.selection_changed.connect(lambda oid: window.diff_panel.clear_selection("Loading diff…"))
|
|
567
|
+
session.selection_changed.connect(lambda oid: window.file_tree_panel.clear_selection("Loading tree…"))
|
|
568
|
+
session.closed.connect(window.file_tree_panel.clear_selection)
|
|
569
|
+
session.navigation_requested.connect(lambda oid: self.history_read_controller.read_navigation(owner_id, oid))
|
|
570
|
+
# The window has seen nothing of this worktree's pause state yet. A walk into a submodule asks no observation of
|
|
571
|
+
# its own and relies on the automatic one of its first worktree read; an observation made for an earlier visit,
|
|
572
|
+
# less than the refresh interval ago, would hold that one back, and a quick return to a parent paused in a rebase
|
|
573
|
+
# or a replay would then show neither its banner nor its rebase window.
|
|
574
|
+
new_key = WorktreeKey.from_context(context)
|
|
575
|
+
self.rebase_controller.rebase_observed_at_by_key.pop(new_key, None)
|
|
576
|
+
self.replay_controller.replay_observed_at_by_key.pop(new_key, None)
|
|
577
|
+
switched = kind is NavigationKind.SWITCH
|
|
578
|
+
if switched:
|
|
579
|
+
# A repository chosen in the switch menu opens like a new window: no position, filter or submodule family
|
|
580
|
+
# of the repository left behind may come back, now or when the window later reads its family again.
|
|
581
|
+
self.anchor_by_window.pop(owner_id, None)
|
|
582
|
+
self.refresh_controller.submodule_snapshot_by_session.pop(owner_id, None)
|
|
583
|
+
window.show_repository(context, fresh=switched)
|
|
584
|
+
anchor = None if switched else self.anchor_by_window.get(owner_id, {}).get(WorktreeKey.from_context(context))
|
|
585
|
+
selected_oid = anchor.selected_oid if anchor else (commit.oid if commit else None)
|
|
586
|
+
window.clear_commit_selection()
|
|
587
|
+
with QSignalBlocker(window.filter_edit):
|
|
588
|
+
window.filter_edit.setText(anchor.filter_text if anchor else "")
|
|
589
|
+
window.show_filter_mode(anchor.filter_mode if anchor else HISTORY_FILTER_INITIAL_MODE)
|
|
590
|
+
session.filter_text = window.filter_edit.text()
|
|
591
|
+
snapshot.generation = generation
|
|
592
|
+
session.snapshot = snapshot
|
|
593
|
+
session.streamed_history = streamed_history
|
|
594
|
+
self.repository.snapshot_by_session[owner_id] = snapshot
|
|
595
|
+
missing_selection = anchor is not None and selected_oid is not None and not any(item.oid == selected_oid for item in commits)
|
|
596
|
+
if missing_selection:
|
|
597
|
+
with QSignalBlocker(window):
|
|
598
|
+
window.show_matches(snapshot, commits, session.filter_text, selected_oid)
|
|
599
|
+
window.restore_anchor(anchor)
|
|
600
|
+
window.clear_commit_selection()
|
|
601
|
+
window.statusBar().showMessage("The previously selected commit is no longer available. Select a commit to continue.")
|
|
602
|
+
else:
|
|
603
|
+
window.show_matches(snapshot, commits, session.filter_text, selected_oid)
|
|
604
|
+
if anchor is not None:
|
|
605
|
+
window.restore_anchor(anchor)
|
|
606
|
+
# A remote read the previous repository started for the Fetch menu was dropped with its session and never
|
|
607
|
+
# releases the button: the busy state follows the transports of the repository shown now.
|
|
608
|
+
window.set_fetch_busy(self.transport_controller.fetch_busy_for(context.common_git_dir))
|
|
609
|
+
if switched:
|
|
610
|
+
# As when a window opens: the pause banners of the new repository are read now rather than at the next
|
|
611
|
+
# working-tree count, and a paused rebase offers its dialog. The automatic observation that follows is held
|
|
612
|
+
# back by the rebase refresh interval, so the state is not read twice.
|
|
613
|
+
self.rebase_controller.observe_rebase(owner_id, open_paused=True)
|
|
614
|
+
if family is not None:
|
|
615
|
+
family_generation = self.refresh_controller.submodule_read_generation_by_session.get(owner_id, 0) + 1
|
|
616
|
+
self.refresh_controller.submodule_read_generation_by_session[owner_id] = family_generation
|
|
617
|
+
self.refresh_controller.publish_submodule_family(owner_id, family, identity, family_generation)
|
|
618
|
+
elif not switched:
|
|
619
|
+
self.session_registry_controller.window_by_session[owner_id].show_submodules(self.refresh_controller.submodule_snapshot_by_session[owner_id])
|
|
620
|
+
# The capture the target's history was built from becomes the new window's branch list once its reference files
|
|
621
|
+
# are checked unchanged; only its generation changes, to the new window's.
|
|
622
|
+
previous = replace(snapshot.reference_snapshot, generation=generation) if snapshot.reference_snapshot is not None else None
|
|
623
|
+
self.reference_read_controller.read_references(owner_id, previous=previous)
|
|
624
|
+
if WindowCapability.MANAGE in session.capabilities and not context.is_bare:
|
|
625
|
+
# The window shows no count of this worktree yet (a switch cleared it, a walk still shows the previous
|
|
626
|
+
# repository's), and a worktree read earlier in the run answers with what it already had.
|
|
627
|
+
self.staging_session_registry_controller.staging_session(context).refresh(announce=True)
|
|
628
|
+
if family is None:
|
|
629
|
+
self.refresh_controller.read_submodules(owner_id)
|
|
630
|
+
diagnostic = self.opening_controller.remember_repository(context.repository_dir)
|
|
631
|
+
if switched:
|
|
632
|
+
# A window just opened takes its reading proportions from the preferences, not from the repository left behind.
|
|
633
|
+
window.apply_reading_preference(self.preference.value_by_key)
|
|
634
|
+
if diagnostic is not None:
|
|
635
|
+
self.opening_controller.show_preference_failure(diagnostic, window)
|
|
636
|
+
|
|
637
|
+
def navigation_commit_batch_read(self, identity: RequestIdentity, commits: list[Commit]) -> None:
|
|
638
|
+
"""Keep the rows a navigation's history read just parsed, so the window it replaces can show them without reading Git again."""
|
|
639
|
+
if identity.session_id not in self.session_registry_controller.navigation_owner_by_id:
|
|
640
|
+
return
|
|
641
|
+
snapshot = self.repository.snapshot_by_session.get(identity.session_id)
|
|
642
|
+
if snapshot is not None:
|
|
643
|
+
previous = self.navigation_streamed_history_by_id.get(identity.session_id)
|
|
644
|
+
self.navigation_streamed_history_by_id[identity.session_id] = keep_streamed_commits(previous, snapshot, commits)
|
|
645
|
+
|
|
646
|
+
def navigation_matching_read(self, identity: RequestIdentity, commits: list[Commit], scanned_count: int, complete: bool) -> None:
|
|
647
|
+
"""Gather the commits found for a navigation, then replace the window's repository once the last ones arrive."""
|
|
648
|
+
if identity.session_id not in self.session_registry_controller.navigation_owner_by_id:
|
|
649
|
+
return
|
|
650
|
+
self.navigation_commits_by_id[identity.session_id].extend(commits)
|
|
651
|
+
if complete:
|
|
652
|
+
self.publish_repository(identity.session_id, self.navigation_snapshot_by_id[identity.session_id])
|
|
653
|
+
|
|
654
|
+
def navigation_read_failed(self, identity: RequestIdentity, failure: RepositoryFailure) -> None:
|
|
655
|
+
"""Give up a navigation whose repository read failed, keeping the window on the repository it shows."""
|
|
656
|
+
if identity.session_id not in self.session_registry_controller.navigation_owner_by_id:
|
|
657
|
+
return
|
|
658
|
+
self._navigation_failed(identity.session_id, failure.cause)
|