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,813 @@
|
|
|
1
|
+
"""Exchange branches with the remotes of a repository from its windows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclasses import replace
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from PySide6.QtCore import QObject, QTimer
|
|
10
|
+
from shiboken6 import isValid
|
|
11
|
+
|
|
12
|
+
from gitpane.account.authentication import transfer_account
|
|
13
|
+
from gitpane.config import LOCAL_PREFIX, TAG_PREFIX, ConsoleOperation, SharedText
|
|
14
|
+
from gitpane.controller.refresh_controller import RefreshScope
|
|
15
|
+
from gitpane.history.file_history import WindowCapability
|
|
16
|
+
from gitpane.operation.diagnostic import Diagnostic
|
|
17
|
+
from gitpane.operation.operation_status import OperationStatus
|
|
18
|
+
from gitpane.operation.repository_operation import NO_LONGER_AUTHORIZED_TEXT, OperationAction, OperationIntent
|
|
19
|
+
from gitpane.preference.preference_store import FETCH_DEFAULT_PREFERENCE_KEY, FetchMode, fetch_mode_preference
|
|
20
|
+
from gitpane.process.process_runner import READ_PROFILE
|
|
21
|
+
from gitpane.reference.reference_edit import reference_signature
|
|
22
|
+
from gitpane.system.path_identity import canonical_path
|
|
23
|
+
from gitpane.transport.branch_exchange import TransportRequest
|
|
24
|
+
from gitpane.transport.push_tracking import PushTrackingChoice, push_tracking_choice, save_push_tracking_choice, tracking_question_applies
|
|
25
|
+
from gitpane.transport.refusal_notice import account_notice, failure_headline, failure_summary, server_refusal_headline
|
|
26
|
+
from gitpane.transport.remote_config import fetch_remote_name
|
|
27
|
+
from gitpane.ui.console.operation_dialog import OperationDialog
|
|
28
|
+
from gitpane.ui.transport.push_dialog import PushDialog
|
|
29
|
+
from gitpane.ui.transport.push_rejection_prompt import PushRejectionPrompt
|
|
30
|
+
from gitpane.ui.transport.push_tracking_question import PushTrackingAnswer, ask_push_tracking
|
|
31
|
+
from gitpane.ui.transport.remote_dialog import RemoteDialog
|
|
32
|
+
from gitpane.ui.window_control.dialog_modality import open_window_modal
|
|
33
|
+
from gitpane.worktree.command_sequence import FileTask, GitCommand
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from collections.abc import Callable, Generator, Sequence
|
|
37
|
+
|
|
38
|
+
from PySide6.QtWidgets import QMessageBox, QWidget
|
|
39
|
+
|
|
40
|
+
from gitpane.account.authentication import AuthenticationService
|
|
41
|
+
from gitpane.commit.commit_push import CommitPushIntent, CommitPushService
|
|
42
|
+
from gitpane.commit.commit_write import CommitRequest
|
|
43
|
+
from gitpane.controller.account_controller import AccountController
|
|
44
|
+
from gitpane.controller.clone_controller import CloneController
|
|
45
|
+
from gitpane.controller.console_registry import ConsoleRegistry
|
|
46
|
+
from gitpane.controller.operation_policy import OperationPolicy
|
|
47
|
+
from gitpane.controller.reference_read_controller import ReferenceReadController
|
|
48
|
+
from gitpane.controller.refresh_controller import RefreshController
|
|
49
|
+
from gitpane.controller.session_registry import SessionRegistry
|
|
50
|
+
from gitpane.controller.staging_session_registry import StagingSessionRegistry
|
|
51
|
+
from gitpane.history.repository import GitRepository
|
|
52
|
+
from gitpane.operation.diagnostic import DiagnosticLog
|
|
53
|
+
from gitpane.operation.repository_operation import RepositoryOperationService
|
|
54
|
+
from gitpane.preference.preference_store import PreferenceStore
|
|
55
|
+
from gitpane.transport.branch_exchange import TransportOutcome, TransportRun, TransportService
|
|
56
|
+
from gitpane.transport.remote import RemoteService
|
|
57
|
+
from gitpane.transport.remote_config import RemoteSnapshot
|
|
58
|
+
from gitpane.ui.transport.push_dialog import PushWindowRequest
|
|
59
|
+
from gitpane.worktree.staging import StagingSession
|
|
60
|
+
from gitpane.worktree.worktree_service import WorktreeService
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TransportController(QObject):
|
|
64
|
+
"""Fetch, Push and Manage remotes of every repository window: their forms, the consoles of their transfers, the Push a Commit & push hands over, and what the end of a transfer shows and reads again in the windows of the same repository."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, *, session_registry_controller: SessionRegistry, operation_policy_controller: OperationPolicy, staging_session_registry_controller: StagingSessionRegistry, console_registry_controller: ConsoleRegistry,
|
|
67
|
+
reference_read_controller: ReferenceReadController, refresh_controller: RefreshController, account_controller: AccountController, clone_controller: CloneController, preference: PreferenceStore, repository: GitRepository,
|
|
68
|
+
worktree_service: WorktreeService, repository_operation: RepositoryOperationService, remote: RemoteService, authentication: AuthenticationService, transport: TransportService, commit_push: CommitPushService,
|
|
69
|
+
diagnostic_log: DiagnosticLog | None, parent: QObject | None = None) -> None:
|
|
70
|
+
"""Receive the services and the lower controllers a transfer works with; the wiring builds it after the staging controller, and the repository windows, the preparation sessions and the commit-and-push service reach it through the wiring."""
|
|
71
|
+
super().__init__(parent)
|
|
72
|
+
self.session_registry_controller = session_registry_controller
|
|
73
|
+
self.operation_policy_controller = operation_policy_controller
|
|
74
|
+
self.console_registry_controller = console_registry_controller
|
|
75
|
+
self.reference_read_controller = reference_read_controller
|
|
76
|
+
self.refresh_controller = refresh_controller
|
|
77
|
+
self.account_controller = account_controller
|
|
78
|
+
self.clone_controller = clone_controller
|
|
79
|
+
self.preference = preference
|
|
80
|
+
self.repository = repository
|
|
81
|
+
self.worktree_service = worktree_service
|
|
82
|
+
self.remote = remote
|
|
83
|
+
self.authentication = authentication
|
|
84
|
+
self.transport = transport
|
|
85
|
+
self.commit_push = commit_push
|
|
86
|
+
self.diagnostic_log = diagnostic_log
|
|
87
|
+
self.staging_session_registry_controller = staging_session_registry_controller
|
|
88
|
+
self.repository_operation = repository_operation
|
|
89
|
+
self.push_dialog_by_session: dict[str, PushDialog] = {}
|
|
90
|
+
self.push_rejection_prompt_by_session: dict[str, PushRejectionPrompt] = {}
|
|
91
|
+
self.push_tracking_question_by_session: dict[str, QMessageBox] = {}
|
|
92
|
+
self.fetch_read_by_session: dict[str, str] = {}
|
|
93
|
+
self.remote_dialog_by_session: dict[str, RemoteDialog] = {}
|
|
94
|
+
|
|
95
|
+
def prepare_push_target(self, session_id: str, dialog: PushDialog) -> None:
|
|
96
|
+
"""Bind the displayed destination to its current account and tools without contacting it."""
|
|
97
|
+
if dialog.loading or dialog.snapshot is None or dialog.closed or not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
|
|
98
|
+
return
|
|
99
|
+
endpoint = dialog.chosen_endpoint()
|
|
100
|
+
name = dialog.remote_selector.currentData() if dialog.remote_radio.isChecked() else ""
|
|
101
|
+
dialog.push_target = None
|
|
102
|
+
dialog.target_error = ""
|
|
103
|
+
try:
|
|
104
|
+
dialog.push_target = self.transport.capture_push_target(dialog.snapshot, name or "", endpoint)
|
|
105
|
+
except (ValueError, OSError, RuntimeError) as error:
|
|
106
|
+
dialog.target_error = str(error)
|
|
107
|
+
dialog.update_action()
|
|
108
|
+
|
|
109
|
+
def read_push(self, session_id: str, dialog: PushDialog) -> None:
|
|
110
|
+
"""Read local sources and effective destinations without contacting a remote server."""
|
|
111
|
+
if not isValid(dialog) or dialog.closed or dialog.busy or not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
|
|
112
|
+
return
|
|
113
|
+
session = self.session_registry_controller.session_by_id[session_id]
|
|
114
|
+
generation = self.session_registry_controller.generation_by_session[session_id]
|
|
115
|
+
git_filename = self.repository.git_filename
|
|
116
|
+
dialog.loading = True
|
|
117
|
+
read_id = uuid.uuid4().hex
|
|
118
|
+
dialog.read_id = read_id
|
|
119
|
+
dialog.update_action()
|
|
120
|
+
|
|
121
|
+
def read() -> Generator:
|
|
122
|
+
"""Capture actual HEAD and tags alongside the remote configuration."""
|
|
123
|
+
reference_state = yield FileTask(lambda: reference_signature(session.repository_context))
|
|
124
|
+
head = yield GitCommand(["symbolic-ref", "--quiet", "HEAD"], accepted_codes=(0, 1), profile=READ_PROFILE)
|
|
125
|
+
tags = yield GitCommand(["for-each-ref", "--format=%(refname)%00%(objectname)", TAG_PREFIX], profile=READ_PROFILE)
|
|
126
|
+
snapshot = yield from self.remote.capture(session.repository_context, git_filename, generation)
|
|
127
|
+
if reference_state != snapshot.reference_state:
|
|
128
|
+
raise ValueError("References changed while reading Push. Reread destination.")
|
|
129
|
+
tag_oid_by_ref = {}
|
|
130
|
+
for record in tags.splitlines():
|
|
131
|
+
full_ref, oid = record.decode("utf-8", "surrogateescape").split("\0")
|
|
132
|
+
tag_oid_by_ref[full_ref] = oid
|
|
133
|
+
return snapshot, head.decode("utf-8", "surrogateescape").strip(), tag_oid_by_ref
|
|
134
|
+
|
|
135
|
+
def complete(value: object, error: Exception | None) -> None:
|
|
136
|
+
"""Discard closed or stale responses while retaining a recoverable visible draft."""
|
|
137
|
+
if not isValid(dialog) or dialog.closed or dialog.read_id != read_id or not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
|
|
138
|
+
return
|
|
139
|
+
if generation != self.session_registry_controller.generation_by_session.get(session_id):
|
|
140
|
+
dialog.needs_reread = True
|
|
141
|
+
dialog.show_error("The repository view changed. Reread destination before sending.")
|
|
142
|
+
return
|
|
143
|
+
if error is not None:
|
|
144
|
+
dialog.needs_reread = True
|
|
145
|
+
dialog.show_error(str(error))
|
|
146
|
+
return
|
|
147
|
+
dialog.populate(*value)
|
|
148
|
+
self.prepare_push_target(session_id, dialog)
|
|
149
|
+
|
|
150
|
+
self.worktree_service.run(session.repository_context, git_filename, read(), complete)
|
|
151
|
+
|
|
152
|
+
def _reread_push_windows(self, common_git_dir: str) -> None:
|
|
153
|
+
"""Reread every idle Push window of a repository after a Fetch, so its remote branches and fetched state are current.
|
|
154
|
+
|
|
155
|
+
After a refused push, the user fetches from the rejection prompt and pushes again from the same window.
|
|
156
|
+
"""
|
|
157
|
+
for session_id, dialog in tuple(self.push_dialog_by_session.items()):
|
|
158
|
+
session = self.session_registry_controller.session_by_id.get(session_id)
|
|
159
|
+
if session is None or session.is_closed or not isValid(dialog) or dialog.closed or dialog.busy:
|
|
160
|
+
continue
|
|
161
|
+
if canonical_path(session.repository_context.common_git_dir) == common_git_dir:
|
|
162
|
+
self.read_push(session_id, dialog)
|
|
163
|
+
|
|
164
|
+
def _start_push(self, session_id: str, request: TransportRequest) -> TransportRun:
|
|
165
|
+
"""Send one Push request and show its console, which Abort can stop like Fetch."""
|
|
166
|
+
operation = request.operation
|
|
167
|
+
run = self.transport.push(request)
|
|
168
|
+
output_dialog = OperationDialog(run.trace, "Push", self.session_registry_controller.window_by_session[session_id], preference=self.preference, console_operation=ConsoleOperation.PUSH)
|
|
169
|
+
self.console_registry_controller.transport_dialog_by_id[operation.operation_id] = output_dialog
|
|
170
|
+
output_dialog.abort_requested.connect(lambda: self.transport.cancel(operation.operation_id))
|
|
171
|
+
output_dialog.finished.connect(run.trace.close)
|
|
172
|
+
output_dialog.set_running()
|
|
173
|
+
open_window_modal(output_dialog, keeps_window_buttons=True)
|
|
174
|
+
return run
|
|
175
|
+
|
|
176
|
+
def _show_push_rejection(self, run: TransportRun, outcome: TransportOutcome) -> None:
|
|
177
|
+
"""Explain a Push refused because the server branch holds commits it would replace, and offer Fetch or Force push with lease.
|
|
178
|
+
|
|
179
|
+
The prompt opens after every other reaction to the result (console, Push window, Commit & push follow-up), so it
|
|
180
|
+
is the newest window and answers first. A new refusal replaces an older prompt of the same repository window.
|
|
181
|
+
Whatever the answer (Force push with lease, Fetch, Cancel, Escape or the title-bar close), the refused push
|
|
182
|
+
console has done its job, so it is settled and closes unless Keep dialog open is checked.
|
|
183
|
+
"""
|
|
184
|
+
session_id = run.request.operation.session_id
|
|
185
|
+
if outcome.rejection is None or not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
|
|
186
|
+
return
|
|
187
|
+
previous = self.push_rejection_prompt_by_session.get(session_id)
|
|
188
|
+
if previous is not None and isValid(previous):
|
|
189
|
+
previous.reject()
|
|
190
|
+
prompt = PushRejectionPrompt(outcome.rejection, self.session_registry_controller.window_by_session[session_id])
|
|
191
|
+
remote_name = outcome.rejection.remote_name
|
|
192
|
+
prompt.fetch_requested.connect(lambda: self.fetch(session_id, (FetchMode.FETCH, remote_name, None)))
|
|
193
|
+
prompt.force_requested.connect(lambda: self._force_push_with_lease(run))
|
|
194
|
+
prompt.finished.connect(lambda _result: self.console_registry_controller.settle_answered_console(run.request.operation.operation_id))
|
|
195
|
+
self.push_rejection_prompt_by_session[session_id] = prompt
|
|
196
|
+
open_window_modal(prompt)
|
|
197
|
+
|
|
198
|
+
def _force_push_with_lease(self, run: TransportRun) -> None:
|
|
199
|
+
"""Send a refused branch again as a force push with lease on the last fetched state, chosen from the rejection prompt.
|
|
200
|
+
|
|
201
|
+
The local capture is read again first; the transport then checks that the branch and destination did not change.
|
|
202
|
+
A Commit & push keeps going through its own service so the created commit is never committed twice. The tracking
|
|
203
|
+
answer given before the refused Push is kept without asking again, so the branch is linked only if that answer
|
|
204
|
+
asked for it and the forced Push is verified.
|
|
205
|
+
"""
|
|
206
|
+
request = run.request
|
|
207
|
+
session_id = request.operation.session_id
|
|
208
|
+
if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
|
|
209
|
+
return
|
|
210
|
+
session = self.session_registry_controller.session_by_id[session_id]
|
|
211
|
+
generation = self.session_registry_controller.generation_by_session[session_id]
|
|
212
|
+
git_filename = self.repository.git_filename
|
|
213
|
+
intent = self.commit_push.intent_by_push_id.get(request.operation.operation_id)
|
|
214
|
+
|
|
215
|
+
def refused(message: str) -> None:
|
|
216
|
+
"""Show why the force push could not start, in the Push window when it is open, else in the status bar."""
|
|
217
|
+
dialog = self.push_dialog_by_session.get(session_id)
|
|
218
|
+
if dialog is not None and isValid(dialog) and not dialog.closed:
|
|
219
|
+
dialog.show_error(message)
|
|
220
|
+
return
|
|
221
|
+
window = self.session_registry_controller.window_by_session.get(session_id)
|
|
222
|
+
if window is not None and isValid(window):
|
|
223
|
+
window.statusBar().showMessage(message)
|
|
224
|
+
|
|
225
|
+
def captured(snapshot: RemoteSnapshot | None, error: Exception | None) -> None:
|
|
226
|
+
"""Start the force push under the current authority, or explain why it cannot start."""
|
|
227
|
+
if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
|
|
228
|
+
return
|
|
229
|
+
if generation != self.session_registry_controller.generation_by_session.get(session_id):
|
|
230
|
+
refused("The repository view changed. Push again to choose Force push with lease.")
|
|
231
|
+
return
|
|
232
|
+
if error is not None:
|
|
233
|
+
refused(str(error))
|
|
234
|
+
return
|
|
235
|
+
try:
|
|
236
|
+
target = self.transport.capture_push_target(snapshot, request.remote_name, request.endpoint)
|
|
237
|
+
except (ValueError, OSError, RuntimeError) as failure:
|
|
238
|
+
refused(str(failure))
|
|
239
|
+
return
|
|
240
|
+
if intent is not None:
|
|
241
|
+
forced = self.commit_push.retry_push(intent, snapshot, request.remote_name, request.endpoint, request.destination_ref, target, True, add_tracking=request.add_tracking)
|
|
242
|
+
else:
|
|
243
|
+
operation = OperationIntent.capture(uuid.uuid4().hex, snapshot.context, snapshot.git_filename, generation=snapshot.generation, session_id=session_id,
|
|
244
|
+
operation="Push", action=OperationAction.REMOTE_WRITE, targets=(request.source_ref, request.destination_ref, request.endpoint))
|
|
245
|
+
forced = self._start_push(session_id, TransportRequest(operation, remote_name=request.remote_name, snapshot=snapshot, mode="push", source_ref=request.source_ref,
|
|
246
|
+
source_oid=request.source_oid, destination_ref=request.destination_ref, endpoint=request.endpoint,
|
|
247
|
+
force=True, push_target=target, add_tracking=request.add_tracking))
|
|
248
|
+
if forced is None:
|
|
249
|
+
refused("This commit's repository is no longer authorized for Push.")
|
|
250
|
+
return
|
|
251
|
+
dialog = self.push_dialog_by_session.get(session_id)
|
|
252
|
+
if dialog is not None and isValid(dialog) and not dialog.closed:
|
|
253
|
+
dialog.follow_operation(forced.request.operation.operation_id)
|
|
254
|
+
|
|
255
|
+
self.worktree_service.run(session.repository_context, git_filename, self.remote.capture(session.repository_context, git_filename, generation), captured)
|
|
256
|
+
|
|
257
|
+
def fetch(self, session_id: str, choice: tuple | None = None) -> OperationDialog | None:
|
|
258
|
+
"""Capture a management intention and show all preparation and Fetch output.
|
|
259
|
+
|
|
260
|
+
``choice`` is the Fetch mode, the remote and its capture picked in a menu; without it the saved default mode is used.
|
|
261
|
+
"""
|
|
262
|
+
if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
|
|
263
|
+
return None
|
|
264
|
+
session = self.session_registry_controller.session_by_id[session_id]
|
|
265
|
+
for operation_id, run in self.transport.run_by_id.items():
|
|
266
|
+
if run.request.operation.common_git_dir == canonical_path(session.repository_context.common_git_dir) and operation_id not in self.transport.outcome_by_id:
|
|
267
|
+
return self.console_registry_controller.transport_dialog_by_id.get(operation_id)
|
|
268
|
+
if session_id in self.fetch_read_by_session:
|
|
269
|
+
return None
|
|
270
|
+
mode, remote_name, snapshot = choice if choice is not None else (fetch_mode_preference(self.preference.read(FETCH_DEFAULT_PREFERENCE_KEY)), "", None)
|
|
271
|
+
if mode == FetchMode.FETCH and snapshot is None and not remote_name:
|
|
272
|
+
self.read_fetch_choice(session_id, launch=True)
|
|
273
|
+
return None
|
|
274
|
+
if mode == FetchMode.FETCH and not remote_name:
|
|
275
|
+
return None
|
|
276
|
+
operation = OperationIntent.capture(uuid.uuid4().hex, session.repository_context, self.repository.git_filename,
|
|
277
|
+
session_id=session_id, generation=self.session_registry_controller.generation_by_session[session_id], operation="Fetch", action=OperationAction.REMOTE_WRITE)
|
|
278
|
+
run = self.transport.fetch(TransportRequest(operation, mode == FetchMode.PRUNE_ALL, remote_name, snapshot))
|
|
279
|
+
dialog = OperationDialog(run.trace, "Fetch", self.session_registry_controller.window_by_session[session_id], preference=self.preference, console_operation=ConsoleOperation.FETCH)
|
|
280
|
+
self.console_registry_controller.transport_dialog_by_id[operation.operation_id] = dialog
|
|
281
|
+
dialog.abort_requested.connect(lambda: self.transport.cancel(operation.operation_id))
|
|
282
|
+
dialog.finished.connect(run.trace.close)
|
|
283
|
+
dialog.set_running()
|
|
284
|
+
open_window_modal(dialog, keeps_window_buttons=True)
|
|
285
|
+
for related_window in self.session_registry_controller.window_by_session.values():
|
|
286
|
+
if canonical_path(related_window.context.common_git_dir) == operation.common_git_dir:
|
|
287
|
+
related_window.set_fetch_busy(True)
|
|
288
|
+
return dialog
|
|
289
|
+
|
|
290
|
+
def fetch_busy_for(self, common_git_dir: str) -> bool:
|
|
291
|
+
"""Tell whether a Fetch or another transport that has not finished yet runs on this repository.
|
|
292
|
+
|
|
293
|
+
Every window of the repository blocks a second primary Fetch while one runs, including a window that has just
|
|
294
|
+
moved to it from another repository.
|
|
295
|
+
"""
|
|
296
|
+
canonical_git_dir = canonical_path(common_git_dir)
|
|
297
|
+
for operation_id, run in self.transport.run_by_id.items():
|
|
298
|
+
if run.request.operation.common_git_dir == canonical_git_dir and operation_id not in self.transport.outcome_by_id:
|
|
299
|
+
return True
|
|
300
|
+
return False
|
|
301
|
+
|
|
302
|
+
def read_fetch_choice(self, session_id: str, *, launch: bool = False) -> None:
|
|
303
|
+
"""Read destinations without network activity and bind later clicks to the displayed capture."""
|
|
304
|
+
if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE) or session_id in self.fetch_read_by_session:
|
|
305
|
+
return
|
|
306
|
+
window = self.session_registry_controller.window_by_session[session_id]
|
|
307
|
+
if window.fetch_busy:
|
|
308
|
+
return
|
|
309
|
+
session = self.session_registry_controller.session_by_id[session_id]
|
|
310
|
+
generation = self.session_registry_controller.generation_by_session[session_id]
|
|
311
|
+
git_filename = self.repository.git_filename
|
|
312
|
+
request_id = uuid.uuid4().hex
|
|
313
|
+
self.fetch_read_by_session[session_id] = request_id
|
|
314
|
+
branch_ref = window.branch_selector.currentData()
|
|
315
|
+
reference_snapshot = self.reference_read_controller.reference_snapshot_by_session.get(session_id)
|
|
316
|
+
if branch_ref is None and reference_snapshot is not None:
|
|
317
|
+
branch_ref = reference_snapshot.head_ref
|
|
318
|
+
window.show_fetch_choice(None, "", "Reading remotes…")
|
|
319
|
+
window.fetch_choice_action_by_mode[FetchMode.FETCH].setEnabled(False)
|
|
320
|
+
window.set_fetch_busy(True)
|
|
321
|
+
|
|
322
|
+
def complete(snapshot: RemoteSnapshot | None, error: Exception | None) -> None:
|
|
323
|
+
"""Keep stale or closed menu reads from selecting another repository or launching later."""
|
|
324
|
+
if self.fetch_read_by_session.get(session_id) != request_id:
|
|
325
|
+
return
|
|
326
|
+
self.fetch_read_by_session.pop(session_id, None)
|
|
327
|
+
if not isValid(window) or not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
|
|
328
|
+
return
|
|
329
|
+
window.set_fetch_busy(self.fetch_busy_for(window.context.common_git_dir))
|
|
330
|
+
if self.session_registry_controller.generation_by_session.get(session_id) != generation:
|
|
331
|
+
window.show_fetch_choice(None, "", "The repository view changed. Open the Fetch menu again.")
|
|
332
|
+
return
|
|
333
|
+
if error is not None:
|
|
334
|
+
diagnostic = Diagnostic("Read Fetch remotes", window.context.repository_dir, self.repository.git_filename, [], str(error)).prepare()
|
|
335
|
+
window.show_fetch_choice(None, "", diagnostic.cause)
|
|
336
|
+
return
|
|
337
|
+
name = fetch_remote_name(snapshot, branch_ref)
|
|
338
|
+
window.show_fetch_choice(snapshot, name)
|
|
339
|
+
if not launch:
|
|
340
|
+
return
|
|
341
|
+
if name:
|
|
342
|
+
self.fetch(session_id, (FetchMode.FETCH, name, snapshot))
|
|
343
|
+
else:
|
|
344
|
+
window.fetch_remote_menu.popup(window.fetch_button.mapToGlobal(window.fetch_button.rect().bottomLeft()))
|
|
345
|
+
|
|
346
|
+
def read() -> Generator:
|
|
347
|
+
"""Resolve an initial loading toolbar from native HEAD before capturing its remotes."""
|
|
348
|
+
nonlocal branch_ref
|
|
349
|
+
if branch_ref is None:
|
|
350
|
+
output = yield GitCommand(["symbolic-ref", "--quiet", "HEAD"], accepted_codes=(0, 1), profile=READ_PROFILE)
|
|
351
|
+
branch_ref = output.decode("utf-8", "surrogateescape").strip() or None
|
|
352
|
+
return (yield from self.remote.capture(session.repository_context, git_filename, generation))
|
|
353
|
+
|
|
354
|
+
self.worktree_service.run(session.repository_context, git_filename, read(), complete)
|
|
355
|
+
|
|
356
|
+
def save_fetch_default(self, session_id: str, mode: FetchMode) -> None:
|
|
357
|
+
"""Publish a non-secret default across windows only after its storage sync succeeds; the file receives the mode's saved word."""
|
|
358
|
+
if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
|
|
359
|
+
return
|
|
360
|
+
window = self.session_registry_controller.window_by_session[session_id]
|
|
361
|
+
try:
|
|
362
|
+
self.preference.update({FETCH_DEFAULT_PREFERENCE_KEY: mode.value})
|
|
363
|
+
except (OSError, ValueError, TypeError) as error:
|
|
364
|
+
diagnostic = Diagnostic("Save Fetch default", window.context.repository_dir, "", [], str(error)).prepare()
|
|
365
|
+
window.statusBar().showMessage(diagnostic.cause)
|
|
366
|
+
if self.diagnostic_log is not None:
|
|
367
|
+
self.diagnostic_log.write(diagnostic)
|
|
368
|
+
window.apply_fetch_preference(self.preference.value_by_key)
|
|
369
|
+
|
|
370
|
+
def transport_finished(self, outcome: object) -> None:
|
|
371
|
+
"""Publish terminal output and refresh related management views without losing reading state."""
|
|
372
|
+
run = self.transport.run_by_id[outcome.operation_id]
|
|
373
|
+
for window in self.session_registry_controller.window_by_session.values():
|
|
374
|
+
if canonical_path(window.context.common_git_dir) == run.request.operation.common_git_dir:
|
|
375
|
+
busy = any(active.request.operation.common_git_dir == run.request.operation.common_git_dir and operation_id not in self.transport.outcome_by_id
|
|
376
|
+
for operation_id, active in self.transport.run_by_id.items())
|
|
377
|
+
window.set_fetch_busy(busy)
|
|
378
|
+
window.statusBar().showMessage(run.request.operation.operation + ": " + outcome.status.value)
|
|
379
|
+
account = transfer_account(outcome.selection, outcome.endpoint, self.authentication.missing_secret_namespace_set)
|
|
380
|
+
push_dialog = self.push_dialog_by_session.get(run.request.operation.session_id)
|
|
381
|
+
if push_dialog is not None and isValid(push_dialog) and not push_dialog.closed and push_dialog.operation_id == outcome.operation_id:
|
|
382
|
+
push_dialog.show_outcome(outcome, account)
|
|
383
|
+
if run.request.mode == "push" and outcome.rejection is not None:
|
|
384
|
+
QTimer.singleShot(0, self, lambda: self._show_push_rejection(run, outcome))
|
|
385
|
+
dialog = self.console_registry_controller.transport_dialog_by_id.get(outcome.operation_id)
|
|
386
|
+
failed = outcome.status != OperationStatus.SUCCESS and outcome.diagnostic is not None
|
|
387
|
+
if dialog is not None and isValid(dialog):
|
|
388
|
+
failure_text = failure_summary(outcome.diagnostic) if failed else ""
|
|
389
|
+
# The same first sentence as the Push window: the server's own refusal when there is one, else the account one.
|
|
390
|
+
headline = (server_refusal_headline(outcome.server_refusals, outcome.diagnostic.stderr) or failure_headline(outcome.diagnostic, account)) if failed else ""
|
|
391
|
+
dialog.show_result(outcome.status, failure_text=failure_text, failure_headline=headline, stays_open=outcome.upstream_not_linked)
|
|
392
|
+
# Workaround: a refused Commit & push push reopens its Push window from this same signal, after this handler. Qt
|
|
393
|
+
# only lets the newest modal window take input, so Settings or the install window opened here, before the Push
|
|
394
|
+
# window, would be locked out by it (on macOS the Push window is modal for the whole application). Both open on
|
|
395
|
+
# the next turn of the event loop instead, above the Push window, in the same order as for any other refusal.
|
|
396
|
+
reopens_push_window = outcome.operation_id in self.commit_push.intent_by_push_id
|
|
397
|
+
if failed:
|
|
398
|
+
self.clone_controller.lookup_refused_account(outcome.selection, outcome.diagnostic, lambda: self._show_transport_needs_reconnect(run, outcome))
|
|
399
|
+
# After the console and the Push window show their red failure, which stays; Settings only comes in addition.
|
|
400
|
+
notice = account_notice(outcome.diagnostic, account, outcome.remote_name or (account.address if account is not None else ""))
|
|
401
|
+
if notice is not None:
|
|
402
|
+
# Bound now: the deferred call must open Settings for the refused repository, whatever window opened since.
|
|
403
|
+
refused_session_id = run.request.operation.session_id
|
|
404
|
+
if reopens_push_window:
|
|
405
|
+
QTimer.singleShot(0, self, lambda refused_session_id=refused_session_id, notice=notice: self.account_controller.offer_account_after_refusal(refused_session_id, notice))
|
|
406
|
+
else:
|
|
407
|
+
self.account_controller.offer_account_after_refusal(refused_session_id, notice)
|
|
408
|
+
# The console and the Push window keep their red line; the install window only comes in addition, even for a
|
|
409
|
+
# partial Fetch, since its HTTPS remote was stopped.
|
|
410
|
+
if outcome.credential_manager_refusal is not None:
|
|
411
|
+
if reopens_push_window:
|
|
412
|
+
QTimer.singleShot(0, self, lambda: self._offer_transport_credential_manager_install(run, outcome))
|
|
413
|
+
else:
|
|
414
|
+
self._offer_transport_credential_manager_install(run, outcome)
|
|
415
|
+
if not self.operation_policy_controller.allows(run.request.operation.session_id, WindowCapability.MANAGE):
|
|
416
|
+
run.trace.close()
|
|
417
|
+
if outcome.snapshot is None:
|
|
418
|
+
return
|
|
419
|
+
if not outcome.refresh_required:
|
|
420
|
+
# The transport proved that neither the server nor the repository changed: the views already show the
|
|
421
|
+
# truth, so nothing is read again. A refresh the user asked for meanwhile keeps its own course.
|
|
422
|
+
return
|
|
423
|
+
# Fetch, Push and remote deletion change remote-tracking and local references only; a HEAD that moved
|
|
424
|
+
# meanwhile still makes the refresh read everything.
|
|
425
|
+
for related_session_id, session in tuple(self.session_registry_controller.session_by_id.items()):
|
|
426
|
+
if (not session.is_closed and WindowCapability.MANAGE in session.capabilities
|
|
427
|
+
and canonical_path(session.repository_context.common_git_dir) == run.request.operation.common_git_dir):
|
|
428
|
+
self.refresh_controller.checkout_refresh_session_set.add(related_session_id)
|
|
429
|
+
self.refresh_controller.refresh(related_session_id, scope=RefreshScope.REFERENCES)
|
|
430
|
+
# After the refresh, which starts a new view generation that an earlier read would not belong to.
|
|
431
|
+
if run.request.mode == "fetch":
|
|
432
|
+
self._reread_push_windows(run.request.operation.common_git_dir)
|
|
433
|
+
|
|
434
|
+
def _offer_transport_credential_manager_install(self, run: TransportRun, outcome: TransportOutcome) -> None:
|
|
435
|
+
"""Open the install window for a transport stopped by the credential manager search, over its console while that console is shown."""
|
|
436
|
+
dialog = self.console_registry_controller.transport_dialog_by_id.get(outcome.operation_id)
|
|
437
|
+
parent = dialog if dialog is not None and isValid(dialog) and dialog.isVisible() else self.session_registry_controller.window_by_session.get(run.request.operation.session_id)
|
|
438
|
+
self.account_controller.offer_credential_manager_install(outcome.credential_manager_refusal, parent)
|
|
439
|
+
|
|
440
|
+
def _show_transport_needs_reconnect(self, run: TransportRun, outcome: TransportOutcome) -> None:
|
|
441
|
+
"""Say that the refused account needs a reconnect in the console of this transfer and in the Push window, while each still shows its failure.
|
|
442
|
+
|
|
443
|
+
A console belongs to one transfer; a Push window only counts while its last send is still this transfer.
|
|
444
|
+
"""
|
|
445
|
+
account = transfer_account(outcome.selection, outcome.endpoint, self.authentication.missing_secret_namespace_set)
|
|
446
|
+
dialog = self.console_registry_controller.transport_dialog_by_id.get(outcome.operation_id)
|
|
447
|
+
if dialog is not None and isValid(dialog) and not dialog.running:
|
|
448
|
+
dialog.show_failure_text(failure_summary(outcome.diagnostic), failure_headline(outcome.diagnostic, account))
|
|
449
|
+
push_dialog = self.push_dialog_by_session.get(run.request.operation.session_id)
|
|
450
|
+
if push_dialog is not None and isValid(push_dialog) and not push_dialog.closed and not push_dialog.busy and push_dialog.operation_id == outcome.operation_id:
|
|
451
|
+
push_dialog.show_outcome(outcome, account)
|
|
452
|
+
|
|
453
|
+
def open_push(self, session_id: str) -> PushDialog | None:
|
|
454
|
+
"""Open a read-only Push draft for the actual management repository."""
|
|
455
|
+
if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
|
|
456
|
+
return None
|
|
457
|
+
existing = self.push_dialog_by_session.get(session_id)
|
|
458
|
+
if existing is not None and not existing.closed:
|
|
459
|
+
existing.raise_()
|
|
460
|
+
return existing
|
|
461
|
+
session = self.session_registry_controller.session_by_id[session_id]
|
|
462
|
+
common_git_dir = canonical_path(session.repository_context.common_git_dir)
|
|
463
|
+
dialog = PushDialog(session.repository_context.repository_dir, self.session_registry_controller.window_by_session[session_id],
|
|
464
|
+
is_available=lambda: not any(operation.common_git_dir == common_git_dir for operation in self.repository_operation.active_by_id.values()))
|
|
465
|
+
self.push_dialog_by_session[session_id] = dialog
|
|
466
|
+
# A bound method of the dialog: Qt drops the connection when the dialog is destroyed, and a closed dialog ignores the call.
|
|
467
|
+
self.repository_operation.changed.connect(dialog.check_availability)
|
|
468
|
+
dialog.push_requested.connect(lambda request: self.submit_push(session_id, dialog, request))
|
|
469
|
+
dialog.reread_requested.connect(lambda: self.read_push(session_id, dialog))
|
|
470
|
+
dialog.destination_changed.connect(lambda: self.prepare_push_target(session_id, dialog))
|
|
471
|
+
|
|
472
|
+
def manage() -> None:
|
|
473
|
+
"""Reconcile the invoking draft after editing the same repository's remotes."""
|
|
474
|
+
remote_dialog = self.open_remote(session_id)
|
|
475
|
+
if remote_dialog is not None:
|
|
476
|
+
remote_dialog.finished.connect(lambda: self.read_push(session_id, dialog))
|
|
477
|
+
|
|
478
|
+
dialog.remote_requested.connect(manage)
|
|
479
|
+
open_window_modal(dialog)
|
|
480
|
+
self.read_push(session_id, dialog)
|
|
481
|
+
return dialog
|
|
482
|
+
|
|
483
|
+
def commit_push_requested(self, staging: StagingSession, request: CommitRequest) -> None:
|
|
484
|
+
"""Bind the staging intention to a live managing window before committing once."""
|
|
485
|
+
if not staging.busy or request is not staging.pending_commit_push_request or self.staging_session_registry_controller.staging_by_key.get(staging.key) is not staging:
|
|
486
|
+
return
|
|
487
|
+
session_id = staging.commit_push_session_id
|
|
488
|
+
if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE) or self.session_registry_controller.session_by_id[session_id].repository_context != staging.context:
|
|
489
|
+
staging.finish_commit_action(request.operation_id)
|
|
490
|
+
staging.busy = False
|
|
491
|
+
staging.pending_commit_push_request = None
|
|
492
|
+
staging.diagnostic = Diagnostic("Commit & push", staging.context.repository_dir, staging.git_filename, [], NO_LONGER_AUTHORIZED_TEXT).prepare()
|
|
493
|
+
staging._show()
|
|
494
|
+
return
|
|
495
|
+
operation = OperationIntent.capture(request.operation_id, staging.context, staging.git_filename, session_id=session_id,
|
|
496
|
+
generation=self.session_registry_controller.generation_by_session[session_id], operation="Commit & push", action=OperationAction.COMMIT_PUSH)
|
|
497
|
+
|
|
498
|
+
def captured(intent: CommitPushIntent | None, error: Exception | None) -> None:
|
|
499
|
+
"""Submit the original request once after capture or release its refused preparation."""
|
|
500
|
+
if staging.pending_commit_push_request is not request or self.staging_session_registry_controller.staging_by_key.get(staging.key) is not staging:
|
|
501
|
+
return
|
|
502
|
+
staging.pending_commit_push_request = None
|
|
503
|
+
if error is not None:
|
|
504
|
+
staging.finish_commit_action(request.operation_id)
|
|
505
|
+
staging.busy = False
|
|
506
|
+
staging.diagnostic = Diagnostic("Commit & push", staging.context.repository_dir, staging.git_filename, [], str(error)).prepare()
|
|
507
|
+
staging._show()
|
|
508
|
+
return
|
|
509
|
+
staging.commit_service.submit(intent.commit_request)
|
|
510
|
+
|
|
511
|
+
self.commit_push.capture(operation, request, captured)
|
|
512
|
+
|
|
513
|
+
def commit_push_started(self, run: TransportRun) -> None:
|
|
514
|
+
"""Show the actual Push operation after its local commit has already succeeded."""
|
|
515
|
+
operation = run.request.operation
|
|
516
|
+
parent = self.session_registry_controller.window_by_session.get(operation.session_id)
|
|
517
|
+
dialog = OperationDialog(run.trace, "Push", parent, preference=self.preference, console_operation=ConsoleOperation.PUSH)
|
|
518
|
+
self.console_registry_controller.transport_dialog_by_id[operation.operation_id] = dialog
|
|
519
|
+
dialog.abort_requested.connect(lambda: self.transport.cancel(operation.operation_id))
|
|
520
|
+
dialog.finished.connect(run.trace.close)
|
|
521
|
+
dialog.set_running()
|
|
522
|
+
open_window_modal(dialog, keeps_window_buttons=True)
|
|
523
|
+
|
|
524
|
+
def commit_push_decide_tracking(self, intent: CommitPushIntent) -> None:
|
|
525
|
+
"""Decide the tracking reference of a Commit & push's created commit, asking over the repository window when nothing decides it, then hand the Push back to the service.
|
|
526
|
+
|
|
527
|
+
The decision reads the capture taken before the commit, whose configuration the service checks again after the
|
|
528
|
+
answer. Yes or a kept Always add pushes and then links the branch, No or a kept Never add pushes without a link;
|
|
529
|
+
once the answer comes, the service checks the authority and the captured commit again, and reads the
|
|
530
|
+
configuration, HEAD and destination again right before the Push. Cancel sends nothing, keeps the created commit
|
|
531
|
+
and says so in the status bar of the repository window. A window that lost its management authority, before
|
|
532
|
+
the question or while it was open, gets no Push and says so in its status bar; a window that is gone has
|
|
533
|
+
nothing to say it in.
|
|
534
|
+
"""
|
|
535
|
+
session_id = intent.operation.session_id
|
|
536
|
+
window = self.session_registry_controller.window_by_session.get(session_id)
|
|
537
|
+
if window is None or not isValid(window):
|
|
538
|
+
return
|
|
539
|
+
|
|
540
|
+
def show(text: SharedText) -> None:
|
|
541
|
+
"""Tell in the repository window, while it is still there, what became of the created commit."""
|
|
542
|
+
if isValid(window):
|
|
543
|
+
window.statusBar().showMessage(text.value)
|
|
544
|
+
|
|
545
|
+
if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
|
|
546
|
+
show(SharedText.COMMIT_PUSH_NOT_SENT)
|
|
547
|
+
return
|
|
548
|
+
destination = intent.destination
|
|
549
|
+
self._decide_push_tracking(session_id, window, intent.snapshot, destination.remote_name, intent.source_ref, destination.destination_ref, (),
|
|
550
|
+
send=lambda add_tracking: self.commit_push.push_after_decision(intent, add_tracking, lambda: show(SharedText.COMMIT_PUSH_NOT_SENT)),
|
|
551
|
+
cancel=lambda: show(SharedText.COMMIT_PUSH_CANCELLED))
|
|
552
|
+
|
|
553
|
+
def commit_push_choose(self, intent: CommitPushIntent, message: str) -> None:
|
|
554
|
+
"""Keep the created commit visible when default routing needs an explicit choice, with its push console still usable."""
|
|
555
|
+
dialog = self.open_push(intent.operation.session_id)
|
|
556
|
+
if dialog is not None:
|
|
557
|
+
dialog.pin_commit(intent, message)
|
|
558
|
+
if dialog.needs_reread:
|
|
559
|
+
self.read_push(intent.operation.session_id, dialog)
|
|
560
|
+
self._keep_push_consoles_over(intent, dialog)
|
|
561
|
+
|
|
562
|
+
def _keep_push_consoles_over(self, intent: CommitPushIntent, push_dialog: PushDialog) -> None:
|
|
563
|
+
"""Keep the open push consoles of this Commit & push in front of its Push window and able to take clicks.
|
|
564
|
+
|
|
565
|
+
Workaround: the console opens before the Push window, both modal, and Qt lets only the newest modal window take
|
|
566
|
+
input, so the Push window shut the console out until Push was closed. Making the Push window the console's owner
|
|
567
|
+
window keeps the console above it and lets it take clicks again; Push stays usable too. This holds on Windows and
|
|
568
|
+
Linux, where both block the repository window, and on macOS, where both block the whole application.
|
|
569
|
+
"""
|
|
570
|
+
push_handle = push_dialog.windowHandle()
|
|
571
|
+
if push_handle is None:
|
|
572
|
+
return
|
|
573
|
+
for push_id, pushed_intent in tuple(self.commit_push.intent_by_push_id.items()):
|
|
574
|
+
if pushed_intent.commit_request.operation_id != intent.commit_request.operation_id:
|
|
575
|
+
continue
|
|
576
|
+
console = self.console_registry_controller.transport_dialog_by_id.get(push_id)
|
|
577
|
+
if console is None or not isValid(console) or not console.isVisible() or console.windowHandle() is None:
|
|
578
|
+
continue
|
|
579
|
+
console.windowHandle().setTransientParent(push_handle)
|
|
580
|
+
console.raise_()
|
|
581
|
+
console.activateWindow()
|
|
582
|
+
|
|
583
|
+
def submit_push(self, session_id: str, dialog: PushDialog, request: PushWindowRequest) -> None:
|
|
584
|
+
"""Capture the form's branch or tag objects under the current management authority."""
|
|
585
|
+
if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE) or dialog.closed or request != dialog.request or not dialog.busy:
|
|
586
|
+
return
|
|
587
|
+
snapshot, remote_name, _endpoint, source_ref, destination_ref, force, _push_target, tag_refs = request
|
|
588
|
+
if dialog.created_tag is not None and tag_refs != dialog.created_tag.selection():
|
|
589
|
+
dialog.show_error("Push must contain only the captured created tag. Close Push and review the tag before sending.")
|
|
590
|
+
return
|
|
591
|
+
if (snapshot.context != self.session_registry_controller.session_by_id[session_id].repository_context or snapshot.git_filename != self.repository.git_filename
|
|
592
|
+
or snapshot.generation != self.session_registry_controller.generation_by_session.get(session_id)):
|
|
593
|
+
dialog.needs_reread = True
|
|
594
|
+
dialog.show_error("The repository capture changed. Reread destination before sending.")
|
|
595
|
+
return
|
|
596
|
+
if dialog.commit_push_intent is not None and (tag_refs or source_ref != (dialog.commit_push_intent.source_ref or dialog.commit_push_intent.created_oid)):
|
|
597
|
+
dialog.show_error("Choose a destination for the captured commit.")
|
|
598
|
+
return
|
|
599
|
+
self._decide_push_tracking(session_id, dialog, snapshot, remote_name, source_ref, destination_ref, tag_refs,
|
|
600
|
+
send=lambda add_tracking: self._send_push(session_id, dialog, request, add_tracking),
|
|
601
|
+
cancel=lambda: self._cancel_push_send(dialog, request, force))
|
|
602
|
+
|
|
603
|
+
def _decide_push_tracking(self, session_id: str, parent: QWidget, snapshot: RemoteSnapshot, remote_name: str, source_ref: str, destination_ref: str, tag_refs: Sequence[tuple[str, str]],
|
|
604
|
+
send: Callable[[bool], None], cancel: Callable[[], None]) -> None:
|
|
605
|
+
"""Decide whether a Push also links its branch to the remote branch, asking the user only when nothing decides it already.
|
|
606
|
+
|
|
607
|
+
A branch that already has an upstream, a tag push, a typed address, an inactive remote or a destination no Fetch
|
|
608
|
+
refspec maps to a remote-tracking branch is sent without a link. Otherwise the choice kept for the repository
|
|
609
|
+
decides; without one, the question is asked before anything is sent, and ``send`` receives whether to link, or
|
|
610
|
+
``cancel`` runs when the user pushes nothing. Don't ask again keeps Yes as Always add and No as Never add for the
|
|
611
|
+
repository before sending; with Cancel it keeps nothing.
|
|
612
|
+
"""
|
|
613
|
+
if not tracking_question_applies(snapshot, remote_name, source_ref, destination_ref, tag_refs):
|
|
614
|
+
send(False)
|
|
615
|
+
return
|
|
616
|
+
choice = push_tracking_choice(self.preference, snapshot.context.common_git_dir)
|
|
617
|
+
if choice == PushTrackingChoice.ALWAYS_ADD:
|
|
618
|
+
send(True)
|
|
619
|
+
return
|
|
620
|
+
if choice == PushTrackingChoice.NEVER_ADD:
|
|
621
|
+
send(False)
|
|
622
|
+
return
|
|
623
|
+
previous = self.push_tracking_question_by_session.get(session_id)
|
|
624
|
+
if previous is not None and isValid(previous):
|
|
625
|
+
previous.reject()
|
|
626
|
+
box: QMessageBox | None = None
|
|
627
|
+
|
|
628
|
+
def answered(answer: PushTrackingAnswer, dont_ask_again: bool) -> None:
|
|
629
|
+
"""Keep the answer when asked to, then send with or without the link, or push nothing, as the user answered."""
|
|
630
|
+
if self.push_tracking_question_by_session.get(session_id) is box:
|
|
631
|
+
del self.push_tracking_question_by_session[session_id]
|
|
632
|
+
if answer == PushTrackingAnswer.YES:
|
|
633
|
+
if dont_ask_again:
|
|
634
|
+
self._remember_push_tracking(session_id, snapshot.context.common_git_dir, PushTrackingChoice.ALWAYS_ADD)
|
|
635
|
+
send(True)
|
|
636
|
+
elif answer == PushTrackingAnswer.NO:
|
|
637
|
+
if dont_ask_again:
|
|
638
|
+
self._remember_push_tracking(session_id, snapshot.context.common_git_dir, PushTrackingChoice.NEVER_ADD)
|
|
639
|
+
send(False)
|
|
640
|
+
else:
|
|
641
|
+
cancel()
|
|
642
|
+
|
|
643
|
+
box = ask_push_tracking(parent, source_ref.removeprefix(LOCAL_PREFIX), destination_ref.removeprefix(LOCAL_PREFIX), answered)
|
|
644
|
+
self.push_tracking_question_by_session[session_id] = box
|
|
645
|
+
|
|
646
|
+
def _remember_push_tracking(self, session_id: str, common_git_dir: str, choice: PushTrackingChoice) -> None:
|
|
647
|
+
"""Keep the tracking answer for the repository in the preferences file, and say in the repository window when it cannot be written.
|
|
648
|
+
|
|
649
|
+
The check box only spares the question next time, so a write that fails never stops the Push the user asked for.
|
|
650
|
+
"""
|
|
651
|
+
try:
|
|
652
|
+
save_push_tracking_choice(self.preference, common_git_dir, choice)
|
|
653
|
+
except (OSError, ValueError, TypeError) as error:
|
|
654
|
+
window = self.session_registry_controller.window_by_session.get(session_id)
|
|
655
|
+
repository_dir = window.context.repository_dir if window is not None and isValid(window) else ""
|
|
656
|
+
diagnostic = Diagnostic(SharedText.SAVE_TRACKING_CHOICE.value, repository_dir, "", [], str(error)).prepare()
|
|
657
|
+
if window is not None and isValid(window):
|
|
658
|
+
window.statusBar().showMessage(SharedText.PUSH_TRACKING_NOT_SAVED.value.format(cause=diagnostic.cause))
|
|
659
|
+
if self.diagnostic_log is not None:
|
|
660
|
+
self.diagnostic_log.write(diagnostic)
|
|
661
|
+
|
|
662
|
+
def _send_push(self, session_id: str, dialog: PushDialog, request: PushWindowRequest, add_tracking: bool) -> None:
|
|
663
|
+
"""Start the Push the form asked for, checking again what may have changed while the tracking question was open.
|
|
664
|
+
|
|
665
|
+
A window holding a Commit & push's created commit sends that commit through its service, so it is never
|
|
666
|
+
committed twice.
|
|
667
|
+
"""
|
|
668
|
+
if (not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE) or not isValid(dialog) or dialog.closed or request is not dialog.request
|
|
669
|
+
or not dialog.busy):
|
|
670
|
+
return
|
|
671
|
+
snapshot, remote_name, endpoint, source_ref, destination_ref, force, push_target, tag_refs = request
|
|
672
|
+
if (snapshot.context != self.session_registry_controller.session_by_id[session_id].repository_context or snapshot.git_filename != self.repository.git_filename
|
|
673
|
+
or snapshot.generation != self.session_registry_controller.generation_by_session.get(session_id)):
|
|
674
|
+
dialog.needs_reread = True
|
|
675
|
+
dialog.show_error("The repository capture changed. Reread destination before sending.")
|
|
676
|
+
return
|
|
677
|
+
if dialog.commit_push_intent is not None:
|
|
678
|
+
run = self.commit_push.retry_push(dialog.commit_push_intent, snapshot, remote_name, endpoint, destination_ref, push_target, force, add_tracking=add_tracking)
|
|
679
|
+
if run is None:
|
|
680
|
+
dialog.show_error("This commit's repository is no longer authorized for Push.")
|
|
681
|
+
return
|
|
682
|
+
dialog.operation_id = run.request.operation.operation_id
|
|
683
|
+
return
|
|
684
|
+
targets = []
|
|
685
|
+
for full_ref, oid in tag_refs:
|
|
686
|
+
targets.extend((full_ref, oid))
|
|
687
|
+
operation = OperationIntent.capture(uuid.uuid4().hex, snapshot.context, snapshot.git_filename, generation=snapshot.generation,
|
|
688
|
+
session_id=session_id, operation="Push", action=OperationAction.REMOTE_WRITE, targets=(*targets, endpoint) if tag_refs else (source_ref, destination_ref, endpoint),
|
|
689
|
+
from_form=True)
|
|
690
|
+
dialog.operation_id = operation.operation_id
|
|
691
|
+
self._start_push(session_id, TransportRequest(operation, remote_name=remote_name, snapshot=snapshot, mode="push", source_ref=source_ref,
|
|
692
|
+
source_oid=snapshot.tracking_by_ref[source_ref].oid if source_ref else "", destination_ref=destination_ref,
|
|
693
|
+
endpoint=endpoint, force=force, push_target=push_target, tag_refs=tag_refs, add_tracking=add_tracking))
|
|
694
|
+
|
|
695
|
+
def _cancel_push_send(self, dialog: PushDialog, request: PushWindowRequest, force: bool) -> None:
|
|
696
|
+
"""Give the Push window its draft back after Cancel, unless it was closed or sent another draft meanwhile."""
|
|
697
|
+
if not isValid(dialog) or dialog.closed or request is not dialog.request:
|
|
698
|
+
return
|
|
699
|
+
dialog.cancel_send(force)
|
|
700
|
+
|
|
701
|
+
def open_remote(self, session_id: str) -> RemoteDialog | None:
|
|
702
|
+
"""Open the remote editor only for a live repository management window."""
|
|
703
|
+
if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
|
|
704
|
+
return None
|
|
705
|
+
existing = self.remote_dialog_by_session.get(session_id)
|
|
706
|
+
if existing is not None and not existing.closed:
|
|
707
|
+
existing.raise_()
|
|
708
|
+
return existing
|
|
709
|
+
session = self.session_registry_controller.session_by_id[session_id]
|
|
710
|
+
generation = session.generation
|
|
711
|
+
dialog = RemoteDialog(session_id, self.session_registry_controller.window_by_session[session_id])
|
|
712
|
+
self.remote_dialog_by_session[session_id] = dialog
|
|
713
|
+
dialog.finished.connect(lambda: self.remote_dialog_by_session.pop(session_id, None) if self.remote_dialog_by_session.get(session_id) is dialog else None)
|
|
714
|
+
|
|
715
|
+
def is_current() -> bool:
|
|
716
|
+
"""Reject late reads and queued writes after the originating view changes."""
|
|
717
|
+
return (isValid(dialog) and not dialog.closed and not session.is_closed and session.generation == generation
|
|
718
|
+
and self.session_registry_controller.generation_by_session.get(session_id) == generation
|
|
719
|
+
and self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE))
|
|
720
|
+
|
|
721
|
+
def complete(snapshot: object, error: Exception | None) -> None:
|
|
722
|
+
"""Publish only the captured repository's complete current configuration."""
|
|
723
|
+
if not is_current():
|
|
724
|
+
return
|
|
725
|
+
if error is not None:
|
|
726
|
+
diagnostic = Diagnostic("Read remotes", session.repository_context.repository_dir, self.repository.git_filename, [], str(error)).prepare()
|
|
727
|
+
dialog.show_error(diagnostic.cause)
|
|
728
|
+
return
|
|
729
|
+
dialog.populate(snapshot)
|
|
730
|
+
|
|
731
|
+
def submit(request: tuple, action: str = "save") -> None:
|
|
732
|
+
"""Bind remote changes to the exact live form until their native completion."""
|
|
733
|
+
if not is_current() or request != dialog.request or not dialog.busy:
|
|
734
|
+
if isValid(dialog) and not dialog.closed and request == dialog.request:
|
|
735
|
+
dialog.request = None
|
|
736
|
+
dialog.show_error("The repository view changed. Reopen Manage remotes before making another change.")
|
|
737
|
+
return
|
|
738
|
+
snapshot, name, changes = request
|
|
739
|
+
if snapshot.context != session.repository_context or snapshot.generation != generation or snapshot.git_filename != self.repository.git_filename:
|
|
740
|
+
dialog.request = None
|
|
741
|
+
dialog.show_error("The remote capture changed. Reopen Manage remotes before making another change.")
|
|
742
|
+
return
|
|
743
|
+
operation_id = uuid.uuid4().hex
|
|
744
|
+
|
|
745
|
+
def finished(outcome: object) -> None:
|
|
746
|
+
"""Publish each field result without silently changing the editor generation."""
|
|
747
|
+
nonlocal generation
|
|
748
|
+
if outcome.operation_id != operation_id:
|
|
749
|
+
return
|
|
750
|
+
self.repository_operation.finished.disconnect(finished)
|
|
751
|
+
self.remote.outcome_by_id[operation_id] = outcome
|
|
752
|
+
if is_current():
|
|
753
|
+
dialog.show_outcome(outcome)
|
|
754
|
+
if outcome.diagnostic is not None and self.diagnostic_log is not None:
|
|
755
|
+
self.diagnostic_log.write(outcome.diagnostic)
|
|
756
|
+
if outcome.status != OperationStatus.REFUSED:
|
|
757
|
+
references_changed = any(step.operation in ("Name", "Remove remote") and step.status in (OperationStatus.SUCCESS, OperationStatus.UNKNOWN) for step in outcome.steps)
|
|
758
|
+
for current_id, current_session in tuple(self.session_registry_controller.session_by_id.items()):
|
|
759
|
+
if not current_session.is_closed and canonical_path(current_session.repository_context.common_git_dir) == canonical_path(snapshot.context.common_git_dir):
|
|
760
|
+
if not references_changed:
|
|
761
|
+
self.reference_read_controller.read_references(current_id)
|
|
762
|
+
continue
|
|
763
|
+
current_form = current_id == session_id and is_current()
|
|
764
|
+
already_refreshing = current_session.refresh_active
|
|
765
|
+
self.refresh_controller.refresh(current_id, scope=RefreshScope.REFERENCES)
|
|
766
|
+
if current_form and not already_refreshing:
|
|
767
|
+
# The editor's own ref change refreshes the graph; retain its exact config
|
|
768
|
+
# capture while advancing only the generation that this refresh owns.
|
|
769
|
+
generation = current_session.generation
|
|
770
|
+
if dialog.snapshot is not None:
|
|
771
|
+
dialog.snapshot = replace(dialog.snapshot, generation=generation)
|
|
772
|
+
elif current_form:
|
|
773
|
+
dialog.uncertain = True
|
|
774
|
+
dialog.show_error("Remote saved. The repository is refreshing; reopen Manage remotes before making another change.")
|
|
775
|
+
dialog.save_button.setEnabled(False)
|
|
776
|
+
|
|
777
|
+
self.repository_operation.finished.connect(finished)
|
|
778
|
+
if action == "active":
|
|
779
|
+
self.remote.set_active(snapshot, name, changes, session_id=session_id, is_current=is_current, operation_id=operation_id)
|
|
780
|
+
elif action == "remove":
|
|
781
|
+
self.remote.remove(snapshot, name, session_id=session_id, is_current=is_current, operation_id=operation_id)
|
|
782
|
+
elif action == "tracking":
|
|
783
|
+
self.remote.set_tracking(snapshot, name, changes, session_id=session_id, is_current=is_current, operation_id=operation_id)
|
|
784
|
+
else:
|
|
785
|
+
self.remote.save(snapshot, name, changes, session_id=session_id, is_current=is_current, operation_id=operation_id)
|
|
786
|
+
|
|
787
|
+
def reread_tracking() -> None:
|
|
788
|
+
"""Refresh the same branch editor only after an explicit reread request."""
|
|
789
|
+
if not is_current():
|
|
790
|
+
if isValid(dialog) and not dialog.closed:
|
|
791
|
+
dialog.show_error("The repository view changed. Reopen Manage remotes to read its current tracking.")
|
|
792
|
+
return
|
|
793
|
+
|
|
794
|
+
def read_complete(snapshot: object, error: Exception | None) -> None:
|
|
795
|
+
"""Accept fresh tracking only while the requesting form remains current."""
|
|
796
|
+
if not is_current():
|
|
797
|
+
return
|
|
798
|
+
if error is not None:
|
|
799
|
+
diagnostic = Diagnostic("Read tracking", session.repository_context.repository_dir, self.repository.git_filename, [], str(error)).prepare()
|
|
800
|
+
dialog.show_error(diagnostic.cause)
|
|
801
|
+
return
|
|
802
|
+
dialog.tracking_reloaded(snapshot)
|
|
803
|
+
|
|
804
|
+
self.remote.read(session.repository_context, self.repository.git_filename, generation, read_complete)
|
|
805
|
+
|
|
806
|
+
dialog.save_requested.connect(submit)
|
|
807
|
+
dialog.active_requested.connect(lambda request: submit(request, "active"))
|
|
808
|
+
dialog.remove_requested.connect(lambda request: submit(request, "remove"))
|
|
809
|
+
dialog.tracking_requested.connect(lambda request: submit(request, "tracking"))
|
|
810
|
+
dialog.tracking_reload_requested.connect(reread_tracking)
|
|
811
|
+
open_window_modal(dialog)
|
|
812
|
+
self.remote.read(session.repository_context, self.repository.git_filename, generation, complete)
|
|
813
|
+
return dialog
|