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,592 @@
|
|
|
1
|
+
"""Edit the shared settings of GitPane, starting with the Git it runs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import uuid
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from PySide6.QtCore import QObject, Signal
|
|
12
|
+
from shiboken6 import isValid
|
|
13
|
+
|
|
14
|
+
from gitpane.account.credential_manager import ignore_line
|
|
15
|
+
from gitpane.account.ssh import effective_source
|
|
16
|
+
from gitpane.config import SharedText
|
|
17
|
+
from gitpane.history.file_history import WindowCapability
|
|
18
|
+
from gitpane.history.repository_value import RepositoryFailure, RequestIdentity
|
|
19
|
+
from gitpane.operation.diagnostic import Diagnostic
|
|
20
|
+
from gitpane.system.desktop_icon_fact import read_session_desktop_icon_facts
|
|
21
|
+
from gitpane.system.desktop_shortcut import create_installed_desktop_shortcut
|
|
22
|
+
from gitpane.system.linux_distribution import DistributionFamily, DistributionRelease
|
|
23
|
+
from gitpane.tool.difftool import DifftoolRecipe
|
|
24
|
+
from gitpane.tool.git_executable import detect_git_candidates
|
|
25
|
+
from gitpane.tool.kdiff3 import detect_kdiff3_filename
|
|
26
|
+
from gitpane.tool.kdiff3_install import kdiff3_install_line
|
|
27
|
+
from gitpane.tool.mergetool_recipe import MERGETOOL_COMMAND_PREFERENCE_KEY, MERGETOOL_FILENAME_PREFERENCE_KEY, MergetoolRecipe
|
|
28
|
+
from gitpane.transport.push_tracking import push_tracking_choice, save_push_tracking_choice
|
|
29
|
+
from gitpane.ui.console.error_dialog import ErrorDialog
|
|
30
|
+
from gitpane.ui.setting.account_page import ACCOUNTS_PAGE_NAME, AccountPage
|
|
31
|
+
from gitpane.ui.setting.git_setting_widget import GitSettingWidget
|
|
32
|
+
from gitpane.ui.setting.setting_dialog import GIT_AVAILABLE_TEMPLATE, SSH_PAGE_NAME, SettingDialog
|
|
33
|
+
from gitpane.ui.setting.ssh_widget import SshWidget
|
|
34
|
+
from gitpane.ui.setting.utility_page import UtilityPage
|
|
35
|
+
from gitpane.ui.window_control.dialog_modality import open_window_modal
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from collections.abc import Callable
|
|
39
|
+
|
|
40
|
+
from gitpane.account.ssh import SshService, SshVerification
|
|
41
|
+
from gitpane.controller.history_read_controller import HistoryReadController
|
|
42
|
+
from gitpane.controller.operation_policy import OperationPolicy
|
|
43
|
+
from gitpane.controller.session_registry import SessionRegistry
|
|
44
|
+
from gitpane.controller.staging_session_registry import StagingSessionRegistry
|
|
45
|
+
from gitpane.controller.window_activation import WindowActivationFilter
|
|
46
|
+
from gitpane.git_config.git_setting import GitSettingService
|
|
47
|
+
from gitpane.history.repository import GitRepository
|
|
48
|
+
from gitpane.history.repository_value import RepositoryContext
|
|
49
|
+
from gitpane.operation.diagnostic import DiagnosticLog
|
|
50
|
+
from gitpane.preference.preference_store import PreferenceStore
|
|
51
|
+
from gitpane.sequencer.mergetool import MergetoolService
|
|
52
|
+
from gitpane.system.desktop_icon_support import DesktopIconFacts
|
|
53
|
+
from gitpane.system.desktop_shortcut import ShortcutReport
|
|
54
|
+
from gitpane.tool.git_executable import GitExecutableValidator
|
|
55
|
+
from gitpane.tool.system_editor_default import SystemEditorDefault
|
|
56
|
+
from gitpane.tool.terminal_recipe import TerminalRegistry
|
|
57
|
+
from gitpane.transport.push_tracking import PushTrackingChoice
|
|
58
|
+
|
|
59
|
+
SETTING_FAILED_DRAFTS_TEXT = "Settings could not all be saved. Your failed drafts are retained."
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class WaitingOpening:
|
|
64
|
+
"""One opening asked before the startup check of Git answered, with the Git it was asked with.
|
|
65
|
+
|
|
66
|
+
``checks_folder`` says that the opening, once released, asks whether its folder exists before Git reads it; only the
|
|
67
|
+
openings of the welcome window do, a clone's destination being known to exist.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
identity: RequestIdentity
|
|
71
|
+
repository_dir: str
|
|
72
|
+
git_filename: str
|
|
73
|
+
checks_folder: bool = False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class SettingController(QObject):
|
|
77
|
+
"""The Settings window with its drafts, and the checks of the Git GitPane runs: at startup, after a change made elsewhere, and from the window."""
|
|
78
|
+
|
|
79
|
+
# Settings window just built, its accounts page, the session that asked (or None), the repository it opened for (or None):
|
|
80
|
+
# the accounts page is filled and connected while the window is being built, before its own buttons are connected.
|
|
81
|
+
setting_opened = Signal(object, object, object, object)
|
|
82
|
+
# The startup check found no Git, or none that works: the welcome window says so and opens Settings, where a failure
|
|
83
|
+
# of the last Git tried is shown next.
|
|
84
|
+
git_missing = Signal()
|
|
85
|
+
# An opening asked during the startup check of Git, released with the Git that check accepted: the opening
|
|
86
|
+
# controller goes on with it.
|
|
87
|
+
waiting_opening_released = Signal(object)
|
|
88
|
+
|
|
89
|
+
def __init__(self, *, session_registry_controller: SessionRegistry, operation_policy_controller: OperationPolicy, staging_session_registry_controller: StagingSessionRegistry, window_activation_controller: WindowActivationFilter,
|
|
90
|
+
history_read_controller: HistoryReadController, preference: PreferenceStore, repository: GitRepository, git_setting_service: GitSettingService, ssh: SshService, mergetool: MergetoolService,
|
|
91
|
+
diagnostic_log: DiagnosticLog | None, terminal_registry: TerminalRegistry, validator: GitExecutableValidator, system_editor_default: SystemEditorDefault,
|
|
92
|
+
desktop_shortcut: Callable[[], ShortcutReport] = create_installed_desktop_shortcut,
|
|
93
|
+
desktop_icon_facts: Callable[[], DesktopIconFacts] = read_session_desktop_icon_facts, distribution_family: DistributionFamily = DistributionFamily.UNKNOWN,
|
|
94
|
+
distribution_release: DistributionRelease = DistributionRelease.OTHER, parent: QObject | None = None) -> None:
|
|
95
|
+
"""Receive the services and the lower controllers Settings works with; the wiring builds it after the history read controller, and the accounts, the clone forms and the welcome learn from its signals what it opened or found.
|
|
96
|
+
|
|
97
|
+
``desktop_shortcut`` writes the desktop shortcut of the running installation when the Utilities page asks for
|
|
98
|
+
it; tests give their own, which writes only in their temporary folder. ``desktop_icon_facts`` reads from the
|
|
99
|
+
session whether the desktop can show the shortcut's icon, each time the Utilities page is shown, on the page's
|
|
100
|
+
worker thread; tests give their own, which reads nothing from the machine. ``diagnostic_log``, when a ``--log``
|
|
101
|
+
journal is kept, also receives the errors nobody expects of these two workers. ``system_editor_default`` keeps the
|
|
102
|
+
text editor the system names, which the editor detection follows while no editor choice is saved.
|
|
103
|
+
``distribution_family`` and ``distribution_release`` name the Linux distribution of the session, read once by the
|
|
104
|
+
wiring, so the help and the refusals of a missing diff or merge tool say how to install KDiff3 there.
|
|
105
|
+
"""
|
|
106
|
+
super().__init__(parent)
|
|
107
|
+
self.session_registry_controller = session_registry_controller
|
|
108
|
+
self.operation_policy_controller = operation_policy_controller
|
|
109
|
+
self.staging_session_registry_controller = staging_session_registry_controller
|
|
110
|
+
self.window_activation_controller = window_activation_controller
|
|
111
|
+
self.history_read_controller = history_read_controller
|
|
112
|
+
self.preference = preference
|
|
113
|
+
self.repository = repository
|
|
114
|
+
self.git_setting_service = git_setting_service
|
|
115
|
+
self.ssh = ssh
|
|
116
|
+
self.mergetool = mergetool
|
|
117
|
+
self.diagnostic_log = diagnostic_log
|
|
118
|
+
self.terminal_registry = terminal_registry
|
|
119
|
+
self.validator = validator
|
|
120
|
+
self.system_editor_default = system_editor_default
|
|
121
|
+
self.desktop_shortcut = desktop_shortcut
|
|
122
|
+
self.desktop_icon_facts = desktop_icon_facts
|
|
123
|
+
self.distribution_family = distribution_family
|
|
124
|
+
self.distribution_release = distribution_release
|
|
125
|
+
self.setting_session_id = uuid.uuid4().hex
|
|
126
|
+
self.setting_generation = 0
|
|
127
|
+
self.setting_identity: RequestIdentity | None = None
|
|
128
|
+
self.save_requested = False
|
|
129
|
+
self.startup_identity: RequestIdentity | None = None
|
|
130
|
+
self.launch_validation_identity: RequestIdentity | None = None
|
|
131
|
+
# The Git the open Settings draft has just validated, and the one a save in progress is writing with that proof.
|
|
132
|
+
self.setting_validated_git_filename = ""
|
|
133
|
+
self.saving_validated_git_filename = ""
|
|
134
|
+
# Set while the check in progress was asked by Verify Git, the only check whose success is announced with a box.
|
|
135
|
+
self.setting_verify_clicked = False
|
|
136
|
+
# Warning about the tools folder of the Git the current Settings check accepts; cleared with that check.
|
|
137
|
+
self.setting_tools_folder_warning = ""
|
|
138
|
+
self.candidate_filenames: list[str] = []
|
|
139
|
+
# Openings asked while the startup check of Git is still running, with the Git each one was asked with.
|
|
140
|
+
self.waiting_opening_by_session: dict[str, WaitingOpening] = {}
|
|
141
|
+
|
|
142
|
+
def _validate_selected_git(self, git_filename: str) -> None:
|
|
143
|
+
"""Check a Git accepted elsewhere, in a session of its own, so a slow answer cannot revive an older choice."""
|
|
144
|
+
if self.launch_validation_identity is not None:
|
|
145
|
+
self.validator.cancel(self.launch_validation_identity.session_id)
|
|
146
|
+
self.launch_validation_identity = RequestIdentity(uuid.uuid4().hex)
|
|
147
|
+
self.validator.validate(git_filename, self.launch_validation_identity)
|
|
148
|
+
|
|
149
|
+
def show_setting(self, session_id: str | None = None, *, section: str | None = None) -> SettingDialog | None:
|
|
150
|
+
"""Open shared settings and reveal a requested section without discarding an existing draft."""
|
|
151
|
+
if session_id is not None and not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
|
|
152
|
+
return None
|
|
153
|
+
if self.session_registry_controller.setting_dialog is not None:
|
|
154
|
+
self.session_registry_controller.setting_dialog.showNormal()
|
|
155
|
+
self.session_registry_controller.setting_dialog.raise_()
|
|
156
|
+
self.session_registry_controller.setting_dialog.activateWindow()
|
|
157
|
+
if section == "mergetool":
|
|
158
|
+
self.session_registry_controller.setting_dialog.show_mergetool_setting()
|
|
159
|
+
if section == "editor":
|
|
160
|
+
self.session_registry_controller.setting_dialog.show_editor_setting()
|
|
161
|
+
if section == "accounts":
|
|
162
|
+
self.session_registry_controller.setting_dialog.show_page(ACCOUNTS_PAGE_NAME)
|
|
163
|
+
return self.session_registry_controller.setting_dialog
|
|
164
|
+
mergetool_filename = self.preference.initialize_mergetool(save_detected=False)
|
|
165
|
+
merge_warning = self.preference.warning
|
|
166
|
+
difftool_filename = self.preference.initialize_difftool(save=False)
|
|
167
|
+
# The notice asks for a Save only when the merge tool shown would not be used without one. A detected KDiff3 is used
|
|
168
|
+
# as it is by the conflict action, so it gets no notice even though it is not saved yet.
|
|
169
|
+
detected_kdiff3_filename = detect_kdiff3_filename(sys.platform)
|
|
170
|
+
mergetool_suggested = bool(mergetool_filename) and mergetool_filename not in (self.preference.value_by_key[MERGETOOL_FILENAME_PREFERENCE_KEY], detected_kdiff3_filename)
|
|
171
|
+
context = self.session_registry_controller.session_by_id[session_id].repository_context if session_id is not None else None
|
|
172
|
+
editor_recipe = self.preference.initialize_editor(repository_dirs=(context.repository_dir,) if context is not None else (),
|
|
173
|
+
system_editor_id=self.system_editor_default.system_editor_id)
|
|
174
|
+
# Read again for the next opening and for Show in text editor, so a default editor changed on the desktop is followed.
|
|
175
|
+
self.system_editor_default.request_read()
|
|
176
|
+
git_setting_widget = self._git_setting_widget(context)
|
|
177
|
+
account_page = AccountPage(context.repository_dir if context is not None else None)
|
|
178
|
+
# The form gets only the saved client: a detected one shown there would be saved by the next Save.
|
|
179
|
+
ssh_widget = SshWidget(self.ssh.saved_configuration(), *self._effective_ssh())
|
|
180
|
+
utility_page = UtilityPage(self.desktop_shortcut, self.desktop_icon_facts, log_line=self.diagnostic_log.write_line if self.diagnostic_log is not None else ignore_line)
|
|
181
|
+
dialog = SettingDialog(str(self.preference.value_by_key["git_filename"]) or self.repository.git_filename,
|
|
182
|
+
difftool_filename=difftool_filename, difftool_command=str(self.preference.value_by_key["difftool_command"]), git_setting_widget=git_setting_widget,
|
|
183
|
+
account_page=account_page, ssh_widget=ssh_widget, utility_page=utility_page, mergetool_filename=mergetool_filename,
|
|
184
|
+
mergetool_command=str(self.preference.value_by_key[MERGETOOL_COMMAND_PREFERENCE_KEY]), mergetool_suggested=mergetool_suggested,
|
|
185
|
+
editor_recipe=editor_recipe, kdiff3_install_line="" if detected_kdiff3_filename else kdiff3_install_line(sys.platform, self.distribution_family, self.distribution_release))
|
|
186
|
+
# Settings are shared by every window and have no Qt parent; they open over the window that asked for them.
|
|
187
|
+
dialog.placement_anchor = self.session_registry_controller.window_by_session.get(session_id) if session_id is not None else next(iter(self.session_registry_controller.welcome_by_session.values()), None)
|
|
188
|
+
self.session_registry_controller.setting_dialog = dialog
|
|
189
|
+
ssh_widget.verification_requested.connect(self._verify_ssh)
|
|
190
|
+
self.setting_opened.emit(dialog, account_page, session_id, context)
|
|
191
|
+
dialog.validation_requested.connect(self._validate_setting)
|
|
192
|
+
dialog.git_verify_requested.connect(self._verify_setting)
|
|
193
|
+
dialog.detection_requested.connect(self._detect_setting)
|
|
194
|
+
dialog.draft_changed.connect(self._invalidate_setting)
|
|
195
|
+
dialog.finished.connect(self._close_setting)
|
|
196
|
+
dialog.installEventFilter(self.window_activation_controller)
|
|
197
|
+
if self.repository.git_version:
|
|
198
|
+
dialog.status_label.setText(GIT_AVAILABLE_TEMPLATE.format(version=self.repository.git_version))
|
|
199
|
+
if self.preference.warning:
|
|
200
|
+
dialog.status_label.setText(self.preference.warning)
|
|
201
|
+
if merge_warning:
|
|
202
|
+
dialog.status_label.setText(merge_warning)
|
|
203
|
+
# Modal for the whole application on every system: no other GitPane window can come in front of Settings or take
|
|
204
|
+
# a click while it is open, and the windows it opens are modal over it. On macOS AppKit keeps a running modal
|
|
205
|
+
# session above the application's other windows, and puts it back to the normal level while another
|
|
206
|
+
# application is active, so Settings never floats above the browser of a sign-in.
|
|
207
|
+
open_window_modal(dialog, blocks_application=True)
|
|
208
|
+
if section == "mergetool":
|
|
209
|
+
dialog.show_mergetool_setting()
|
|
210
|
+
if section == "editor":
|
|
211
|
+
dialog.show_editor_setting()
|
|
212
|
+
if section == "accounts":
|
|
213
|
+
dialog.show_page(ACCOUNTS_PAGE_NAME)
|
|
214
|
+
return dialog
|
|
215
|
+
|
|
216
|
+
def _git_setting_widget(self, context: RepositoryContext | None) -> GitSettingWidget:
|
|
217
|
+
"""Build the Git page; opened from a repository, it also offers that repository's tracking choice, kept in the preferences file.
|
|
218
|
+
|
|
219
|
+
The choice is keyed by the shared Git folder of the repository Settings was opened from, so linked worktrees share
|
|
220
|
+
it and no other repository's entry is touched.
|
|
221
|
+
"""
|
|
222
|
+
if context is None:
|
|
223
|
+
return GitSettingWidget(self.git_setting_service, None, self.repository.git_filename)
|
|
224
|
+
|
|
225
|
+
def save_push_tracking(choice: PushTrackingChoice) -> None:
|
|
226
|
+
"""Keep the choice for the repository Settings was opened from."""
|
|
227
|
+
save_push_tracking_choice(self.preference, context.common_git_dir, choice)
|
|
228
|
+
|
|
229
|
+
return GitSettingWidget(self.git_setting_service, context, self.repository.git_filename,
|
|
230
|
+
push_tracking_choice=push_tracking_choice(self.preference, context.common_git_dir), save_push_tracking=save_push_tracking)
|
|
231
|
+
|
|
232
|
+
def _verify_ssh(self, save: bool = False) -> None:
|
|
233
|
+
"""Verify a captured SSH candidate and discard results from a closed Settings view.
|
|
234
|
+
|
|
235
|
+
With Executable empty the candidate is the client operations use now, so Verify is useful without any setting;
|
|
236
|
+
that check never marks the draft as verified.
|
|
237
|
+
"""
|
|
238
|
+
dialog = self.session_registry_controller.setting_dialog
|
|
239
|
+
if dialog is None or dialog.ssh_widget.request_id:
|
|
240
|
+
return
|
|
241
|
+
widget = dialog.ssh_widget
|
|
242
|
+
configuration = widget.verification_configuration()
|
|
243
|
+
generation = self.setting_generation
|
|
244
|
+
widget.set_verifying()
|
|
245
|
+
|
|
246
|
+
def complete(result: SshVerification) -> None:
|
|
247
|
+
"""Continue an explicit Save only if its original candidate is still current."""
|
|
248
|
+
if self.session_registry_controller.setting_dialog is not dialog or not isValid(widget):
|
|
249
|
+
return
|
|
250
|
+
widget.show_result(result)
|
|
251
|
+
if save and result.succeeded and generation == self.setting_generation:
|
|
252
|
+
self._validate_setting(dialog.typed_git_filename(), True)
|
|
253
|
+
|
|
254
|
+
widget.request_id = self.ssh.verify(configuration, complete)
|
|
255
|
+
|
|
256
|
+
def _save_ssh(self, dialog: SettingDialog) -> bool:
|
|
257
|
+
"""Persist tool paths independently and retain a failed SSH draft on partial saves."""
|
|
258
|
+
widget = dialog.ssh_widget
|
|
259
|
+
configuration = widget.configuration()
|
|
260
|
+
try:
|
|
261
|
+
configuration.validate(optional=True)
|
|
262
|
+
if widget.request_id:
|
|
263
|
+
raise ValueError("Wait for SSH verification to finish before saving settings.")
|
|
264
|
+
if configuration.ssh_filename and widget.verified_configuration != configuration:
|
|
265
|
+
raise ValueError("Verify the SSH client before saving settings.")
|
|
266
|
+
self.preference.update({"ssh_filename": configuration.ssh_filename, "git_utility_dir": configuration.utility_dir})
|
|
267
|
+
except (OSError, TypeError, ValueError) as error:
|
|
268
|
+
widget.status_label.show_failure(str(error))
|
|
269
|
+
dialog.reveal_widget(SSH_PAGE_NAME, widget.status_label)
|
|
270
|
+
return False
|
|
271
|
+
widget.show_effective(*self._effective_ssh())
|
|
272
|
+
return True
|
|
273
|
+
|
|
274
|
+
def _effective_ssh(self) -> tuple[str, str]:
|
|
275
|
+
"""Name the SSH client operations use now, and the source Settings shows for it: saved, detected or none."""
|
|
276
|
+
return self.ssh.configuration().ssh_filename, effective_source(self.ssh.saved_configuration(), self.ssh.detection())
|
|
277
|
+
|
|
278
|
+
def _verify_setting(self, git_filename: str) -> None:
|
|
279
|
+
"""Check the Git path on an explicit Verify Git click, announcing a success so the click visibly did something."""
|
|
280
|
+
self._validate_setting(git_filename, False, announce=True)
|
|
281
|
+
|
|
282
|
+
def _validate_setting(self, git_filename: str, save: bool, announce: bool = False) -> None:
|
|
283
|
+
"""Validate the current draft with a unique generation before any save; ``announce`` shows a success in green with a confirmation box."""
|
|
284
|
+
if self.session_registry_controller.setting_dialog is not None and self.session_registry_controller.setting_dialog.git_setting_widget.saving:
|
|
285
|
+
return
|
|
286
|
+
if save and self.session_registry_controller.setting_dialog is not None:
|
|
287
|
+
widget = self.session_registry_controller.setting_dialog.ssh_widget
|
|
288
|
+
if widget.request_id:
|
|
289
|
+
return
|
|
290
|
+
if widget.configuration().ssh_filename and widget.verified_configuration != widget.configuration():
|
|
291
|
+
self._verify_ssh(save=True)
|
|
292
|
+
return
|
|
293
|
+
self._invalidate_setting()
|
|
294
|
+
if self.startup_identity is not None:
|
|
295
|
+
self.validator.cancel(self.startup_identity.session_id)
|
|
296
|
+
self.startup_identity = None
|
|
297
|
+
self._abandon_waiting_openings()
|
|
298
|
+
if self.session_registry_controller.setting_dialog is None:
|
|
299
|
+
return
|
|
300
|
+
if save:
|
|
301
|
+
draft = self.session_registry_controller.setting_dialog.tool_draft()
|
|
302
|
+
try:
|
|
303
|
+
DifftoolRecipe(draft.difftool_filename, draft.difftool_command).validate(family=self.distribution_family, release=self.distribution_release)
|
|
304
|
+
MergetoolRecipe(draft.mergetool_filename, draft.mergetool_command).validate(optional=True, family=self.distribution_family, release=self.distribution_release)
|
|
305
|
+
draft.editor_recipe.validate()
|
|
306
|
+
except ValueError as error:
|
|
307
|
+
self.session_registry_controller.setting_dialog.status_label.show_failure(str(error))
|
|
308
|
+
return
|
|
309
|
+
if git_filename == self.preference.value_by_key["git_filename"]:
|
|
310
|
+
self._save_setting(git_filename, self.repository.git_version)
|
|
311
|
+
return
|
|
312
|
+
self.save_requested = save
|
|
313
|
+
self.setting_verify_clicked = announce
|
|
314
|
+
self.setting_identity = RequestIdentity(self.setting_session_id, self.setting_generation)
|
|
315
|
+
self.session_registry_controller.setting_dialog.set_loading(True)
|
|
316
|
+
self.validator.validate(git_filename, self.setting_identity, read_tools_folder=True)
|
|
317
|
+
|
|
318
|
+
def _invalidate_setting(self) -> None:
|
|
319
|
+
"""Ensure an edited or closed draft cannot accept an older validation result."""
|
|
320
|
+
self.setting_generation += 1
|
|
321
|
+
self.setting_identity = None
|
|
322
|
+
self.save_requested = False
|
|
323
|
+
self.setting_verify_clicked = False
|
|
324
|
+
self.setting_validated_git_filename = ""
|
|
325
|
+
self.setting_tools_folder_warning = ""
|
|
326
|
+
self.validator.cancel(self.setting_session_id)
|
|
327
|
+
|
|
328
|
+
def _detect_setting(self) -> None:
|
|
329
|
+
"""Put an explicitly detected candidate in the draft without changing active Git."""
|
|
330
|
+
candidate_filenames = detect_git_candidates()
|
|
331
|
+
if self.session_registry_controller.setting_dialog is None:
|
|
332
|
+
return
|
|
333
|
+
if not candidate_filenames:
|
|
334
|
+
self.session_registry_controller.setting_dialog.status_label.setText("Git was not found\nNo usable Git executable was found. Choose its path in Settings.")
|
|
335
|
+
return
|
|
336
|
+
first_candidate_index = 0
|
|
337
|
+
self.session_registry_controller.setting_dialog.path_edit.setText(candidate_filenames[first_candidate_index])
|
|
338
|
+
self._validate_setting(self.session_registry_controller.setting_dialog.typed_git_filename(), False)
|
|
339
|
+
|
|
340
|
+
def _close_setting(self) -> None:
|
|
341
|
+
"""Abandon the draft and its validation without touching accepted preferences."""
|
|
342
|
+
self._invalidate_setting()
|
|
343
|
+
dialog = self.session_registry_controller.setting_dialog
|
|
344
|
+
if dialog is not None:
|
|
345
|
+
if dialog.ssh_widget.request_id:
|
|
346
|
+
self.repository.runner.cancel(dialog.ssh_widget.request_id)
|
|
347
|
+
dialog.account_page.abandon_sign_in()
|
|
348
|
+
dialog.git_setting_widget.stop_read()
|
|
349
|
+
dialog.deleteLater()
|
|
350
|
+
self.session_registry_controller.setting_dialog = None
|
|
351
|
+
|
|
352
|
+
def _restore_welcome_setting_label(self) -> None:
|
|
353
|
+
"""Give every welcome window's settings button its usual label back once a validated Git is in use."""
|
|
354
|
+
for welcome in self.session_registry_controller.welcome_by_session.values():
|
|
355
|
+
welcome.setting_button.setText(SharedText.SETTING_BUTTON.value)
|
|
356
|
+
|
|
357
|
+
def initialize_git(self) -> None:
|
|
358
|
+
"""Initialize the merge default and validate saved Git before considering detected choices.
|
|
359
|
+
|
|
360
|
+
Run once at launch, after the welcome window is shown. Without any Git to try, the welcome window learns it
|
|
361
|
+
through ``git_missing``; otherwise the answer comes back to ``git_validated``, which tries the next candidate.
|
|
362
|
+
"""
|
|
363
|
+
self.preference.initialize_mergetool(save_detected=False)
|
|
364
|
+
saved_filename = str(self.preference.read("git_filename"))
|
|
365
|
+
self.candidate_filenames = [saved_filename] if saved_filename else detect_git_candidates()
|
|
366
|
+
if self.launch_validation_identity is not None:
|
|
367
|
+
self.validator.cancel(self.launch_validation_identity.session_id)
|
|
368
|
+
self.launch_validation_identity = None
|
|
369
|
+
self.startup_identity = RequestIdentity(uuid.uuid4().hex)
|
|
370
|
+
if not self.candidate_filenames:
|
|
371
|
+
self.startup_identity = None
|
|
372
|
+
self.git_missing.emit()
|
|
373
|
+
return
|
|
374
|
+
first_candidate_index = 0
|
|
375
|
+
candidate_filename = self.candidate_filenames.pop(first_candidate_index)
|
|
376
|
+
# Opening no longer asks Git for its version: an opening asked before this check answers waits for it, and
|
|
377
|
+
# resumes only with the Git the check accepted. The detected path is still shown right away in the settings.
|
|
378
|
+
self.repository.git_filename = candidate_filename
|
|
379
|
+
self.validator.validate(candidate_filename, self.startup_identity)
|
|
380
|
+
|
|
381
|
+
def forget_waiting_opening(self, session_id: str) -> None:
|
|
382
|
+
"""Drop the opening a closed chooser was waiting to start, so the end of the startup check never starts it."""
|
|
383
|
+
self.waiting_opening_by_session.pop(session_id, None)
|
|
384
|
+
|
|
385
|
+
def git_validated(self, identity: RequestIdentity, git_filename: str, version: str, failure: RepositoryFailure | None) -> None:
|
|
386
|
+
"""Publish the accepted executable only after the latest validation and successful sync."""
|
|
387
|
+
if identity == self.startup_identity:
|
|
388
|
+
if failure is not None and self.candidate_filenames:
|
|
389
|
+
first_candidate_index = 0
|
|
390
|
+
self.validator.validate(self.candidate_filenames.pop(first_candidate_index), identity)
|
|
391
|
+
return
|
|
392
|
+
self.startup_identity = None
|
|
393
|
+
if failure is not None:
|
|
394
|
+
self._abandon_waiting_openings()
|
|
395
|
+
self.git_missing.emit()
|
|
396
|
+
self._show_setting_failure(failure)
|
|
397
|
+
return
|
|
398
|
+
self.repository.git_filename = git_filename
|
|
399
|
+
self.repository.git_version = version
|
|
400
|
+
self._restore_welcome_setting_label()
|
|
401
|
+
self._resume_waiting_openings(git_filename)
|
|
402
|
+
return
|
|
403
|
+
if identity == self.launch_validation_identity:
|
|
404
|
+
self.launch_validation_identity = None
|
|
405
|
+
if failure is not None:
|
|
406
|
+
return
|
|
407
|
+
if self.repository.git_filename == git_filename or os.path.abspath(self.repository.git_filename) == git_filename:
|
|
408
|
+
self.repository.git_version = version
|
|
409
|
+
return
|
|
410
|
+
if identity != self.setting_identity or self.session_registry_controller.setting_dialog is None:
|
|
411
|
+
return
|
|
412
|
+
self.setting_identity = None
|
|
413
|
+
if failure is None:
|
|
414
|
+
self.setting_validated_git_filename = git_filename
|
|
415
|
+
self.session_registry_controller.setting_dialog.set_loading(False)
|
|
416
|
+
if failure is not None:
|
|
417
|
+
self._show_setting_failure(failure)
|
|
418
|
+
return
|
|
419
|
+
if self.setting_verify_clicked and not self.save_requested:
|
|
420
|
+
self.session_registry_controller.setting_dialog.show_git_verified(version, git_filename, self.setting_tools_folder_warning)
|
|
421
|
+
else:
|
|
422
|
+
self.session_registry_controller.setting_dialog.show_git_available(version, self.setting_tools_folder_warning)
|
|
423
|
+
widget = self.session_registry_controller.setting_dialog.git_setting_widget
|
|
424
|
+
if widget.git_filename != git_filename and not widget.dirty_target_set:
|
|
425
|
+
widget.stop_read()
|
|
426
|
+
widget.closed = False
|
|
427
|
+
widget.git_filename = git_filename
|
|
428
|
+
widget.reload()
|
|
429
|
+
if not self.save_requested:
|
|
430
|
+
return
|
|
431
|
+
self._save_setting(git_filename, version)
|
|
432
|
+
|
|
433
|
+
def git_tools_folder_checked(self, identity: RequestIdentity, _git_filename: str, warning: str) -> None:
|
|
434
|
+
"""Keep the tools folder warning of the current Settings check until its validation is shown; the startup and launch checks never ask for one."""
|
|
435
|
+
if identity != self.setting_identity:
|
|
436
|
+
return
|
|
437
|
+
self.setting_tools_folder_warning = warning
|
|
438
|
+
|
|
439
|
+
def _save_setting(self, git_filename: str, version: str) -> None:
|
|
440
|
+
"""Report preference and Git saves independently without losing either failed draft."""
|
|
441
|
+
if self.session_registry_controller.setting_dialog is None:
|
|
442
|
+
return
|
|
443
|
+
dialog = self.session_registry_controller.setting_dialog
|
|
444
|
+
generation = self.setting_generation
|
|
445
|
+
ssh_saved = self._save_ssh(dialog)
|
|
446
|
+
preference_saved = True
|
|
447
|
+
# The proof that this very save was validated exists only while it is being written. Its change signal arrives
|
|
448
|
+
# during the call and is the only one allowed to reuse it, which is what tells a real save apart from a change
|
|
449
|
+
# made elsewhere to the same path.
|
|
450
|
+
self.saving_validated_git_filename = git_filename if self.setting_validated_git_filename == git_filename else ""
|
|
451
|
+
draft = dialog.tool_draft()
|
|
452
|
+
try:
|
|
453
|
+
self.preference.update({"git_filename": git_filename, "difftool_filename": draft.difftool_filename, "difftool_command": draft.difftool_command,
|
|
454
|
+
MERGETOOL_FILENAME_PREFERENCE_KEY: draft.mergetool_filename, MERGETOOL_COMMAND_PREFERENCE_KEY: draft.mergetool_command,
|
|
455
|
+
**draft.editor_recipe.preference_value()})
|
|
456
|
+
except (OSError, TypeError, ValueError) as error:
|
|
457
|
+
self._show_setting_failure(RepositoryFailure("Save preferences", "", git_filename, [], str(error)))
|
|
458
|
+
preference_saved = False
|
|
459
|
+
finally:
|
|
460
|
+
self.saving_validated_git_filename = ""
|
|
461
|
+
if preference_saved:
|
|
462
|
+
self.repository.git_filename = git_filename
|
|
463
|
+
self.repository.git_version = version
|
|
464
|
+
self._restore_welcome_setting_label()
|
|
465
|
+
dialog.mergetool_save_notice_label.hide()
|
|
466
|
+
dialog.set_loading(True)
|
|
467
|
+
|
|
468
|
+
def complete(saved: bool) -> None:
|
|
469
|
+
"""Close only after every requested domain succeeds in the original dialog.
|
|
470
|
+
|
|
471
|
+
This Save never writes account choices, so while Accounts still holds unsaved work the window stays open and
|
|
472
|
+
says where, instead of dropping that work by closing. The tools folder warning of the Git this Save checked
|
|
473
|
+
follows that line, since the window is still there to show it.
|
|
474
|
+
"""
|
|
475
|
+
if self.session_registry_controller.setting_dialog is not dialog or not isValid(dialog):
|
|
476
|
+
return
|
|
477
|
+
dialog.set_loading(False)
|
|
478
|
+
if saved and preference_saved and ssh_saved and generation == self.setting_generation:
|
|
479
|
+
place_names = dialog.account_page.unsaved_place_names()
|
|
480
|
+
if place_names:
|
|
481
|
+
dialog.mark_saved()
|
|
482
|
+
dialog.show_success_with_warning("Settings saved. Unsaved changes remain in: " + ", ".join(place_names) + ".", self.setting_tools_folder_warning)
|
|
483
|
+
return
|
|
484
|
+
dialog.accept()
|
|
485
|
+
elif not ssh_saved:
|
|
486
|
+
dialog.status_label.show_failure("Other application preferences saved. SSH settings need attention." if preference_saved else SETTING_FAILED_DRAFTS_TEXT)
|
|
487
|
+
elif not saved:
|
|
488
|
+
dialog.status_label.show_failure("Application preferences saved. Some Git settings need attention." if preference_saved else SETTING_FAILED_DRAFTS_TEXT)
|
|
489
|
+
elif not preference_saved:
|
|
490
|
+
# Git and SSH saved but the Tools preferences did not: without this branch the bottom line kept the
|
|
491
|
+
# "Verifying" wait text, and nothing said that the Tools drafts were not saved.
|
|
492
|
+
dialog.status_label.show_failure(SETTING_FAILED_DRAFTS_TEXT)
|
|
493
|
+
|
|
494
|
+
dialog.git_setting_widget.save(complete)
|
|
495
|
+
|
|
496
|
+
def _show_setting_failure(self, failure: RepositoryFailure) -> None:
|
|
497
|
+
"""Present a sanitized cause and copyable details while preserving the Git draft."""
|
|
498
|
+
if self.session_registry_controller.setting_dialog is None:
|
|
499
|
+
return
|
|
500
|
+
diagnostic = Diagnostic(failure.operation, failure.repository_dir, failure.program_filename, failure.arguments,
|
|
501
|
+
f"Cannot use {failure.program_filename}: {failure.cause}. Correct the path, then verify Git again.", failure.stderr, failure.return_code).prepare()
|
|
502
|
+
self.session_registry_controller.setting_dialog.show_failure(diagnostic)
|
|
503
|
+
if self.diagnostic_log is not None:
|
|
504
|
+
self.diagnostic_log.write(diagnostic)
|
|
505
|
+
dialog = ErrorDialog(diagnostic, self.session_registry_controller.setting_dialog)
|
|
506
|
+
self.session_registry_controller.dialogs.append(dialog)
|
|
507
|
+
|
|
508
|
+
def finish() -> None:
|
|
509
|
+
"""Release the diagnostic and return focus to the editable Git path."""
|
|
510
|
+
if dialog in self.session_registry_controller.dialogs:
|
|
511
|
+
self.session_registry_controller.dialogs.remove(dialog)
|
|
512
|
+
if self.session_registry_controller.setting_dialog is not None:
|
|
513
|
+
self.session_registry_controller.setting_dialog.path_edit.setFocus()
|
|
514
|
+
dialog.deleteLater()
|
|
515
|
+
|
|
516
|
+
dialog.finished.connect(finish)
|
|
517
|
+
open_window_modal(dialog)
|
|
518
|
+
|
|
519
|
+
def preference_changed(self, changed_value_by_key: dict[str, object]) -> None:
|
|
520
|
+
"""Apply successful shared changes to future reads and any currently open chooser.
|
|
521
|
+
|
|
522
|
+
A new Git or Git utilities folder also detects terminals again, since both decide where Git Bash is found.
|
|
523
|
+
"""
|
|
524
|
+
if any(key in changed_value_by_key for key in ("git_filename", MERGETOOL_FILENAME_PREFERENCE_KEY, MERGETOOL_COMMAND_PREFERENCE_KEY)):
|
|
525
|
+
for session_id in tuple(self.session_registry_controller.session_by_id):
|
|
526
|
+
self.mergetool.invalidate_session(session_id)
|
|
527
|
+
if "git_filename" in changed_value_by_key:
|
|
528
|
+
self.repository.git_filename = str(changed_value_by_key["git_filename"])
|
|
529
|
+
self.repository.git_version = ""
|
|
530
|
+
self._abandon_waiting_openings()
|
|
531
|
+
if self.saving_validated_git_filename and self.saving_validated_git_filename == self.repository.git_filename:
|
|
532
|
+
# This very save was already validated, so checking it again would only repeat the same version probe,
|
|
533
|
+
# and an older check still running must not answer for it.
|
|
534
|
+
self.setting_validated_git_filename = ""
|
|
535
|
+
if self.launch_validation_identity is not None:
|
|
536
|
+
self.validator.cancel(self.launch_validation_identity.session_id)
|
|
537
|
+
self.launch_validation_identity = None
|
|
538
|
+
else:
|
|
539
|
+
self._validate_selected_git(self.repository.git_filename)
|
|
540
|
+
if "recent_dirs" in changed_value_by_key:
|
|
541
|
+
for window in self.session_registry_controller.welcome_by_session.values():
|
|
542
|
+
window.show_recent(changed_value_by_key["recent_dirs"])
|
|
543
|
+
for repository_window in self.session_registry_controller.window_by_session.values():
|
|
544
|
+
repository_window.show_recent_repositories(changed_value_by_key["recent_dirs"])
|
|
545
|
+
for session_id, window in self.session_registry_controller.window_by_session.items():
|
|
546
|
+
window.apply_reading_preference(changed_value_by_key)
|
|
547
|
+
window.apply_fetch_preference(changed_value_by_key)
|
|
548
|
+
if "diff_show_entire_file" in changed_value_by_key:
|
|
549
|
+
self.history_read_controller.change_diff_context(session_id, changed_value_by_key["diff_show_entire_file"])
|
|
550
|
+
for staging in self.staging_session_registry_controller.staging_by_key.values():
|
|
551
|
+
if "git_filename" in changed_value_by_key:
|
|
552
|
+
staging.set_git_filename(self.repository.git_filename)
|
|
553
|
+
if staging.window is not None and isValid(staging.window):
|
|
554
|
+
staging.window.apply_preference(changed_value_by_key)
|
|
555
|
+
# These two settings decide where Git Bash is searched; a terminal the user already chose is never replaced.
|
|
556
|
+
if "git_filename" in changed_value_by_key or "git_utility_dir" in changed_value_by_key:
|
|
557
|
+
self.terminal_registry.redetect()
|
|
558
|
+
|
|
559
|
+
def _resume_waiting_openings(self, accepted_git_filename: str) -> None:
|
|
560
|
+
"""Release the openings asked during the startup check, but only those asked with the Git it accepted.
|
|
561
|
+
|
|
562
|
+
An opening whose window was closed or cancelled, or that was asked with another Git, is dropped. The others go
|
|
563
|
+
to ``waiting_opening_released``, whose receiver asks about their folder or starts their read.
|
|
564
|
+
"""
|
|
565
|
+
waiting_openings = tuple(self.waiting_opening_by_session.items())
|
|
566
|
+
self.waiting_opening_by_session.clear()
|
|
567
|
+
for session_id, waiting in waiting_openings:
|
|
568
|
+
if self.session_registry_controller.identity_by_session.get(session_id) != waiting.identity or session_id not in self.session_registry_controller.welcome_by_session:
|
|
569
|
+
continue
|
|
570
|
+
if os.path.normcase(os.path.abspath(waiting.git_filename)) != os.path.normcase(os.path.abspath(accepted_git_filename)):
|
|
571
|
+
self._stop_waiting_opening(session_id, waiting, "Git changed while the repository was opening. Open it again.")
|
|
572
|
+
continue
|
|
573
|
+
self.waiting_opening_released.emit(waiting)
|
|
574
|
+
|
|
575
|
+
def _abandon_waiting_openings(self) -> None:
|
|
576
|
+
"""Stop the openings waiting for a startup check that failed or was replaced, leaving their chooser usable."""
|
|
577
|
+
waiting_openings = tuple(self.waiting_opening_by_session.items())
|
|
578
|
+
self.waiting_opening_by_session.clear()
|
|
579
|
+
for session_id, waiting in waiting_openings:
|
|
580
|
+
self._stop_waiting_opening(session_id, waiting, "")
|
|
581
|
+
|
|
582
|
+
def _stop_waiting_opening(self, session_id: str, waiting: WaitingOpening, message: str) -> None:
|
|
583
|
+
"""Give the chooser back to the user for one opening that will not start."""
|
|
584
|
+
if self.session_registry_controller.identity_by_session.get(session_id) != waiting.identity:
|
|
585
|
+
return
|
|
586
|
+
self.session_registry_controller.identity_by_session.pop(session_id, None)
|
|
587
|
+
welcome = self.session_registry_controller.welcome_by_session.get(session_id)
|
|
588
|
+
if welcome is None:
|
|
589
|
+
return
|
|
590
|
+
welcome.set_loading(False)
|
|
591
|
+
if message:
|
|
592
|
+
welcome.show_status(message)
|