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,776 @@
|
|
|
1
|
+
"""Clone a repository from the welcome window, submodules included."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import replace
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from PySide6.QtCore import QObject, Signal
|
|
9
|
+
from shiboken6 import isValid
|
|
10
|
+
|
|
11
|
+
from gitpane.account.account_choice import clone_account_row
|
|
12
|
+
from gitpane.account.address_owner import address_owner, owner_account_namespace
|
|
13
|
+
from gitpane.account.authentication import check_repository_url, endpoint_origin, transfer_account
|
|
14
|
+
from gitpane.config import AUTHENTICATION_PREFERENCE_KEY, ConsoleOperation
|
|
15
|
+
from gitpane.operation.console_tone import ConsoleTone
|
|
16
|
+
from gitpane.operation.diagnostic import Diagnostic
|
|
17
|
+
from gitpane.operation.operation_status import OperationStatus
|
|
18
|
+
from gitpane.transport.clone import BARE_CLONE_DONE_TEXT, CloneIntent, clipboard_local_repository_candidate, clipboard_repository_url
|
|
19
|
+
from gitpane.transport.git_refusal import authentication_refused
|
|
20
|
+
from gitpane.transport.refusal_notice import CLONE_NOTICE_SUBJECT_TEXT, SUBMODULE_NOTICE_SUBJECT_TEXT, account_notice, failure_headline, failure_summary
|
|
21
|
+
from gitpane.ui.console.operation_dialog import OperationDialog
|
|
22
|
+
from gitpane.ui.setting.account_page import fill_repository_combo
|
|
23
|
+
from gitpane.ui.setting.add_account_dialog import AddAccountDialog, sign_in_result_text
|
|
24
|
+
from gitpane.ui.transport.clone_dialog import CLONE_CANCELLED_PRESERVED_TEXT, CLONE_CANCELLED_TEXT, OPENING_CANCELLED_TEXT, CloneDialog, CloneDialogText
|
|
25
|
+
from gitpane.ui.widget.result_label import ResultTone
|
|
26
|
+
from gitpane.ui.window_control.dialog_modality import open_window_modal
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from collections.abc import Callable
|
|
30
|
+
|
|
31
|
+
from PySide6.QtWidgets import QApplication, QWidget
|
|
32
|
+
|
|
33
|
+
from gitpane.account.account_choice import CloneAccountRow
|
|
34
|
+
from gitpane.account.authentication import AccountSelection, AuthenticationService, AuthOutcome, SecretLookup
|
|
35
|
+
from gitpane.controller.account_controller import AccountController
|
|
36
|
+
from gitpane.controller.operation_policy import OperationPolicy
|
|
37
|
+
from gitpane.controller.session_registry import SessionRegistry
|
|
38
|
+
from gitpane.history.repository import GitRepository
|
|
39
|
+
from gitpane.operation.diagnostic import DiagnosticLog, PreparedDiagnostic
|
|
40
|
+
from gitpane.preference.preference_store import PreferenceStore
|
|
41
|
+
from gitpane.transport.clone import BranchList, CloneRun, CloneService
|
|
42
|
+
from gitpane.transport.refusal_notice import TransferAccount
|
|
43
|
+
|
|
44
|
+
# Title of the operation console a clone opens as soon as it starts, the same console Fetch and Push use.
|
|
45
|
+
CLONE_CONSOLE_TITLE = "Clone repository output"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CloneController(QObject):
|
|
49
|
+
"""The clone form of each welcome window with its console, its sign-in window and its retries, up to the repository it hands to the opening."""
|
|
50
|
+
|
|
51
|
+
# Session of the welcome window, folder of the cloned repository: the opening reads it as if the user had chosen it.
|
|
52
|
+
open_requested = Signal(str, str)
|
|
53
|
+
# Session of the welcome window whose opening of the cloned repository is cancelled with the clone.
|
|
54
|
+
open_cancel_requested = Signal(str)
|
|
55
|
+
|
|
56
|
+
def __init__(self, *, session_registry_controller: SessionRegistry, operation_policy_controller: OperationPolicy, account_controller: AccountController, application: QApplication, preference: PreferenceStore, repository: GitRepository,
|
|
57
|
+
authentication: AuthenticationService, clone: CloneService, diagnostic_log: DiagnosticLog | None, parent: QObject | None = None) -> None:
|
|
58
|
+
"""Receive the services and the lower controllers a clone works with; the wiring builds it after the accounts controller, and the opening learns from its signals which cloned repository to open or which opening to cancel."""
|
|
59
|
+
super().__init__(parent)
|
|
60
|
+
self.session_registry_controller = session_registry_controller
|
|
61
|
+
self.operation_policy_controller = operation_policy_controller
|
|
62
|
+
self.account_controller = account_controller
|
|
63
|
+
self.application = application
|
|
64
|
+
self.preference = preference
|
|
65
|
+
self.repository = repository
|
|
66
|
+
self.authentication = authentication
|
|
67
|
+
self.clone = clone
|
|
68
|
+
self.diagnostic_log = diagnostic_log
|
|
69
|
+
self.clone_output_by_id: dict[str, OperationDialog] = {}
|
|
70
|
+
|
|
71
|
+
def show_clone(self, session_id: str) -> CloneDialog | None:
|
|
72
|
+
"""Open a clone form only for a current welcome window."""
|
|
73
|
+
welcome = self.session_registry_controller.welcome_by_session.get(session_id)
|
|
74
|
+
if welcome is None or session_id in self.session_registry_controller.identity_by_session:
|
|
75
|
+
return None
|
|
76
|
+
previous = self.session_registry_controller.clone_dialog_by_session.get(session_id)
|
|
77
|
+
if previous is not None:
|
|
78
|
+
previous.show()
|
|
79
|
+
previous.raise_()
|
|
80
|
+
return previous
|
|
81
|
+
dialog = CloneDialog(welcome)
|
|
82
|
+
self.session_registry_controller.clone_dialog_by_session[session_id] = dialog
|
|
83
|
+
dialog.clone_requested.connect(lambda: self._submit_clone(session_id))
|
|
84
|
+
dialog.cancel_requested.connect(lambda: self.cancel_clone(session_id))
|
|
85
|
+
dialog.open_requested.connect(lambda: self._retry_clone_open(session_id))
|
|
86
|
+
dialog.retry_requested.connect(lambda: self._retry_clone_submodules(session_id))
|
|
87
|
+
dialog.add_account_button.clicked.connect(lambda: self._show_clone_add_account(session_id))
|
|
88
|
+
dialog.url_edit.textChanged.connect(lambda: self._refresh_clone_endpoint(session_id))
|
|
89
|
+
dialog.branch_list_requested.connect(lambda: self._list_clone_branches(session_id))
|
|
90
|
+
dialog.branch_list_outdated.connect(lambda: self._cancel_clone_branch_list(dialog))
|
|
91
|
+
dialog.finished.connect(lambda: self._cancel_clone_branch_list(dialog))
|
|
92
|
+
dialog.finished.connect(lambda: self.session_registry_controller.clone_dialog_by_session.pop(session_id, None))
|
|
93
|
+
self._prefill_clone_form(dialog)
|
|
94
|
+
open_window_modal(dialog)
|
|
95
|
+
return dialog
|
|
96
|
+
|
|
97
|
+
def _prefill_clone_form(self, dialog: CloneDialog) -> None:
|
|
98
|
+
"""Fill a new clone form with the destinations of the last started clones and with a repository found on the clipboard.
|
|
99
|
+
|
|
100
|
+
The remembered folders are offered as saved, without looking at the disk: a disconnected network drive must not
|
|
101
|
+
freeze the form, and the clone itself refuses a folder that no longer exists. A repository URL on the clipboard
|
|
102
|
+
is filled in at once, after the signals are connected, so the repository name and the account row follow it like
|
|
103
|
+
a typed URL. An absolute folder path on one line is handed to the form, which checks on a background thread
|
|
104
|
+
whether a repository is there and only then writes it into the field, if the field is still empty. Nothing is
|
|
105
|
+
started from the clipboard text.
|
|
106
|
+
"""
|
|
107
|
+
dialog.set_destination_dirs(self.preference.read_clone_destinations())
|
|
108
|
+
clipboard_text = self.application.clipboard().text()
|
|
109
|
+
clipboard_url = clipboard_repository_url(clipboard_text)
|
|
110
|
+
if clipboard_url:
|
|
111
|
+
dialog.url_edit.setText(clipboard_url)
|
|
112
|
+
return
|
|
113
|
+
local_candidate = clipboard_local_repository_candidate(clipboard_text)
|
|
114
|
+
if local_candidate:
|
|
115
|
+
dialog.check_clipboard_repository(local_candidate)
|
|
116
|
+
|
|
117
|
+
def _list_clone_branches(self, session_id: str) -> None:
|
|
118
|
+
"""Read the remote branches of the clone form's URL in the background, with the account the clone would use.
|
|
119
|
+
|
|
120
|
+
An empty or unfinished URL only keeps the remote HEAD entry. A failure only shows a warning icon beside the list,
|
|
121
|
+
saying that the branches could not be read: it never opens Settings and never writes to a clone's console, since the user may
|
|
122
|
+
still be typing. Nothing is read once the form shows a finished clone or the address of a submodule that asked
|
|
123
|
+
for an account: its URL is then fixed and its account menu describes the submodule's address, not the URL's. After
|
|
124
|
+
a failure the form may switch itself to the account named like the address's owner, which reads the list again.
|
|
125
|
+
"""
|
|
126
|
+
dialog = self.session_registry_controller.clone_dialog_by_session.get(session_id)
|
|
127
|
+
if dialog is None or session_id not in self.session_registry_controller.welcome_by_session or self.session_registry_controller.shutdown_requested:
|
|
128
|
+
return
|
|
129
|
+
if dialog.partial or dialog.authentication_endpoint:
|
|
130
|
+
return
|
|
131
|
+
self._cancel_clone_branch_list(dialog)
|
|
132
|
+
url = dialog.repository_url()
|
|
133
|
+
if not self.clone.accepts_endpoint(url):
|
|
134
|
+
dialog.show_branches(())
|
|
135
|
+
return
|
|
136
|
+
selection = None
|
|
137
|
+
namespace = dialog.account_choice()
|
|
138
|
+
if namespace is not None:
|
|
139
|
+
selection = self._clone_listed_account(dialog, namespace)
|
|
140
|
+
if selection is None:
|
|
141
|
+
dialog.show_branch_failure()
|
|
142
|
+
return
|
|
143
|
+
dialog.set_branch_loading()
|
|
144
|
+
|
|
145
|
+
def complete(branch_list: BranchList) -> None:
|
|
146
|
+
"""Show the answer only in the same form, and only for the last branch list it asked for."""
|
|
147
|
+
if self.session_registry_controller.clone_dialog_by_session.get(session_id) is not dialog or not isValid(dialog) or dialog.branch_request_id != branch_list.request_id:
|
|
148
|
+
return
|
|
149
|
+
dialog.branch_request_id = ""
|
|
150
|
+
if branch_list.cause:
|
|
151
|
+
dialog.show_branch_failure()
|
|
152
|
+
self._choose_owner_account(dialog)
|
|
153
|
+
else:
|
|
154
|
+
dialog.show_branches(branch_list.branches)
|
|
155
|
+
|
|
156
|
+
dialog.branch_request_id = self.clone.list_remote_branches(url, self.repository.git_filename, selection, complete)
|
|
157
|
+
|
|
158
|
+
def _choose_owner_account(self, dialog: CloneDialog) -> None:
|
|
159
|
+
"""Switch the clone form to the account named like the owner written in its address, after the branches could not be read.
|
|
160
|
+
|
|
161
|
+
The user often has one account per owner and keeps another as the default, which cannot read a private
|
|
162
|
+
repository of the first. Only a safe case switches: the user did not pick an account in this form, no clone runs,
|
|
163
|
+
the form is about the typed URL (not a submodule's address), exactly one listed account of that service address
|
|
164
|
+
is named like the owner, and it is not already the account in use, the chosen one or the default behind 'Use
|
|
165
|
+
global default'. The switch reads the branches again once; if they still cannot be read, the owner's account is
|
|
166
|
+
then the one in use and nothing switches again. The form keeps the owner and the choice made before, so that an
|
|
167
|
+
address naming another owner puts that choice back and removes the line.
|
|
168
|
+
"""
|
|
169
|
+
if dialog.account_chosen_by_user or dialog.running or dialog.partial or dialog.authentication_endpoint or not dialog.account_origin:
|
|
170
|
+
return
|
|
171
|
+
try:
|
|
172
|
+
catalogue = self.authentication.catalogue()
|
|
173
|
+
except (OSError, ValueError, TypeError):
|
|
174
|
+
return
|
|
175
|
+
namespace = owner_account_namespace(catalogue.accounts, dialog.repository_url())
|
|
176
|
+
if namespace is None:
|
|
177
|
+
return
|
|
178
|
+
account_in_use = dialog.account_choice() or catalogue.default_namespace_by_origin.get(dialog.account_origin)
|
|
179
|
+
if account_in_use == namespace:
|
|
180
|
+
return
|
|
181
|
+
for selection in catalogue.accounts:
|
|
182
|
+
if selection.namespace == namespace and selection.origin == dialog.account_origin:
|
|
183
|
+
dialog.choose_owner_account(namespace, address_owner(dialog.repository_url()), CloneDialogText.OWNER_ACCOUNT_CHOSEN.value.format(label=selection.label))
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
def _cancel_clone_branch_list(self, dialog: CloneDialog) -> None:
|
|
187
|
+
"""Stop the branch list a clone form waits for, when its URL or account changed or when it closed; a late answer is then ignored."""
|
|
188
|
+
request_id = dialog.branch_request_id
|
|
189
|
+
if not request_id:
|
|
190
|
+
return
|
|
191
|
+
dialog.branch_request_id = ""
|
|
192
|
+
self.clone.cancel_branch_list(request_id)
|
|
193
|
+
|
|
194
|
+
def _submit_clone(self, session_id: str) -> None:
|
|
195
|
+
"""Freeze exact form data and retain the process owner outside the dialog.
|
|
196
|
+
|
|
197
|
+
'Use global default' gives no selection: the clone then resolves the address's default and nothing is saved for
|
|
198
|
+
the new repository. An explicit account is used by the clone and saved for the new repository once it opens.
|
|
199
|
+
"""
|
|
200
|
+
dialog = self.session_registry_controller.clone_dialog_by_session.get(session_id)
|
|
201
|
+
if dialog is None or dialog.running or dialog.partial or session_id not in self.session_registry_controller.welcome_by_session:
|
|
202
|
+
return
|
|
203
|
+
if self._clone_form_signing_in(dialog):
|
|
204
|
+
dialog.status_label.show_failure("Finish signing in successfully before cloning.")
|
|
205
|
+
return
|
|
206
|
+
selection = None
|
|
207
|
+
namespace = dialog.account_choice()
|
|
208
|
+
if namespace is not None:
|
|
209
|
+
selection = self._clone_listed_account(dialog, namespace)
|
|
210
|
+
if selection is None:
|
|
211
|
+
dialog.status_label.show_failure("Choose a confirmed account for this repository before cloning.")
|
|
212
|
+
return
|
|
213
|
+
generation = self.session_registry_controller.clone_generation_by_session.get(session_id, 0) + 1
|
|
214
|
+
self.session_registry_controller.clone_generation_by_session[session_id] = generation
|
|
215
|
+
intent = CloneIntent(dialog.repository_url(), dialog.destination_dir(), dialog.subdirectory_name(), session_id, generation, selection, options=dialog.clone_options())
|
|
216
|
+
dialog.set_running(True)
|
|
217
|
+
run = self.clone.submit(intent, self.repository.git_filename)
|
|
218
|
+
dialog.operation_id = run.operation_id
|
|
219
|
+
if run.clone_status != "running":
|
|
220
|
+
# Refused before Git ran (invalid URL, existing destination): there is no output to show, and the form
|
|
221
|
+
# shows the cause once the refusal is published.
|
|
222
|
+
return
|
|
223
|
+
self._open_clone_console(session_id, run, dialog)
|
|
224
|
+
self._remember_clone_destination(intent.parent_dir)
|
|
225
|
+
|
|
226
|
+
def _remember_clone_destination(self, parent_dir: str) -> None:
|
|
227
|
+
"""Put the parent folder of a clone that really started at the front of the next clone forms' Destination list.
|
|
228
|
+
|
|
229
|
+
A refused write only costs the suggestion, so it goes to the --log file and never interrupts the clone.
|
|
230
|
+
"""
|
|
231
|
+
try:
|
|
232
|
+
self.preference.remember_clone_destination(parent_dir)
|
|
233
|
+
except (OSError, ValueError, TypeError) as error:
|
|
234
|
+
diagnostic = Diagnostic("Save clone destination", "", "", [], str(error)).prepare()
|
|
235
|
+
if self.diagnostic_log is not None:
|
|
236
|
+
self.diagnostic_log.write(diagnostic)
|
|
237
|
+
|
|
238
|
+
def _clone_form_signing_in(self, dialog: CloneDialog) -> bool:
|
|
239
|
+
"""Tell whether the clone form's sign-in window is still waiting for a sign-in result."""
|
|
240
|
+
add_dialog = dialog.add_account_dialog
|
|
241
|
+
return add_dialog is not None and isValid(add_dialog) and bool(add_dialog.operation_id)
|
|
242
|
+
|
|
243
|
+
def _clone_listed_account(self, dialog: CloneDialog, namespace: str) -> AccountSelection | None:
|
|
244
|
+
"""Find the connected account chosen in the clone form, for the form's address only.
|
|
245
|
+
|
|
246
|
+
None means the account is no longer listed, or the list could not be read (the cause is then shown under the menu).
|
|
247
|
+
"""
|
|
248
|
+
try:
|
|
249
|
+
accounts = self.authentication.catalogue().accounts
|
|
250
|
+
except (OSError, ValueError, TypeError) as error:
|
|
251
|
+
dialog.show_account_status(Diagnostic("Clone authentication", "", "", [], str(error)).prepare().cause)
|
|
252
|
+
return None
|
|
253
|
+
for selection in accounts:
|
|
254
|
+
if selection.namespace == namespace and selection.origin == dialog.account_origin:
|
|
255
|
+
return selection
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
def _refresh_clone_endpoint(self, session_id: str) -> None:
|
|
259
|
+
"""Show the account row of the address the clone will contact, never the last opened repository's.
|
|
260
|
+
|
|
261
|
+
The address comes from a failed submodule when one asked for authentication, otherwise from the URL. Without an
|
|
262
|
+
HTTPS address (nothing typed yet, SSH, local folder) the row stays in place, greyed with the reason, and a
|
|
263
|
+
running sign-in is cancelled and its window closed. An address on another service, or the first HTTPS address
|
|
264
|
+
after none, cancels a running sign-in, closes its window and starts again from 'Use global default'; another
|
|
265
|
+
address on the same service keeps the running sign-in, which learns the repository to test. After the form
|
|
266
|
+
switched itself to the account of the address's owner, an address naming another owner on the same service puts
|
|
267
|
+
back the choice made before the switch and removes the line that said so; another repository of the same owner
|
|
268
|
+
keeps the owner's account. The choice made before the switch only comes back while the user has chosen no
|
|
269
|
+
account in this form, by the menu or by a sign-in: an account the user chose is never replaced by that earlier
|
|
270
|
+
choice.
|
|
271
|
+
"""
|
|
272
|
+
dialog = self.session_registry_controller.clone_dialog_by_session.get(session_id)
|
|
273
|
+
if dialog is None:
|
|
274
|
+
return
|
|
275
|
+
account_row = self._read_clone_account_row(dialog)
|
|
276
|
+
if account_row is None or not account_row.origin:
|
|
277
|
+
self._close_clone_add_account(session_id)
|
|
278
|
+
dialog.account_origin = ""
|
|
279
|
+
if account_row is not None:
|
|
280
|
+
dialog.show_account_status("")
|
|
281
|
+
dialog.show_account_state(account_row.disabled_text, account_row.add_account_enabled)
|
|
282
|
+
return
|
|
283
|
+
selected_namespace = dialog.account_choice()
|
|
284
|
+
if account_row.origin != dialog.account_origin:
|
|
285
|
+
self._close_clone_add_account(session_id)
|
|
286
|
+
dialog.account_origin = account_row.origin
|
|
287
|
+
dialog.show_account_status("")
|
|
288
|
+
dialog.account_chosen_by_user = False
|
|
289
|
+
dialog.forget_owner_switch()
|
|
290
|
+
selected_namespace = None
|
|
291
|
+
if (not dialog.account_chosen_by_user and dialog.owner_switch_namespace and dialog.owner_switch_namespace == selected_namespace
|
|
292
|
+
and address_owner(dialog.repository_url()).casefold() != dialog.owner_switch_owner):
|
|
293
|
+
selected_namespace = dialog.account_before_owner_switch
|
|
294
|
+
dialog.forget_owner_switch()
|
|
295
|
+
add_dialog = dialog.add_account_dialog
|
|
296
|
+
if add_dialog is not None and isValid(add_dialog) and not add_dialog.closed:
|
|
297
|
+
add_dialog.set_known_check_url(account_row.origin, self._clone_check_url(dialog))
|
|
298
|
+
self._show_clone_account_row(dialog, account_row, selected_namespace)
|
|
299
|
+
|
|
300
|
+
def _read_clone_account_row(self, dialog: CloneDialog) -> CloneAccountRow | None:
|
|
301
|
+
"""Read the connected accounts and describe the clone form's Account row, or say under the row why they could not be read.
|
|
302
|
+
|
|
303
|
+
None means the accounts could not be read: the row is then greyed without any entry, since nothing can be offered.
|
|
304
|
+
"""
|
|
305
|
+
try:
|
|
306
|
+
return clone_account_row(self.authentication.catalogue(), self._clone_contacted_url(dialog))
|
|
307
|
+
except (OSError, ValueError, TypeError) as error:
|
|
308
|
+
dialog.show_account_state("", False)
|
|
309
|
+
dialog.show_account_status(Diagnostic("Clone authentication", "", "", [], str(error)).prepare().cause)
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
def _show_clone_account_row(self, dialog: CloneDialog, account_row: CloneAccountRow, selected_namespace: str | None) -> None:
|
|
313
|
+
"""Fill the clone form's menu like a This repository row ('Use global default — …' first, then the address's accounts), or grey it with its reason."""
|
|
314
|
+
if account_row.disabled_text or account_row.row is None:
|
|
315
|
+
dialog.show_account_state(account_row.disabled_text, account_row.add_account_enabled)
|
|
316
|
+
else:
|
|
317
|
+
fill_repository_combo(dialog.account_combo, account_row.row, selected_namespace)
|
|
318
|
+
dialog.add_account_button.setEnabled(account_row.add_account_enabled)
|
|
319
|
+
if account_row.row is not None:
|
|
320
|
+
dialog.account_combo.setToolTip(account_row.row.title)
|
|
321
|
+
|
|
322
|
+
def _clone_contacted_url(self, dialog: CloneDialog) -> str:
|
|
323
|
+
"""Return the URL the clone form is about: the refused submodule's when one asked for authentication, else the typed repository URL."""
|
|
324
|
+
return dialog.authentication_endpoint or dialog.repository_url()
|
|
325
|
+
|
|
326
|
+
def _clone_check_url(self, dialog: CloneDialog) -> str:
|
|
327
|
+
"""Return the repository on which an account added from the clone form is tested: the URL the form contacts, without its credentials part.
|
|
328
|
+
|
|
329
|
+
A URL the test cannot use is passed as typed, so the account service refuses it with its own reason instead of
|
|
330
|
+
the window asking for another repository.
|
|
331
|
+
"""
|
|
332
|
+
url = self._clone_contacted_url(dialog).strip()
|
|
333
|
+
try:
|
|
334
|
+
return check_repository_url(url, dialog.account_origin)
|
|
335
|
+
except ValueError:
|
|
336
|
+
return url
|
|
337
|
+
|
|
338
|
+
def refresh_clone_account(self, session_id: str, selected_namespace: str | None) -> None:
|
|
339
|
+
"""Show the clone form's menu again after the accounts changed, for the address it already shows, with this account chosen.
|
|
340
|
+
|
|
341
|
+
None as the choice follows 'Use global default'. A form whose address changed meanwhile is left to its own refresh.
|
|
342
|
+
"""
|
|
343
|
+
dialog = self.session_registry_controller.clone_dialog_by_session.get(session_id)
|
|
344
|
+
if dialog is None or not dialog.account_origin:
|
|
345
|
+
return
|
|
346
|
+
account_row = self._read_clone_account_row(dialog)
|
|
347
|
+
if account_row is None or account_row.origin != dialog.account_origin:
|
|
348
|
+
return
|
|
349
|
+
self._show_clone_account_row(dialog, account_row, selected_namespace)
|
|
350
|
+
|
|
351
|
+
def _show_clone_add_account(self, session_id: str) -> None:
|
|
352
|
+
"""Open the shared sign-in window with the address fixed to the one the clone will contact.
|
|
353
|
+
|
|
354
|
+
A username + password on that address is tested on the repository being cloned, or on the refused submodule,
|
|
355
|
+
so the window names that URL instead of asking for one.
|
|
356
|
+
"""
|
|
357
|
+
dialog = self.session_registry_controller.clone_dialog_by_session.get(session_id)
|
|
358
|
+
if dialog is None or dialog.running or not dialog.account_origin or session_id not in self.session_registry_controller.welcome_by_session:
|
|
359
|
+
return
|
|
360
|
+
existing = dialog.add_account_dialog
|
|
361
|
+
if existing is not None and isValid(existing) and not existing.closed:
|
|
362
|
+
existing.showNormal()
|
|
363
|
+
existing.raise_()
|
|
364
|
+
existing.activateWindow()
|
|
365
|
+
return
|
|
366
|
+
add_dialog = AddAccountDialog(dialog, locked_origin=dialog.account_origin, service_name_by_origin=self.account_controller.service_name_by_origin(),
|
|
367
|
+
known_check_url_by_origin={dialog.account_origin: self._clone_check_url(dialog)},
|
|
368
|
+
browser_timeout_ms=self.authentication.browser_timeout_ms)
|
|
369
|
+
dialog.add_account_dialog = add_dialog
|
|
370
|
+
|
|
371
|
+
def finished() -> None:
|
|
372
|
+
"""Forget the closed window so the next Add account… opens a fresh one."""
|
|
373
|
+
if isValid(dialog) and dialog.add_account_dialog is add_dialog:
|
|
374
|
+
dialog.add_account_dialog = None
|
|
375
|
+
add_dialog.deleteLater()
|
|
376
|
+
|
|
377
|
+
add_dialog.sign_in_requested.connect(lambda token_mode: self._start_clone_account_sign_in(session_id, dialog, add_dialog, token_mode))
|
|
378
|
+
add_dialog.cancel_requested.connect(lambda: self.account_controller.cancel_account_sign_in(add_dialog))
|
|
379
|
+
add_dialog.finished.connect(finished)
|
|
380
|
+
add_dialog.show()
|
|
381
|
+
|
|
382
|
+
def _start_clone_account_sign_in(self, session_id: str, dialog: CloneDialog, add_dialog: AddAccountDialog, token_mode: bool) -> None:
|
|
383
|
+
"""Start a sign-in whose result is accepted only while the same clone form still shows the same address.
|
|
384
|
+
|
|
385
|
+
When a failed submodule asked for the sign-in, the result also needs the same partial clone, the same failed
|
|
386
|
+
submodule and an unchanged destination. The address sent is the fixed one, never the full URL, so a user name
|
|
387
|
+
written in the URL is not used as a sign-in hint. A username + password is tested on the URL the form contacts
|
|
388
|
+
at the moment of the click; a refused test leaves the partial clone as it is and retries nothing.
|
|
389
|
+
"""
|
|
390
|
+
if dialog.running:
|
|
391
|
+
add_dialog.show_refusal("Wait until the clone stops before signing in.")
|
|
392
|
+
return
|
|
393
|
+
origin = dialog.account_origin
|
|
394
|
+
add_dialog.set_known_check_url(origin, self._clone_check_url(dialog))
|
|
395
|
+
run = self.clone.run_by_id.get(dialog.operation_id) if dialog.operation_id is not None else None
|
|
396
|
+
target = run.authentication_target if run is not None else None
|
|
397
|
+
clone_intent = run.intent if run is not None else None
|
|
398
|
+
|
|
399
|
+
def owner_is_current() -> bool:
|
|
400
|
+
"""Accept a sign-in result only for its still-visible clone form and address."""
|
|
401
|
+
if target is not None:
|
|
402
|
+
try:
|
|
403
|
+
run.destination.verify()
|
|
404
|
+
except (OSError, ValueError):
|
|
405
|
+
return False
|
|
406
|
+
if run.authentication_target is not target or run.intent is not clone_intent or not self.operation_policy_controller.clone_is_current(clone_intent):
|
|
407
|
+
return False
|
|
408
|
+
return (self.session_registry_controller.clone_dialog_by_session.get(session_id) is dialog and session_id in self.session_registry_controller.welcome_by_session and not self.session_registry_controller.shutdown_requested
|
|
409
|
+
and dialog.account_origin == origin)
|
|
410
|
+
|
|
411
|
+
self.account_controller.start_account_sign_in(add_dialog, token_mode, owner_is_current, lambda outcome: self._clone_account_signed_in(session_id, outcome))
|
|
412
|
+
|
|
413
|
+
def _clone_account_signed_in(self, session_id: str, outcome: AuthOutcome) -> None:
|
|
414
|
+
"""Show the new account in the clone form: through 'Use global default' when it became the default, else chosen explicitly.
|
|
415
|
+
|
|
416
|
+
After a submodule asked for authentication, the new account is always chosen explicitly for that submodule and
|
|
417
|
+
the submodules are retried at once; it is saved for that submodule only, after the retry succeeds. Otherwise the
|
|
418
|
+
branch list is read again, since the menu was changed by the program and the new account may see the branches
|
|
419
|
+
the last list could not read. The account signed in from the form is the user's choice: the form then forgets
|
|
420
|
+
its own switch to the account of the address's owner, so that no later address puts back the choice made before
|
|
421
|
+
that switch; the line saying how the sign-in went stays. A refusal notice an open Settings window shows for the
|
|
422
|
+
same address is cleared, as when the account is added from Settings itself.
|
|
423
|
+
"""
|
|
424
|
+
dialog = self.session_registry_controller.clone_dialog_by_session.get(session_id)
|
|
425
|
+
if dialog is None or outcome.selection is None:
|
|
426
|
+
return
|
|
427
|
+
run = self.clone.run_by_id.get(dialog.operation_id) if dialog.operation_id is not None else None
|
|
428
|
+
for_submodule = run is not None and run.authentication_target is not None
|
|
429
|
+
selected_namespace = None if outcome.became_default and not for_submodule else outcome.selection.namespace
|
|
430
|
+
self.refresh_clone_account(session_id, selected_namespace)
|
|
431
|
+
dialog.show_account_status(sign_in_result_text(outcome, reconnect=False))
|
|
432
|
+
account = transfer_account(outcome.selection)
|
|
433
|
+
setting_dialog = self.session_registry_controller.setting_dialog
|
|
434
|
+
if account is not None and setting_dialog is not None and isValid(setting_dialog):
|
|
435
|
+
setting_dialog.account_page.clear_account_notice(account.address)
|
|
436
|
+
if for_submodule:
|
|
437
|
+
run.child_selection = outcome.selection
|
|
438
|
+
self._retry_clone_submodules(session_id)
|
|
439
|
+
return
|
|
440
|
+
dialog.account_chosen_by_user = True
|
|
441
|
+
dialog.forget_owner_switch()
|
|
442
|
+
dialog.restart_branch_list()
|
|
443
|
+
|
|
444
|
+
def _cancel_clone_authentication(self, session_id: str) -> None:
|
|
445
|
+
"""Stop only the sign-in started from this clone form; its late result is then ignored."""
|
|
446
|
+
dialog = self.session_registry_controller.clone_dialog_by_session.get(session_id)
|
|
447
|
+
if dialog is None:
|
|
448
|
+
return
|
|
449
|
+
if self._clone_form_signing_in(dialog):
|
|
450
|
+
self.account_controller.cancel_account_sign_in(dialog.add_account_dialog)
|
|
451
|
+
|
|
452
|
+
def _close_clone_add_account(self, session_id: str) -> None:
|
|
453
|
+
"""Cancel the clone form's sign-in and close its window, whose fixed address no longer matches the form."""
|
|
454
|
+
self._cancel_clone_authentication(session_id)
|
|
455
|
+
dialog = self.session_registry_controller.clone_dialog_by_session.get(session_id)
|
|
456
|
+
if dialog is None:
|
|
457
|
+
return
|
|
458
|
+
add_dialog = dialog.add_account_dialog
|
|
459
|
+
if add_dialog is not None and isValid(add_dialog) and not add_dialog.closed:
|
|
460
|
+
add_dialog.reject()
|
|
461
|
+
|
|
462
|
+
def clone_finished(self, run: CloneRun) -> None:
|
|
463
|
+
"""Finish the clone's console whatever became of its form, then publish only current terminal clones in the form."""
|
|
464
|
+
output = self.clone_output_by_id.get(run.operation_id)
|
|
465
|
+
if output is not None and isValid(output):
|
|
466
|
+
failure_text, headline = self._clone_console_failure(run)
|
|
467
|
+
output.show_result(OperationStatus(run.recursive_status), failure_text=failure_text, failure_headline=headline)
|
|
468
|
+
dialog = self.session_registry_controller.clone_dialog_by_session.get(run.intent.session_id)
|
|
469
|
+
if run.cancelled and dialog is not None and dialog.operation_id == run.operation_id:
|
|
470
|
+
cause = CLONE_CANCELLED_TEXT
|
|
471
|
+
if run.destination is not None and not run.destination_removed:
|
|
472
|
+
cause = CLONE_CANCELLED_PRESERVED_TEXT + run.destination.destination_dir
|
|
473
|
+
if run.incomplete_repository:
|
|
474
|
+
dialog.show_partial_submodules(cause, run.validation_status == OperationStatus.SUCCESS.value, cancelled=True)
|
|
475
|
+
else:
|
|
476
|
+
dialog.show_cancelled(cause, partial=run.clone_status == OperationStatus.SUCCESS.value)
|
|
477
|
+
return
|
|
478
|
+
if run.cancelled or not self.operation_policy_controller.clone_is_current(run.intent):
|
|
479
|
+
return
|
|
480
|
+
dialog = self.session_registry_controller.clone_dialog_by_session[run.intent.session_id]
|
|
481
|
+
if run.recursive_status == OperationStatus.SUCCESS.value:
|
|
482
|
+
if run.validation_status == OperationStatus.SUCCESS.value and run.intent.options.bare:
|
|
483
|
+
self._finish_bare_clone(run, dialog)
|
|
484
|
+
elif run.validation_status == OperationStatus.SUCCESS.value:
|
|
485
|
+
self._open_clone(run.intent.session_id)
|
|
486
|
+
else:
|
|
487
|
+
# Opening is offered again only for a repository with a working tree; a bare clone is never opened.
|
|
488
|
+
dialog.show_failure(failure_summary(run.diagnostic) if run.diagnostic is not None else "Repository validation failed.", partial=not run.intent.options.bare)
|
|
489
|
+
elif run.incomplete_repository:
|
|
490
|
+
partial_cause = failure_summary(run.diagnostic, self._submodule_transfer_account(run)) if run.diagnostic else run.recursive_status
|
|
491
|
+
dialog.show_partial_submodules(partial_cause, run.validation_status == OperationStatus.SUCCESS.value, explained=bool(run.checkout_refusal_text))
|
|
492
|
+
if run.authentication_target is not None:
|
|
493
|
+
if dialog.authentication_endpoint != run.authentication_target.endpoint:
|
|
494
|
+
# Forgetting the shown address makes the refresh below treat the submodule's address as new: any
|
|
495
|
+
# sign-in is cancelled and the row starts again from 'Use global default'.
|
|
496
|
+
dialog.account_origin = ""
|
|
497
|
+
run.child_selection = None
|
|
498
|
+
dialog.authentication_endpoint = run.authentication_target.endpoint
|
|
499
|
+
self._refresh_clone_endpoint(run.intent.session_id)
|
|
500
|
+
dialog.account_section.setEnabled(True)
|
|
501
|
+
cause = "Authentication required for submodule: " + run.authentication_target.entry.path + "\n" + run.authentication_target.endpoint
|
|
502
|
+
dialog.show_account_status(Diagnostic("Submodule authentication", "", "", [], cause).prepare().cause)
|
|
503
|
+
if run.diagnostic is not None:
|
|
504
|
+
subject = SUBMODULE_NOTICE_SUBJECT_TEXT.format(path=run.authentication_target.entry.path)
|
|
505
|
+
notice = account_notice(run.diagnostic, self._submodule_transfer_account(run), subject)
|
|
506
|
+
if notice is not None:
|
|
507
|
+
self.account_controller.offer_account_after_refusal(None, notice)
|
|
508
|
+
# Read after the account row above is refreshed, so the check and its later rewrite name the same account.
|
|
509
|
+
try:
|
|
510
|
+
submodule_selection = self._submodule_account_selection(run)
|
|
511
|
+
except ValueError:
|
|
512
|
+
submodule_selection = None
|
|
513
|
+
self.lookup_refused_account(submodule_selection, run.diagnostic, lambda: self._show_partial_clone_needs_reconnect(run, dialog))
|
|
514
|
+
elif run.diagnostic is None:
|
|
515
|
+
dialog.show_failure(run.clone_status)
|
|
516
|
+
else:
|
|
517
|
+
account = transfer_account(run.selection, run.intent.endpoint, self.authentication.missing_secret_namespace_set)
|
|
518
|
+
dialog.show_failure(failure_summary(run.diagnostic, account), explained=bool(failure_headline(run.diagnostic, account)))
|
|
519
|
+
self.lookup_refused_account(run.selection, run.diagnostic, lambda: self._show_clone_needs_reconnect(run, dialog))
|
|
520
|
+
notice = account_notice(run.diagnostic, account, CLONE_NOTICE_SUBJECT_TEXT)
|
|
521
|
+
if notice is not None:
|
|
522
|
+
self.account_controller.offer_account_after_refusal(None, notice)
|
|
523
|
+
# The form keeps the refusal as its red cause; the install window comes on top of it, and the user clicks Clone
|
|
524
|
+
# or Retry submodules again once the credential manager is installed.
|
|
525
|
+
if run.credential_manager_refusal is not None:
|
|
526
|
+
self.account_controller.offer_credential_manager_install(run.credential_manager_refusal, dialog)
|
|
527
|
+
|
|
528
|
+
def _finish_bare_clone(self, run: CloneRun, dialog: CloneDialog) -> None:
|
|
529
|
+
"""End a successful bare clone without opening it: a bare repository has no working tree to show in a repository window.
|
|
530
|
+
|
|
531
|
+
The console says where the repository was written and stays on screen, the form closes without cancelling
|
|
532
|
+
anything, and the recent list is left as it was since nothing was opened.
|
|
533
|
+
"""
|
|
534
|
+
run.trace.append_event(run.operation_id, "status", BARE_CLONE_DONE_TEXT + run.destination.destination_dir, ConsoleTone.SUCCESS)
|
|
535
|
+
welcome = self.session_registry_controller.welcome_by_session.get(run.intent.session_id)
|
|
536
|
+
if welcome is not None:
|
|
537
|
+
# A successful result closes the console by itself unless Keep dialog open is checked, and here the path it
|
|
538
|
+
# shows is the only answer the user gets, so it is shown again.
|
|
539
|
+
self.keep_clone_console(run.operation_id, welcome, show=True)
|
|
540
|
+
dialog.accept()
|
|
541
|
+
|
|
542
|
+
def _show_clone_needs_reconnect(self, run: CloneRun, dialog: CloneDialog) -> None:
|
|
543
|
+
"""Say that the refused account needs a reconnect in the clone's console and in the clone form, while each still shows the failure of that same clone."""
|
|
544
|
+
output = self.clone_output_by_id.get(run.operation_id)
|
|
545
|
+
if output is not None and isValid(output) and not output.running:
|
|
546
|
+
failure_text, headline = self._clone_console_failure(run)
|
|
547
|
+
output.show_failure_text(failure_text, headline)
|
|
548
|
+
if (self.session_registry_controller.clone_dialog_by_session.get(run.intent.session_id) is not dialog or not isValid(dialog) or dialog.running or dialog.operation_id != run.operation_id
|
|
549
|
+
or dialog.status_label.tone is not ResultTone.FAILURE):
|
|
550
|
+
return
|
|
551
|
+
account = transfer_account(run.selection, run.intent.endpoint, self.authentication.missing_secret_namespace_set)
|
|
552
|
+
dialog.show_failure(failure_summary(run.diagnostic, account), explained=True)
|
|
553
|
+
|
|
554
|
+
def _show_partial_clone_needs_reconnect(self, run: CloneRun, dialog: CloneDialog) -> None:
|
|
555
|
+
"""Say that the account refused for a submodule needs a reconnect, in the clone's console and in the clone form.
|
|
556
|
+
|
|
557
|
+
The form keeps its partial state (Retry submodules, Open repository) and is only rewritten while it still shows
|
|
558
|
+
that same partial clone.
|
|
559
|
+
"""
|
|
560
|
+
output = self.clone_output_by_id.get(run.operation_id)
|
|
561
|
+
if output is not None and isValid(output) and not output.running:
|
|
562
|
+
failure_text, headline = self._clone_console_failure(run)
|
|
563
|
+
output.show_failure_text(failure_text, headline)
|
|
564
|
+
if (self.session_registry_controller.clone_dialog_by_session.get(run.intent.session_id) is not dialog or not isValid(dialog) or dialog.running or dialog.operation_id != run.operation_id
|
|
565
|
+
or not dialog.partial or dialog.status_label.tone is not ResultTone.FAILURE or run.diagnostic is None):
|
|
566
|
+
return
|
|
567
|
+
dialog.show_partial_submodules(failure_summary(run.diagnostic, self._submodule_transfer_account(run)), run.validation_status == OperationStatus.SUCCESS.value,
|
|
568
|
+
explained=bool(run.checkout_refusal_text))
|
|
569
|
+
|
|
570
|
+
def keep_clone_console(self, operation_id: str, owner: QWidget, *, show: bool) -> None:
|
|
571
|
+
"""Move a clone's console under the window that stays on screen once the clone form closes.
|
|
572
|
+
|
|
573
|
+
The console is a child of the clone form, and the form goes away with its Open repository window: left there,
|
|
574
|
+
the console and the clone's output would disappear with them. It keeps its own window flags so it stays a
|
|
575
|
+
separate window. ``setParent`` hides a window, so a console that was on screen is shown again, and so is one the
|
|
576
|
+
caller asks to show.
|
|
577
|
+
"""
|
|
578
|
+
output = self.clone_output_by_id.get(operation_id)
|
|
579
|
+
if output is None or not isValid(output):
|
|
580
|
+
return
|
|
581
|
+
visible = output.isVisible()
|
|
582
|
+
output.setParent(owner, output.windowFlags())
|
|
583
|
+
if not visible and not show:
|
|
584
|
+
return
|
|
585
|
+
output.show()
|
|
586
|
+
output.raise_()
|
|
587
|
+
|
|
588
|
+
def _open_clone_console(self, session_id: str, run: CloneRun, dialog: CloneDialog) -> None:
|
|
589
|
+
"""Open the standard operation console live for a clone or its submodule retry, as Fetch and Push do.
|
|
590
|
+
|
|
591
|
+
A retry reuses the clone's console, even one the user already closed, since the retry writes to the same trace.
|
|
592
|
+
The clone service closes that trace itself once nothing can write to it anymore, so the console does not.
|
|
593
|
+
"""
|
|
594
|
+
output = self.clone_output_by_id.get(run.operation_id)
|
|
595
|
+
if output is None or not isValid(output):
|
|
596
|
+
output = OperationDialog(run.trace, CLONE_CONSOLE_TITLE, dialog, preference=self.preference, console_operation=ConsoleOperation.CLONE)
|
|
597
|
+
self.clone_output_by_id[run.operation_id] = output
|
|
598
|
+
operation_id = run.operation_id
|
|
599
|
+
output.abort_requested.connect(lambda: self._abort_clone_console(session_id, operation_id))
|
|
600
|
+
output.set_running()
|
|
601
|
+
open_window_modal(output, keeps_window_buttons=True)
|
|
602
|
+
|
|
603
|
+
def _abort_clone_console(self, session_id: str, operation_id: str) -> None:
|
|
604
|
+
"""Stop a clone from its console's Abort exactly as the form's Cancel clone does, or directly once no form shows it."""
|
|
605
|
+
dialog = self.session_registry_controller.clone_dialog_by_session.get(session_id)
|
|
606
|
+
if dialog is not None and dialog.operation_id == operation_id:
|
|
607
|
+
self.cancel_clone(session_id)
|
|
608
|
+
return
|
|
609
|
+
self.clone.cancel(operation_id)
|
|
610
|
+
|
|
611
|
+
def _clone_console_failure(self, run: CloneRun) -> tuple[str, str]:
|
|
612
|
+
"""Return the explanation and the headline the clone's console shows in bold red, both empty when there is no failure to explain.
|
|
613
|
+
|
|
614
|
+
As for Fetch and Push, the headline names the address and the account used: the clone's own, or for a partial
|
|
615
|
+
clone the account used for the submodule that could not be fetched.
|
|
616
|
+
"""
|
|
617
|
+
if run.cancelled or run.diagnostic is None or run.recursive_status == OperationStatus.SUCCESS.value:
|
|
618
|
+
return "", ""
|
|
619
|
+
if run.incomplete_repository:
|
|
620
|
+
account = self._submodule_transfer_account(run)
|
|
621
|
+
else:
|
|
622
|
+
account = transfer_account(run.selection, run.intent.endpoint, self.authentication.missing_secret_namespace_set)
|
|
623
|
+
return failure_summary(run.diagnostic), failure_headline(run.diagnostic, account)
|
|
624
|
+
|
|
625
|
+
def lookup_refused_account(self, selection: AccountSelection | None, diagnostic: PreparedDiagnostic | None, show_needs_reconnect: Callable[[], None]) -> None:
|
|
626
|
+
"""After a transfer an account's authentication was refused for, check in the background whether that account's secret is gone.
|
|
627
|
+
|
|
628
|
+
Git and Git Credential Manager erase a refused secret by themselves. When the check finds it gone,
|
|
629
|
+
``show_needs_reconnect`` rewrites the views still showing that failure; an open Settings window is marked either way.
|
|
630
|
+
Nothing is checked without an account or for another kind of failure.
|
|
631
|
+
"""
|
|
632
|
+
if selection is None or diagnostic is None or not authentication_refused(diagnostic):
|
|
633
|
+
return
|
|
634
|
+
|
|
635
|
+
def answered(lookup: SecretLookup) -> None:
|
|
636
|
+
"""Mark the account in Settings, and in the failure message once its secret is known to be gone."""
|
|
637
|
+
self.account_controller.show_account_status(lookup)
|
|
638
|
+
if selection.namespace in lookup.missing_namespace_set:
|
|
639
|
+
show_needs_reconnect()
|
|
640
|
+
|
|
641
|
+
self.authentication.lookup_secrets((selection,), answered)
|
|
642
|
+
|
|
643
|
+
def _submodule_account_selection(self, run: CloneRun) -> AccountSelection | None:
|
|
644
|
+
"""Return the account used for the submodule a partial clone could not fetch, or None when no submodule asked for authentication or no account was used.
|
|
645
|
+
|
|
646
|
+
A retry uses the account the user chose for the submodule. A first attempt used the account Git was given for
|
|
647
|
+
that address: the parent's account for the same address, otherwise the service default. Raises ValueError when
|
|
648
|
+
that default account can no longer be read.
|
|
649
|
+
"""
|
|
650
|
+
target = run.authentication_target
|
|
651
|
+
if target is None:
|
|
652
|
+
return None
|
|
653
|
+
if run.child_selection is not None:
|
|
654
|
+
return run.child_selection
|
|
655
|
+
if run.selection is not None and endpoint_origin(target.endpoint) == run.selection.origin:
|
|
656
|
+
return run.selection
|
|
657
|
+
return self.authentication.account_for_endpoint(target.endpoint)
|
|
658
|
+
|
|
659
|
+
def _submodule_transfer_account(self, run: CloneRun) -> TransferAccount | None:
|
|
660
|
+
"""Name the address and account used for the submodule a partial clone could not fetch, or None when no submodule asked for authentication.
|
|
661
|
+
|
|
662
|
+
The account is marked as needing a reconnect once a check found its secret gone. When that account can no
|
|
663
|
+
longer be read, nothing is claimed rather than saying no account is connected.
|
|
664
|
+
"""
|
|
665
|
+
target = run.authentication_target
|
|
666
|
+
if target is None:
|
|
667
|
+
return None
|
|
668
|
+
try:
|
|
669
|
+
selection = self._submodule_account_selection(run)
|
|
670
|
+
except ValueError:
|
|
671
|
+
return None
|
|
672
|
+
return transfer_account(selection, target.endpoint, self.authentication.missing_secret_namespace_set)
|
|
673
|
+
|
|
674
|
+
def _retry_clone_submodules(self, session_id: str) -> None:
|
|
675
|
+
"""Resume the captured partial clone without asking for another URL or destination."""
|
|
676
|
+
dialog = self.session_registry_controller.clone_dialog_by_session.get(session_id)
|
|
677
|
+
if dialog is None or dialog.running or dialog.operation_id is None or session_id not in self.session_registry_controller.welcome_by_session:
|
|
678
|
+
return
|
|
679
|
+
run = self.clone.run_by_id[dialog.operation_id]
|
|
680
|
+
if self._clone_form_signing_in(dialog):
|
|
681
|
+
dialog.status_label.show_failure("Finish signing in successfully before retrying submodules.")
|
|
682
|
+
return
|
|
683
|
+
if run.authentication_target is not None:
|
|
684
|
+
namespace = dialog.account_choice()
|
|
685
|
+
child_selection = None
|
|
686
|
+
if namespace is not None:
|
|
687
|
+
child_selection = self._clone_listed_account(dialog, namespace)
|
|
688
|
+
if child_selection is None:
|
|
689
|
+
dialog.status_label.show_failure("Choose a confirmed account for this submodule before retrying.")
|
|
690
|
+
return
|
|
691
|
+
run.child_selection = child_selection
|
|
692
|
+
generation = self.session_registry_controller.clone_generation_by_session.get(session_id, 0) + 1
|
|
693
|
+
self.session_registry_controller.clone_generation_by_session[session_id] = generation
|
|
694
|
+
run.intent = replace(run.intent, generation=generation)
|
|
695
|
+
run.cancelled = False
|
|
696
|
+
dialog.set_running(True)
|
|
697
|
+
# The retry writes to the clone's own trace, so its output continues below the clone's in the same console.
|
|
698
|
+
self._open_clone_console(session_id, run, dialog)
|
|
699
|
+
self.clone.retry_submodules(run, self.repository.git_filename)
|
|
700
|
+
|
|
701
|
+
def _retry_clone_open(self, session_id: str) -> None:
|
|
702
|
+
"""Capture a fresh explicit retry for the same completed clone without repeating Git clone."""
|
|
703
|
+
dialog = self.session_registry_controller.clone_dialog_by_session.get(session_id)
|
|
704
|
+
if dialog is None or dialog.running or dialog.operation_id is None or session_id not in self.session_registry_controller.welcome_by_session:
|
|
705
|
+
return
|
|
706
|
+
run = self.clone.run_by_id[dialog.operation_id]
|
|
707
|
+
generation = self.session_registry_controller.clone_generation_by_session.get(session_id, 0) + 1
|
|
708
|
+
self.session_registry_controller.clone_generation_by_session[session_id] = generation
|
|
709
|
+
run.intent = replace(run.intent, generation=generation)
|
|
710
|
+
run.cancelled = False
|
|
711
|
+
run.validation_status = "not_started"
|
|
712
|
+
self._open_clone(session_id)
|
|
713
|
+
|
|
714
|
+
def _open_clone(self, session_id: str) -> None:
|
|
715
|
+
"""Pass a completed owned destination through the existing repository reader.
|
|
716
|
+
|
|
717
|
+
A bare clone is never opened: it has no working tree to show in a repository window.
|
|
718
|
+
"""
|
|
719
|
+
dialog = self.session_registry_controller.clone_dialog_by_session.get(session_id)
|
|
720
|
+
if dialog is None or dialog.operation_id is None:
|
|
721
|
+
return
|
|
722
|
+
run = self.clone.run_by_id[dialog.operation_id]
|
|
723
|
+
if run.intent.options.bare or (run.clone_status != OperationStatus.SUCCESS.value and not run.incomplete_repository) or run.cancelled or not self.operation_policy_controller.clone_is_current(run.intent):
|
|
724
|
+
return
|
|
725
|
+
try:
|
|
726
|
+
run.destination.verify()
|
|
727
|
+
except (OSError, ValueError) as error:
|
|
728
|
+
diagnostic = Diagnostic("Open repository", run.destination.destination_dir, self.repository.git_filename, [], str(error)).prepare()
|
|
729
|
+
self.clone.open_result(run.operation_id, False, diagnostic)
|
|
730
|
+
dialog.show_failure(failure_summary(diagnostic), partial=True)
|
|
731
|
+
return
|
|
732
|
+
if run.validation_status != OperationStatus.SUCCESS.value:
|
|
733
|
+
dialog.set_running(True)
|
|
734
|
+
|
|
735
|
+
def validated() -> None:
|
|
736
|
+
"""Retry readiness checks without repeating the native clone command."""
|
|
737
|
+
if run.cancelled and self.session_registry_controller.clone_dialog_by_session.get(session_id) is dialog:
|
|
738
|
+
dialog.show_cancelled(OPENING_CANCELLED_TEXT, partial=True)
|
|
739
|
+
return
|
|
740
|
+
if not self.operation_policy_controller.clone_is_current(run.intent) or run.cancelled:
|
|
741
|
+
return
|
|
742
|
+
if run.validation_status == OperationStatus.SUCCESS.value:
|
|
743
|
+
self._open_clone(session_id)
|
|
744
|
+
else:
|
|
745
|
+
dialog.show_failure(failure_summary(run.diagnostic), partial=True)
|
|
746
|
+
|
|
747
|
+
self.clone.validate_repository(run, self.repository.git_filename, validated)
|
|
748
|
+
return
|
|
749
|
+
run.open_status = "running"
|
|
750
|
+
dialog.set_running(True)
|
|
751
|
+
dialog.status_label.setText("Repository cloned. Opening repository…" if run.recursive_status == OperationStatus.SUCCESS.value else "Opening preserved repository; some submodules are still incomplete…")
|
|
752
|
+
self.open_requested.emit(session_id, run.destination.destination_dir)
|
|
753
|
+
|
|
754
|
+
def cancel_clone(self, session_id: str) -> None:
|
|
755
|
+
"""Invalidate the form immediately and stop its native process without deleting files."""
|
|
756
|
+
self._cancel_clone_authentication(session_id)
|
|
757
|
+
self.session_registry_controller.clone_generation_by_session[session_id] = self.session_registry_controller.clone_generation_by_session.get(session_id, 0) + 1
|
|
758
|
+
dialog = self.session_registry_controller.clone_dialog_by_session.get(session_id)
|
|
759
|
+
if dialog is not None and dialog.operation_id is not None:
|
|
760
|
+
run = self.clone.run_by_id[dialog.operation_id]
|
|
761
|
+
# Read before cancelling: a clone still waiting in the queue finishes inside cancel() and shows its own
|
|
762
|
+
# cancellation text, which the opening-cancelled text must not replace.
|
|
763
|
+
clone_was_running = run.has_running_step()
|
|
764
|
+
self.clone.cancel(dialog.operation_id)
|
|
765
|
+
if not clone_was_running:
|
|
766
|
+
dialog.show_cancelled(OPENING_CANCELLED_TEXT, partial=run.clone_status == OperationStatus.SUCCESS.value)
|
|
767
|
+
if session_id in self.session_registry_controller.welcome_by_session and session_id in self.session_registry_controller.identity_by_session:
|
|
768
|
+
self.open_cancel_requested.emit(session_id)
|
|
769
|
+
|
|
770
|
+
def preference_changed_for_clone_forms(self, changed_value_by_key: dict[str, object]) -> None:
|
|
771
|
+
"""Show again, in every open clone form, the account choice a change of the shared preferences touched."""
|
|
772
|
+
# A sign-in, a block save or another window may have changed the accounts; every clone form shows them without
|
|
773
|
+
# losing what the user chose.
|
|
774
|
+
if AUTHENTICATION_PREFERENCE_KEY in changed_value_by_key:
|
|
775
|
+
for session_id, clone_dialog in tuple(self.session_registry_controller.clone_dialog_by_session.items()):
|
|
776
|
+
self.refresh_clone_account(session_id, clone_dialog.account_choice())
|