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,375 @@
|
|
|
1
|
+
"""Open a repository window once Git has read its repository."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import threading
|
|
7
|
+
from functools import partial
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from PySide6.QtCore import QObject, Qt, Signal
|
|
11
|
+
from shiboken6 import isValid
|
|
12
|
+
|
|
13
|
+
from gitpane.controller.session_registry import RepositorySession
|
|
14
|
+
from gitpane.controller.setting_controller import WaitingOpening
|
|
15
|
+
from gitpane.history.file_history import WindowCapability
|
|
16
|
+
from gitpane.history.repository_value import RepositoryFailure, RequestIdentity
|
|
17
|
+
from gitpane.operation.diagnostic import Diagnostic
|
|
18
|
+
from gitpane.recent.recent_availability import folder_is_reachable
|
|
19
|
+
from gitpane.system.path_identity import canonical_path
|
|
20
|
+
from gitpane.thread.weak_signal import weak_signal_emitter
|
|
21
|
+
from gitpane.ui.console.error_dialog import ErrorDialog
|
|
22
|
+
from gitpane.ui.repository.repository_window import RepositoryWindow
|
|
23
|
+
from gitpane.ui.window_control.dialog_modality import open_window_modal
|
|
24
|
+
from gitpane.ui.window_control.window_placement import is_visible_window
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from collections.abc import Callable
|
|
28
|
+
|
|
29
|
+
from PySide6.QtWidgets import QWidget
|
|
30
|
+
|
|
31
|
+
from gitpane.account.authentication import AuthenticationService
|
|
32
|
+
from gitpane.controller.clone_controller import CloneController
|
|
33
|
+
from gitpane.controller.history_read_controller import HistoryReadController
|
|
34
|
+
from gitpane.controller.operation_policy import OperationPolicy
|
|
35
|
+
from gitpane.controller.reference_read_controller import ReferenceReadController
|
|
36
|
+
from gitpane.controller.refresh_controller import RefreshController
|
|
37
|
+
from gitpane.controller.session_registry import SessionRegistry
|
|
38
|
+
from gitpane.controller.setting_controller import SettingController
|
|
39
|
+
from gitpane.controller.staging_session_registry import StagingSessionRegistry
|
|
40
|
+
from gitpane.controller.window_activation import WindowActivationFilter
|
|
41
|
+
from gitpane.history.file_history import FileHistoryAnchor
|
|
42
|
+
from gitpane.history.repository import GitRepository
|
|
43
|
+
from gitpane.history.repository_value import Commit, RepositoryContext
|
|
44
|
+
from gitpane.operation.diagnostic import DiagnosticLog, PreparedDiagnostic
|
|
45
|
+
from gitpane.preference.preference_store import PreferenceStore
|
|
46
|
+
from gitpane.transport.clone import CloneService
|
|
47
|
+
|
|
48
|
+
OPENING_FOLDER_THREAD_NAME = "gitpane-opening-folder-check"
|
|
49
|
+
MISSING_FOLDER_LOG_TEXT = "Open repository: no folder exists at {path}; Git was not started."
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def answer_folder_question(is_folder_present: Callable[[str], bool], folder_dir: str, hand_over: Callable[[bool | None], None]) -> None:
|
|
53
|
+
"""Ask on the worker thread whether the folder exists, and hand the answer over to the controller's thread.
|
|
54
|
+
|
|
55
|
+
The answer is None when the question gave no definite answer (a refused access, a disk not ready, a path the system
|
|
56
|
+
cannot read as written): the opening then goes on to Git as it would without the question.
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
present: bool | None = is_folder_present(folder_dir)
|
|
60
|
+
except (OSError, ValueError):
|
|
61
|
+
present = None
|
|
62
|
+
# The controller may have been deleted while the question ran: the answer then has nobody left to take it.
|
|
63
|
+
try:
|
|
64
|
+
hand_over(present)
|
|
65
|
+
except RuntimeError:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class OpeningController(QObject):
|
|
70
|
+
"""The openings asked from the welcome window, the clone form and the file histories: the checks before a window opens, then the actions that follow its wiring."""
|
|
71
|
+
|
|
72
|
+
# Session of a repository window just opened, whose worktree may be paused by a rebase started before it opened.
|
|
73
|
+
pause_observation_requested = Signal(str)
|
|
74
|
+
# Answer of the folder question of a welcome opening: its identity, the folder, and True, False or None when unknown.
|
|
75
|
+
folder_answered = Signal(object, str, object)
|
|
76
|
+
|
|
77
|
+
def __init__(self, *, session_registry_controller: SessionRegistry, operation_policy_controller: OperationPolicy, staging_session_registry_controller: StagingSessionRegistry, window_activation_controller: WindowActivationFilter,
|
|
78
|
+
reference_read_controller: ReferenceReadController, refresh_controller: RefreshController, history_read_controller: HistoryReadController, setting_controller: SettingController, clone_controller: CloneController,
|
|
79
|
+
preference: PreferenceStore, repository: GitRepository, authentication: AuthenticationService, clone: CloneService, diagnostic_log: DiagnosticLog | None,
|
|
80
|
+
wire_repository_window: Callable[[RequestIdentity, RepositorySession, RepositoryWindow, RepositoryContext, FileHistoryAnchor | None], None],
|
|
81
|
+
is_folder_present: Callable[[str], bool] = folder_is_reachable, parent: QObject | None = None) -> None:
|
|
82
|
+
"""Receive the services, the lower controllers and the wiring function a new repository window is connected with; the wiring builds it after the clone controller, and the rebase learns from its signal which opened window to observe.
|
|
83
|
+
|
|
84
|
+
``is_folder_present`` tells whether the folder of a welcome opening exists, raising when it cannot tell. It may
|
|
85
|
+
block for long on a sleeping disk or a dead network share, so it is only ever asked on a thread of its own;
|
|
86
|
+
tests answer without a disk.
|
|
87
|
+
"""
|
|
88
|
+
super().__init__(parent)
|
|
89
|
+
self.session_registry_controller = session_registry_controller
|
|
90
|
+
self.operation_policy_controller = operation_policy_controller
|
|
91
|
+
self.staging_session_registry_controller = staging_session_registry_controller
|
|
92
|
+
self.window_activation_controller = window_activation_controller
|
|
93
|
+
self.reference_read_controller = reference_read_controller
|
|
94
|
+
self.refresh_controller = refresh_controller
|
|
95
|
+
self.history_read_controller = history_read_controller
|
|
96
|
+
self.setting_controller = setting_controller
|
|
97
|
+
self.clone_controller = clone_controller
|
|
98
|
+
self.preference = preference
|
|
99
|
+
self.repository = repository
|
|
100
|
+
self.authentication = authentication
|
|
101
|
+
self.clone = clone
|
|
102
|
+
self.diagnostic_log = diagnostic_log
|
|
103
|
+
self.wire_repository_window = wire_repository_window
|
|
104
|
+
self.is_folder_present = is_folder_present
|
|
105
|
+
self.folder_answered.connect(self._folder_answered, Qt.ConnectionType.QueuedConnection)
|
|
106
|
+
|
|
107
|
+
def open(self, session_id: str, repository_dir: str) -> None:
|
|
108
|
+
"""Assign a never-reused generation and reject repeated active opening intents."""
|
|
109
|
+
identity = self._begin_opening(session_id, repository_dir)
|
|
110
|
+
if identity is None:
|
|
111
|
+
return
|
|
112
|
+
if self._wait_for_startup_check(session_id, identity, repository_dir, checks_folder=False):
|
|
113
|
+
return
|
|
114
|
+
self.repository.open_repository(repository_dir, identity)
|
|
115
|
+
|
|
116
|
+
def open_from_welcome(self, session_id: str, repository_dir: str) -> None:
|
|
117
|
+
"""Open the folder the welcome window asked for, once a question off the window's thread found that it exists.
|
|
118
|
+
|
|
119
|
+
A folder that does not exist is never handed to Git: the welcome window says "Invalid path" instead of Git's
|
|
120
|
+
failure to start in it. The question may block for long on a sleeping disk or a dead network share, so it is
|
|
121
|
+
never asked on the window's thread; meanwhile the window shows "Opening…" and Cancel reading, as during a read.
|
|
122
|
+
"""
|
|
123
|
+
identity = self._begin_opening(session_id, repository_dir)
|
|
124
|
+
if identity is None:
|
|
125
|
+
return
|
|
126
|
+
if self._wait_for_startup_check(session_id, identity, repository_dir, checks_folder=True):
|
|
127
|
+
return
|
|
128
|
+
self._ask_folder(identity, repository_dir)
|
|
129
|
+
|
|
130
|
+
def resume_waiting_opening(self, waiting: WaitingOpening) -> None:
|
|
131
|
+
"""Go on with an opening the startup check of Git released: ask about its folder first when it asks one, else read Git now.
|
|
132
|
+
|
|
133
|
+
The order stays the same whatever answers first: the startup check of Git, then the folder question, then Git.
|
|
134
|
+
"""
|
|
135
|
+
if waiting.checks_folder:
|
|
136
|
+
self._ask_folder(waiting.identity, waiting.repository_dir)
|
|
137
|
+
return
|
|
138
|
+
self.repository.open_repository(waiting.repository_dir, waiting.identity)
|
|
139
|
+
|
|
140
|
+
def _begin_opening(self, session_id: str, repository_dir: str) -> RequestIdentity | None:
|
|
141
|
+
"""Give an opening its never-reused identity and show the chooser busy, or return None for an opening refused here.
|
|
142
|
+
|
|
143
|
+
An opening is refused while another one of the session runs, once its chooser is gone, and during a clone for any
|
|
144
|
+
folder but the clone's destination.
|
|
145
|
+
"""
|
|
146
|
+
if session_id in self.session_registry_controller.identity_by_session or session_id not in self.session_registry_controller.welcome_by_session:
|
|
147
|
+
return None
|
|
148
|
+
clone_dialog = self.session_registry_controller.clone_dialog_by_session.get(session_id)
|
|
149
|
+
if clone_dialog is not None and clone_dialog.operation_id is not None:
|
|
150
|
+
run = self.clone.run_by_id[clone_dialog.operation_id]
|
|
151
|
+
if run.open_status != "running" or run.destination is None or canonical_path(repository_dir) != canonical_path(run.destination.destination_dir):
|
|
152
|
+
return None
|
|
153
|
+
generation = self.session_registry_controller.generation_by_session.get(session_id, 0) + 1
|
|
154
|
+
self.session_registry_controller.generation_by_session[session_id] = generation
|
|
155
|
+
identity = RequestIdentity(session_id, generation)
|
|
156
|
+
self.session_registry_controller.identity_by_session[session_id] = identity
|
|
157
|
+
self.session_registry_controller.opening_dir_by_session[session_id] = repository_dir
|
|
158
|
+
self.session_registry_controller.welcome_by_session[session_id].set_loading(True)
|
|
159
|
+
return identity
|
|
160
|
+
|
|
161
|
+
def _wait_for_startup_check(self, session_id: str, identity: RequestIdentity, repository_dir: str, *, checks_folder: bool) -> bool:
|
|
162
|
+
"""Leave the opening to the startup check of Git while that check runs, returning whether it now waits for it.
|
|
163
|
+
|
|
164
|
+
``checks_folder`` makes the released opening ask about its folder before Git reads it.
|
|
165
|
+
"""
|
|
166
|
+
if self.setting_controller.startup_identity is None:
|
|
167
|
+
return False
|
|
168
|
+
self.setting_controller.waiting_opening_by_session[session_id] = WaitingOpening(identity, repository_dir, self.repository.git_filename, checks_folder)
|
|
169
|
+
return True
|
|
170
|
+
|
|
171
|
+
def _ask_folder(self, identity: RequestIdentity, repository_dir: str) -> None:
|
|
172
|
+
"""Ask on a thread of its own whether the opening's folder exists; the answer comes back through ``folder_answered``."""
|
|
173
|
+
# The thread holds the controller only weakly: a question blocked on a dead share keeps nothing alive.
|
|
174
|
+
hand_over = partial(weak_signal_emitter(self, lambda controller: controller.folder_answered), identity, repository_dir)
|
|
175
|
+
thread = threading.Thread(target=answer_folder_question, args=(self.is_folder_present, repository_dir, hand_over), name=OPENING_FOLDER_THREAD_NAME, daemon=True)
|
|
176
|
+
thread.start()
|
|
177
|
+
|
|
178
|
+
def _folder_answered(self, identity: RequestIdentity, repository_dir: str, present: bool | None) -> None:
|
|
179
|
+
"""Go on with an opening whose folder question answered, unless Cancel reading, a closed chooser or a newer opening replaced it.
|
|
180
|
+
|
|
181
|
+
Only a definite "absent" refuses the folder; a question without a definite answer leaves the answer to Git.
|
|
182
|
+
"""
|
|
183
|
+
if self.session_registry_controller.identity_by_session.get(identity.session_id) != identity:
|
|
184
|
+
return
|
|
185
|
+
if identity.session_id not in self.session_registry_controller.welcome_by_session:
|
|
186
|
+
return
|
|
187
|
+
if present is False:
|
|
188
|
+
self._refuse_missing_folder(identity, repository_dir)
|
|
189
|
+
return
|
|
190
|
+
# The folder question is already answered: an opening released later by a startup check reads Git at once.
|
|
191
|
+
if self._wait_for_startup_check(identity.session_id, identity, repository_dir, checks_folder=False):
|
|
192
|
+
return
|
|
193
|
+
self.repository.open_repository(repository_dir, identity)
|
|
194
|
+
|
|
195
|
+
def _refuse_missing_folder(self, identity: RequestIdentity, repository_dir: str) -> None:
|
|
196
|
+
"""End an opening whose folder does not exist, give the chooser back to the user, and explain it in a message over it.
|
|
197
|
+
|
|
198
|
+
A recent of that folder is marked Unavailable, as after a failed opening.
|
|
199
|
+
"""
|
|
200
|
+
self.session_registry_controller.identity_by_session.pop(identity.session_id, None)
|
|
201
|
+
welcome = self.session_registry_controller.welcome_by_session[identity.session_id]
|
|
202
|
+
welcome.set_loading(False)
|
|
203
|
+
welcome.mark_unavailable(repository_dir)
|
|
204
|
+
if self.diagnostic_log is not None:
|
|
205
|
+
self.diagnostic_log.write_line(MISSING_FOLDER_LOG_TEXT.format(path=repository_dir))
|
|
206
|
+
box = welcome.invalid_path_message(repository_dir)
|
|
207
|
+
self.session_registry_controller.dialogs.append(box)
|
|
208
|
+
box.finished.connect(lambda: self.session_registry_controller.dialogs.remove(box) if box in self.session_registry_controller.dialogs else None)
|
|
209
|
+
box.finished.connect(box.deleteLater)
|
|
210
|
+
open_window_modal(box)
|
|
211
|
+
|
|
212
|
+
def opened(self, identity: RequestIdentity, context: RepositoryContext, commit: Commit | None, file_history_anchor: FileHistoryAnchor | None = None,
|
|
213
|
+
requesting_window: QWidget | None = None) -> None:
|
|
214
|
+
"""Publish a successful read only to its still-current requesting window.
|
|
215
|
+
|
|
216
|
+
``requesting_window`` is the window a file history was asked from; a new window opens centred on it, or on the
|
|
217
|
+
Open or Clone repository window of its session. A read a navigation owns is left to the navigation's own slot,
|
|
218
|
+
connected before this one. A new window is connected to the controllers before anything selects, registers,
|
|
219
|
+
reads or shows it.
|
|
220
|
+
"""
|
|
221
|
+
if identity.session_id in self.session_registry_controller.navigation_owner_by_id:
|
|
222
|
+
return
|
|
223
|
+
if self.session_registry_controller.identity_by_session.get(identity.session_id) != identity:
|
|
224
|
+
return
|
|
225
|
+
clone_dialog = self.session_registry_controller.clone_dialog_by_session.get(identity.session_id)
|
|
226
|
+
if clone_dialog is not None and clone_dialog.operation_id is not None:
|
|
227
|
+
run = self.clone.run_by_id[clone_dialog.operation_id]
|
|
228
|
+
try:
|
|
229
|
+
if run.cancelled or not self.operation_policy_controller.clone_is_current(run.intent):
|
|
230
|
+
return
|
|
231
|
+
run.destination.verify()
|
|
232
|
+
if canonical_path(context.repository_dir) != canonical_path(run.destination.destination_dir):
|
|
233
|
+
raise ValueError("The opened repository does not match the clone destination.")
|
|
234
|
+
if run.intent.selection is not None and not run.selection_saved:
|
|
235
|
+
self.authentication.save_selection(run.intent.selection.origin, run.intent.selection, context)
|
|
236
|
+
run.selection_saved = True
|
|
237
|
+
except (OSError, ValueError, TypeError) as error:
|
|
238
|
+
self.history_read_controller.failed(identity, RepositoryFailure("Open cloned repository", context.repository_dir, self.repository.git_filename, [], str(error)))
|
|
239
|
+
return
|
|
240
|
+
existing_window = self.session_registry_controller.window_by_session.get(identity.session_id)
|
|
241
|
+
if existing_window is not None:
|
|
242
|
+
existing_window.show_commit(commit)
|
|
243
|
+
self.refresh_controller.recovery_request_set.discard(identity)
|
|
244
|
+
return
|
|
245
|
+
if file_history_anchor is None:
|
|
246
|
+
showing_window = self.window_showing_folder(context.repository_dir, other_than=identity.session_id)
|
|
247
|
+
if showing_window is not None:
|
|
248
|
+
# Opening a folder already shown is still a successful opening: it goes first in the recent list.
|
|
249
|
+
preference_diagnostic = self.remember_repository(context.repository_dir)
|
|
250
|
+
self.bring_forward(showing_window)
|
|
251
|
+
welcome = self.session_registry_controller.welcome_by_session.get(identity.session_id)
|
|
252
|
+
if welcome is not None:
|
|
253
|
+
# The chooser closes as after any opening; its close ends this unused opening.
|
|
254
|
+
welcome.close()
|
|
255
|
+
if preference_diagnostic is not None:
|
|
256
|
+
self.show_preference_failure(preference_diagnostic, showing_window)
|
|
257
|
+
return
|
|
258
|
+
preference_diagnostic = None
|
|
259
|
+
if file_history_anchor is None:
|
|
260
|
+
preference_diagnostic = self.remember_repository(context.repository_dir)
|
|
261
|
+
session = RepositorySession(identity.session_id, context, self.repository, file_history_anchor)
|
|
262
|
+
session.generation = identity.generation
|
|
263
|
+
self.session_registry_controller.session_by_id[identity.session_id] = session
|
|
264
|
+
window = RepositoryWindow(context, commit, mode=session.mode, placement_anchor=self._new_window_anchor(identity.session_id, requesting_window))
|
|
265
|
+
if file_history_anchor is not None:
|
|
266
|
+
_directory, _separator, basename = os.fsdecode(file_history_anchor.path).rpartition("/")
|
|
267
|
+
window.setWindowTitle(f"{basename} — File history — {context.repository_dir} — GitPane")
|
|
268
|
+
self.wire_repository_window(identity, session, window, context, file_history_anchor)
|
|
269
|
+
if commit is not None:
|
|
270
|
+
session.select_commit(commit.oid)
|
|
271
|
+
self.session_registry_controller.window_by_session[identity.session_id] = window
|
|
272
|
+
# Walking from a parent repository into a child inside an already active window delivers no activation event.
|
|
273
|
+
self.window_activation_controller.schedule_activation_update()
|
|
274
|
+
if WindowCapability.MANAGE in session.capabilities and not context.is_bare:
|
|
275
|
+
# The window starts with an unknown count, and this worktree may already have been read for another window.
|
|
276
|
+
self.staging_session_registry_controller.staging_session(context).refresh(announce=True)
|
|
277
|
+
window.show()
|
|
278
|
+
self.preference.refresh()
|
|
279
|
+
window.show_recent_repositories(self.preference.value_by_key["recent_dirs"])
|
|
280
|
+
window.apply_reading_preference(self.preference.value_by_key)
|
|
281
|
+
window.apply_fetch_preference(self.preference.value_by_key)
|
|
282
|
+
welcome = self.session_registry_controller.welcome_by_session.pop(identity.session_id, None)
|
|
283
|
+
clone_dialog = self.session_registry_controller.clone_dialog_by_session.pop(identity.session_id, None)
|
|
284
|
+
if clone_dialog is not None and clone_dialog.operation_id is not None:
|
|
285
|
+
self.clone_controller.keep_clone_console(clone_dialog.operation_id, window, show=False)
|
|
286
|
+
self.clone.open_result(clone_dialog.operation_id, True)
|
|
287
|
+
clone_dialog.accept()
|
|
288
|
+
# The replaced windows are handed to Qt for deletion while these local variables still hold them, so their end
|
|
289
|
+
# never depends on where their last Python reference goes away: destroyed at the wrong moment, in the middle of
|
|
290
|
+
# Qt switching the active window to the repository window, they made GitPane crash.
|
|
291
|
+
if clone_dialog is not None:
|
|
292
|
+
clone_dialog.deleteLater()
|
|
293
|
+
if welcome is not None:
|
|
294
|
+
welcome.close()
|
|
295
|
+
welcome.deleteLater()
|
|
296
|
+
window.begin_history()
|
|
297
|
+
self.reference_read_controller.read_history_with_references(identity.session_id, identity)
|
|
298
|
+
self.refresh_controller.read_submodules(identity.session_id)
|
|
299
|
+
self.pause_observation_requested.emit(identity.session_id)
|
|
300
|
+
if preference_diagnostic is not None:
|
|
301
|
+
self.show_preference_failure(preference_diagnostic, window)
|
|
302
|
+
|
|
303
|
+
def window_showing_folder(self, repository_dir: str, *, other_than: str) -> RepositoryWindow | None:
|
|
304
|
+
"""Return the repository window, other than the ``other_than`` session's, that already shows this working folder.
|
|
305
|
+
|
|
306
|
+
GitPane keeps one window per working folder, so opening the folder again brings that window forward instead.
|
|
307
|
+
A file history window is not a repository window and is left out; another worktree of the same repository is
|
|
308
|
+
another folder, with its own window.
|
|
309
|
+
"""
|
|
310
|
+
# Left as is on purpose: this check runs when an opening or a navigation is asked for, not again when it
|
|
311
|
+
# arrives. So two windows can still show one folder in a rare race: a second request for a folder while a first
|
|
312
|
+
# navigation to that same folder is still reading it, or a switch to a sub-folder of a repository, compared as
|
|
313
|
+
# given with the repository roots the windows show. Nothing is at risk then: every write reserves its
|
|
314
|
+
# repository first, so the two windows can never write to it at the same time.
|
|
315
|
+
target_dir = canonical_path(repository_dir)
|
|
316
|
+
for session_id, session in self.session_registry_controller.session_by_id.items():
|
|
317
|
+
if session_id == other_than or session.is_closed or session.file_history_anchor is not None:
|
|
318
|
+
continue
|
|
319
|
+
window = self.session_registry_controller.window_by_session.get(session_id)
|
|
320
|
+
if window is None or not isValid(window):
|
|
321
|
+
continue
|
|
322
|
+
if canonical_path(session.repository_context.repository_dir) == target_dir:
|
|
323
|
+
return window
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
def bring_forward(self, window: QWidget) -> None:
|
|
327
|
+
"""Show a window again in front of the others, restored if it was minimized."""
|
|
328
|
+
if window.isMinimized():
|
|
329
|
+
window.showNormal()
|
|
330
|
+
window.raise_()
|
|
331
|
+
window.activateWindow()
|
|
332
|
+
|
|
333
|
+
def _new_window_anchor(self, session_id: str, requesting_window: QWidget | None) -> QWidget | None:
|
|
334
|
+
"""Return the window a new repository or file history window opens centred on.
|
|
335
|
+
|
|
336
|
+
The Clone repository window comes first while it is on screen, since the user is looking at it; then the Open
|
|
337
|
+
repository window of the session; a file history session has neither and uses the window that asked for it.
|
|
338
|
+
"""
|
|
339
|
+
clone_dialog = self.session_registry_controller.clone_dialog_by_session.get(session_id)
|
|
340
|
+
if is_visible_window(clone_dialog):
|
|
341
|
+
return clone_dialog
|
|
342
|
+
welcome = self.session_registry_controller.welcome_by_session.get(session_id)
|
|
343
|
+
if is_visible_window(welcome):
|
|
344
|
+
return welcome
|
|
345
|
+
if is_visible_window(requesting_window):
|
|
346
|
+
return requesting_window
|
|
347
|
+
return None
|
|
348
|
+
|
|
349
|
+
def remember_repository(self, repository_dir: str) -> PreparedDiagnostic | None:
|
|
350
|
+
"""Put a successfully opened repository first in the history, returning the diagnostic of a failed save.
|
|
351
|
+
|
|
352
|
+
A failed save never undoes the opening: the caller shows the diagnostic over the repository that is open.
|
|
353
|
+
"""
|
|
354
|
+
try:
|
|
355
|
+
self.preference.remember(repository_dir)
|
|
356
|
+
except OSError as error:
|
|
357
|
+
return Diagnostic("Save preferences", repository_dir, self.repository.git_filename, [], str(error)).prepare()
|
|
358
|
+
return None
|
|
359
|
+
|
|
360
|
+
def show_preference_failure(self, diagnostic: PreparedDiagnostic, window: RepositoryWindow) -> None:
|
|
361
|
+
"""Log a failed preference save and explain it in a dialog over the window that stays open."""
|
|
362
|
+
if self.diagnostic_log is not None:
|
|
363
|
+
self.diagnostic_log.write(diagnostic)
|
|
364
|
+
dialog = ErrorDialog(diagnostic, window, self.diagnostic_log.log_filename if self.diagnostic_log is not None else None)
|
|
365
|
+
self.session_registry_controller.dialogs.append(dialog)
|
|
366
|
+
dialog.finished.connect(lambda: self.session_registry_controller.dialogs.remove(dialog) if dialog in self.session_registry_controller.dialogs else None)
|
|
367
|
+
dialog.finished.connect(dialog.deleteLater)
|
|
368
|
+
open_window_modal(dialog)
|
|
369
|
+
|
|
370
|
+
def cancel_open(self, session_id: str) -> None:
|
|
371
|
+
"""Abandon only the current chooser's read while keeping its form available."""
|
|
372
|
+
self.setting_controller.waiting_opening_by_session.pop(session_id, None)
|
|
373
|
+
self.session_registry_controller.identity_by_session.pop(session_id, None)
|
|
374
|
+
self.repository.cancel(session_id)
|
|
375
|
+
self.session_registry_controller.welcome_by_session[session_id].set_loading(False)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Decide whether a window may still send a captured write to Git."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import replace
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from gitpane.history.file_history import WindowCapability
|
|
9
|
+
from gitpane.operation.repository_operation import NO_LONGER_AUTHORIZED_TEXT, REPOSITORY_CHANGED_SINCE_OPENED_TEXT, REPOSITORY_CHANGED_WHILE_PREPARING_TEXT, rebase_action_diagnostic
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from gitpane.controller.session_registry import SessionRegistry
|
|
13
|
+
from gitpane.history.repository import GitRepository
|
|
14
|
+
from gitpane.operation.repository_operation import OperationIntent, RepositoryOperationService
|
|
15
|
+
from gitpane.sequencer.rebase import RebaseService
|
|
16
|
+
from gitpane.transport.clone import CloneIntent
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OperationPolicy:
|
|
20
|
+
"""The authority a window keeps over the writes it captured."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, *, session_registry_controller: SessionRegistry, repository: GitRepository, repository_operation: RepositoryOperationService, rebase: RebaseService) -> None:
|
|
23
|
+
"""Receive the sessions and the services the decision reads; the wiring builds it right after the rebase service, which it reads."""
|
|
24
|
+
self.session_registry_controller = session_registry_controller
|
|
25
|
+
self.repository = repository
|
|
26
|
+
self.repository_operation = repository_operation
|
|
27
|
+
self.rebase = rebase
|
|
28
|
+
|
|
29
|
+
def clone_is_current(self, intent: CloneIntent) -> bool:
|
|
30
|
+
"""Prevent a closed or superseded welcome from publishing clone results."""
|
|
31
|
+
return (not self.session_registry_controller.shutdown_requested and intent.session_id in self.session_registry_controller.welcome_by_session and intent.session_id in self.session_registry_controller.clone_dialog_by_session
|
|
32
|
+
and self.session_registry_controller.clone_generation_by_session.get(intent.session_id) == intent.generation)
|
|
33
|
+
|
|
34
|
+
def allows(self, session_id: str, capability: WindowCapability) -> bool:
|
|
35
|
+
"""Enforce the same action policy at service boundaries and widget entry points."""
|
|
36
|
+
session = self.session_registry_controller.session_by_id.get(session_id)
|
|
37
|
+
return session is not None and not session.is_closed and capability in session.capabilities
|
|
38
|
+
|
|
39
|
+
def operation_allowed(self, intent: OperationIntent) -> bool:
|
|
40
|
+
"""Validate captured window authority and repository identity at submission time."""
|
|
41
|
+
return not self.operation_refusal(intent)
|
|
42
|
+
|
|
43
|
+
def operation_refusal(self, intent: OperationIntent) -> str:
|
|
44
|
+
"""Say why a captured write may no longer be sent, or "" when it may.
|
|
45
|
+
|
|
46
|
+
A window refreshed after its form was opened (an outside change the automatic refresh saw) has a newer
|
|
47
|
+
generation than the form's capture: the form cannot write any more and has to be reopened, which is what the
|
|
48
|
+
refusal says, rather than blaming another operation. A write captured at the click, with no form keeping it
|
|
49
|
+
(a stash, a rebase start, the push of a Commit & push), only has to be clicked again.
|
|
50
|
+
"""
|
|
51
|
+
reason = self.repository_operation.policy_diagnostic(intent)
|
|
52
|
+
if reason:
|
|
53
|
+
return reason
|
|
54
|
+
state = self.rebase.state_by_key.get(intent.key)
|
|
55
|
+
if state is not None:
|
|
56
|
+
reason = rebase_action_diagnostic(intent.action, state)
|
|
57
|
+
if reason:
|
|
58
|
+
return reason
|
|
59
|
+
if intent.session_id is None:
|
|
60
|
+
return ""
|
|
61
|
+
if not self.allows(intent.session_id, intent.capability):
|
|
62
|
+
return NO_LONGER_AUTHORIZED_TEXT
|
|
63
|
+
session = self.session_registry_controller.session_by_id[intent.session_id]
|
|
64
|
+
if session.repository_context != intent.context or self.session_registry_controller.generation_by_session.get(intent.session_id) != intent.generation:
|
|
65
|
+
return REPOSITORY_CHANGED_SINCE_OPENED_TEXT if intent.from_form else REPOSITORY_CHANGED_WHILE_PREPARING_TEXT
|
|
66
|
+
return ""
|
|
67
|
+
|
|
68
|
+
def commit_push_operation(self, captured: OperationIntent) -> OperationIntent | None:
|
|
69
|
+
"""Retain the original repository and permission through the normal history refresh."""
|
|
70
|
+
session_id = captured.session_id
|
|
71
|
+
if captured.capability != WindowCapability.MANAGE or not self.allows(session_id, WindowCapability.MANAGE):
|
|
72
|
+
return None
|
|
73
|
+
session = self.session_registry_controller.session_by_id[session_id]
|
|
74
|
+
if session.repository_context != captured.context or self.repository.git_filename != captured.git_filename:
|
|
75
|
+
return None
|
|
76
|
+
return replace(captured, generation=self.session_registry_controller.generation_by_session[session_id])
|