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,580 @@
|
|
|
1
|
+
"""Collect explicit push destinations without performing repository operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, NamedTuple
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import Qt, Signal
|
|
8
|
+
from PySide6.QtWidgets import QCheckBox, QDialog, QFileDialog, QGridLayout, QGroupBox, QHBoxLayout, QLayout, QLineEdit, QPushButton, QRadioButton, QScrollArea, QTabWidget, QVBoxLayout, QWidget
|
|
9
|
+
from shiboken6 import isValid
|
|
10
|
+
|
|
11
|
+
from gitpane.config import LOCAL_PREFIX, REMOTE_PREFIX, TAG_PREFIX
|
|
12
|
+
from gitpane.operation.diagnostic import SensitiveTextFilter
|
|
13
|
+
from gitpane.operation.operation_status import OperationStatus
|
|
14
|
+
from gitpane.operation.repository_operation import OWNED_REPOSITORY_TEXT
|
|
15
|
+
from gitpane.text.typed_path import session_typed_path, typed_path_value, typed_start_dir
|
|
16
|
+
from gitpane.text.typed_text import typed_value
|
|
17
|
+
from gitpane.transport.refusal_notice import failure_headline, failure_summary, headline_first, is_no_account_refusal, server_refusal_headline
|
|
18
|
+
from gitpane.transport.remote_config import default_push_destination_name, fetch_remote_name
|
|
19
|
+
from gitpane.ui.look.action_icon import action_icon
|
|
20
|
+
from gitpane.ui.transport.push_rejection_prompt import FORCE_WITH_LEASE_LABEL
|
|
21
|
+
from gitpane.ui.widget.content_growth import ContentGrowthMixin, OpeningFit
|
|
22
|
+
from gitpane.ui.widget.content_measure import ScrolledContentMeasure
|
|
23
|
+
from gitpane.ui.widget.dialog_layout import fit_form
|
|
24
|
+
from gitpane.ui.widget.long_text import WrappingLabel
|
|
25
|
+
from gitpane.ui.widget.result_label import ResultLabel
|
|
26
|
+
from gitpane.ui.widget.wheel_safe_drop_down import UrlSelector
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from collections.abc import Callable
|
|
30
|
+
|
|
31
|
+
from PySide6.QtCore import QRect
|
|
32
|
+
from PySide6.QtGui import QShowEvent
|
|
33
|
+
|
|
34
|
+
from gitpane.commit.commit_push import CommitPushIntent
|
|
35
|
+
from gitpane.transport.branch_exchange import PushTarget, TransportOutcome
|
|
36
|
+
from gitpane.transport.refusal_notice import TransferAccount
|
|
37
|
+
from gitpane.transport.remote_config import RemoteSnapshot
|
|
38
|
+
|
|
39
|
+
# The draft a Push window sends: its capture, remote name, endpoint, source ref, destination ref, force, push target
|
|
40
|
+
# and the tag refs with their objects. Only type checkers read it, so the names above need no import at run time.
|
|
41
|
+
PushWindowRequest = tuple[RemoteSnapshot, str, str, str, str, bool, PushTarget | None, tuple[tuple[str, str], ...]]
|
|
42
|
+
|
|
43
|
+
PUSH_DIALOG_WIDTH = 800
|
|
44
|
+
PUSH_DIALOG_HEIGHT = 480
|
|
45
|
+
BRANCH_TAB_INDEX = 0
|
|
46
|
+
TAG_TAB_INDEX = 1
|
|
47
|
+
TAG_SELECTION_ROLE = Qt.ItemDataRole.UserRole + 1
|
|
48
|
+
ALL_TAG_SELECTION = ""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class PinnedTag(NamedTuple):
|
|
52
|
+
"""The one tag object a creation follow-up may push, and the remote the tag creation proposed for it.
|
|
53
|
+
|
|
54
|
+
An empty remote name leaves the remote choice to Push.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
full_ref: str
|
|
58
|
+
object_oid: str
|
|
59
|
+
remote_name: str = ""
|
|
60
|
+
|
|
61
|
+
def selection(self) -> tuple[tuple[str, str], ...]:
|
|
62
|
+
"""Answer the tag list entry that stands for exactly this tag object, in the form the tag selector stores."""
|
|
63
|
+
return ((self.full_ref, self.object_oid),)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class PushDialog(ContentGrowthMixin, QDialog):
|
|
67
|
+
"""Retain one repository's source and destination draft until an explicit Push."""
|
|
68
|
+
|
|
69
|
+
push_requested = Signal(object)
|
|
70
|
+
remote_requested = Signal()
|
|
71
|
+
reread_requested = Signal()
|
|
72
|
+
destination_changed = Signal()
|
|
73
|
+
|
|
74
|
+
def __init__(self, repository_dir: str, parent: QWidget | None = None, *, is_available: Callable[[], bool] | None = None,
|
|
75
|
+
typed_path: Callable[[str], str] = session_typed_path) -> None:
|
|
76
|
+
"""Place scrolling fields above permanent actions and preserve native keyboard behavior.
|
|
77
|
+
|
|
78
|
+
``typed_path`` reads ``~`` in the Url field where that is the usage; the tests hand their own system and home
|
|
79
|
+
folder.
|
|
80
|
+
"""
|
|
81
|
+
super().__init__(parent)
|
|
82
|
+
self.typed_path = typed_path
|
|
83
|
+
self.snapshot: RemoteSnapshot | None = None
|
|
84
|
+
self.commit_push_intent: CommitPushIntent | None = None
|
|
85
|
+
self.commit_push_message = ""
|
|
86
|
+
self.created_tag: PinnedTag | None = None
|
|
87
|
+
self.closed = False
|
|
88
|
+
self.busy = False
|
|
89
|
+
self.loading = True
|
|
90
|
+
self.request: PushWindowRequest | None = None
|
|
91
|
+
self.operation_id = ""
|
|
92
|
+
self.push_target: PushTarget | None = None
|
|
93
|
+
self.target_error = ""
|
|
94
|
+
self.needs_reread = False
|
|
95
|
+
self.is_available = is_available or (lambda: True)
|
|
96
|
+
self.available = self.is_available()
|
|
97
|
+
self.return_focus = parent.focusWidget() if parent else None
|
|
98
|
+
self.text_filter = SensitiveTextFilter()
|
|
99
|
+
self.setWindowTitle("Push (" + repository_dir + ")")
|
|
100
|
+
layout = QVBoxLayout(self)
|
|
101
|
+
layout.setSizeConstraint(QLayout.SizeConstraint.SetNoConstraint)
|
|
102
|
+
self.scroll_area = QScrollArea()
|
|
103
|
+
self.scroll_area.setWidgetResizable(True)
|
|
104
|
+
self.content = QWidget()
|
|
105
|
+
body = QVBoxLayout(self.content)
|
|
106
|
+
self.commit_result_label = ResultLabel()
|
|
107
|
+
self.commit_result_label.hide()
|
|
108
|
+
body.addWidget(self.commit_result_label)
|
|
109
|
+
destination_group = QGroupBox("Push to")
|
|
110
|
+
destination = QGridLayout(destination_group)
|
|
111
|
+
self.remote_radio = QRadioButton("Remote")
|
|
112
|
+
self.remote_radio.setChecked(True)
|
|
113
|
+
self.url_radio = QRadioButton("Url")
|
|
114
|
+
self.remote_selector = UrlSelector()
|
|
115
|
+
self.remote_selector.setAccessibleName("Push remote")
|
|
116
|
+
self.manage_button = QPushButton("Manage remotes")
|
|
117
|
+
self.url_edit = QLineEdit()
|
|
118
|
+
self.url_edit.setAccessibleName("Push Url")
|
|
119
|
+
self.browse_button = QPushButton("Browse…")
|
|
120
|
+
self.browse_button.setAccessibleName("Browse Push repository")
|
|
121
|
+
destination.addWidget(self.remote_radio, 0, 0)
|
|
122
|
+
destination.addWidget(self.remote_selector, 0, 1)
|
|
123
|
+
destination.addWidget(self.manage_button, 0, 2)
|
|
124
|
+
destination.addWidget(self.url_radio, 1, 0)
|
|
125
|
+
destination.addWidget(self.url_edit, 1, 1)
|
|
126
|
+
destination.addWidget(self.browse_button, 1, 2)
|
|
127
|
+
self.endpoint_selector = UrlSelector()
|
|
128
|
+
self.endpoint_selector.setAccessibleName("Push endpoint")
|
|
129
|
+
endpoint_label = WrappingLabel("Push endpoint")
|
|
130
|
+
endpoint_label.setBuddy(self.endpoint_selector)
|
|
131
|
+
destination.addWidget(endpoint_label, 2, 0)
|
|
132
|
+
destination.addWidget(self.endpoint_selector, 2, 1, 1, 2)
|
|
133
|
+
body.addWidget(destination_group)
|
|
134
|
+
self.tabs = QTabWidget()
|
|
135
|
+
branch_page = QWidget()
|
|
136
|
+
branch = QGridLayout(branch_page)
|
|
137
|
+
branch.setColumnStretch(1, 1)
|
|
138
|
+
branch.setColumnStretch(3, 1)
|
|
139
|
+
self.local_label = WrappingLabel("Local branch")
|
|
140
|
+
self.destination_label = WrappingLabel("Remote branch")
|
|
141
|
+
self.source_selector = UrlSelector()
|
|
142
|
+
self.source_selector.setAccessibleName("Local branch to push")
|
|
143
|
+
self.destination_selector = UrlSelector()
|
|
144
|
+
self.destination_selector.setEditable(True)
|
|
145
|
+
self.destination_selector.setAccessibleName("Remote branch destination")
|
|
146
|
+
self.destination_selector.lineEdit().setPlaceholderText("Existing or new branch name")
|
|
147
|
+
self.local_label.setBuddy(self.source_selector)
|
|
148
|
+
self.destination_label.setBuddy(self.destination_selector)
|
|
149
|
+
branch.addWidget(self.local_label, 0, 1)
|
|
150
|
+
branch.addWidget(self.destination_label, 0, 3)
|
|
151
|
+
branch.addWidget(WrappingLabel("Branch to push"), 1, 0)
|
|
152
|
+
branch.addWidget(self.source_selector, 1, 1)
|
|
153
|
+
branch.addWidget(WrappingLabel("to"), 1, 2)
|
|
154
|
+
branch.addWidget(self.destination_selector, 1, 3)
|
|
155
|
+
self.options_check = QCheckBox("Show options")
|
|
156
|
+
self.force_check = QCheckBox(FORCE_WITH_LEASE_LABEL)
|
|
157
|
+
self.force_check.hide()
|
|
158
|
+
self.options_check.toggled.connect(lambda checked: self.force_check.setVisible(checked or self.force_check.isChecked()))
|
|
159
|
+
self.force_check.toggled.connect(lambda checked: self.options_check.setChecked(True) if checked else None)
|
|
160
|
+
branch.addWidget(self.options_check, 2, 1)
|
|
161
|
+
branch.addWidget(self.force_check, 3, 1)
|
|
162
|
+
branch.setRowStretch(4, 1)
|
|
163
|
+
self.tabs.addTab(branch_page, "Push branches")
|
|
164
|
+
tag_page = QWidget()
|
|
165
|
+
tag_layout = QGridLayout(tag_page)
|
|
166
|
+
tag_label = WrappingLabel("Tag to push")
|
|
167
|
+
self.tag_selector = UrlSelector()
|
|
168
|
+
self.tag_selector.setAccessibleName("Tag to push")
|
|
169
|
+
tag_label.setBuddy(self.tag_selector)
|
|
170
|
+
tag_label_row, tag_force_row, tag_spacer_row = 0, 1, 2
|
|
171
|
+
tag_label_column, tag_selector_column = 0, 1
|
|
172
|
+
tag_layout.addWidget(tag_label, tag_label_row, tag_label_column)
|
|
173
|
+
tag_layout.addWidget(self.tag_selector, tag_label_row, tag_selector_column)
|
|
174
|
+
self.tag_force_check = QCheckBox(FORCE_WITH_LEASE_LABEL)
|
|
175
|
+
tag_layout.addWidget(self.tag_force_check, tag_force_row, tag_selector_column)
|
|
176
|
+
tag_layout.setColumnStretch(tag_selector_column, 1)
|
|
177
|
+
tag_layout.setRowStretch(tag_spacer_row, 1)
|
|
178
|
+
self.tabs.addTab(tag_page, "Push tags")
|
|
179
|
+
body.addWidget(self.tabs, 1)
|
|
180
|
+
self.status_label = ResultLabel("Reading source and destination…")
|
|
181
|
+
body.addWidget(self.status_label)
|
|
182
|
+
self.scroll_area.setWidget(self.content)
|
|
183
|
+
# The note of a refused Commit & push, a refusal in the status line and the reread that replaces it arrive after
|
|
184
|
+
# the window opened, and the window follows them both ways by the rule it shares with the other forms, within
|
|
185
|
+
# its screen, never below the height it opened at or the one the user set.
|
|
186
|
+
self.install_content_growth(ScrolledContentMeasure(self.scroll_area), OpeningFit.GROW_ONLY, PUSH_DIALOG_HEIGHT)
|
|
187
|
+
layout.addWidget(self.scroll_area, 1)
|
|
188
|
+
actions = QHBoxLayout()
|
|
189
|
+
self.reread_button = QPushButton("Reread destination")
|
|
190
|
+
self.close_button = QPushButton("Close")
|
|
191
|
+
self.push_button = QPushButton("Push")
|
|
192
|
+
self.push_button.setIcon(action_icon("push", self.devicePixelRatioF()))
|
|
193
|
+
actions.addWidget(self.reread_button)
|
|
194
|
+
actions.addStretch()
|
|
195
|
+
actions.addWidget(self.close_button)
|
|
196
|
+
actions.addWidget(self.push_button)
|
|
197
|
+
layout.addLayout(actions)
|
|
198
|
+
for button in (self.manage_button, self.browse_button, self.reread_button, self.close_button, self.push_button):
|
|
199
|
+
button.setAutoDefault(False)
|
|
200
|
+
self.push_button.clicked.connect(self.submit)
|
|
201
|
+
self.close_button.clicked.connect(self.reject)
|
|
202
|
+
self.manage_button.clicked.connect(self.remote_requested.emit)
|
|
203
|
+
self.reread_button.clicked.connect(self.reread_requested.emit)
|
|
204
|
+
self.browse_button.clicked.connect(self.browse)
|
|
205
|
+
self.remote_selector.currentIndexChanged.connect(self._remote_changed)
|
|
206
|
+
self.endpoint_selector.currentIndexChanged.connect(self._endpoint_changed)
|
|
207
|
+
self.source_selector.currentIndexChanged.connect(self._source_changed)
|
|
208
|
+
self.tag_selector.currentIndexChanged.connect(self.update_action)
|
|
209
|
+
self.tag_selector.currentIndexChanged.connect(self.reset_force)
|
|
210
|
+
self.source_selector.currentIndexChanged.connect(self.reset_force)
|
|
211
|
+
self.destination_selector.editTextChanged.connect(self.update_action)
|
|
212
|
+
self.destination_selector.editTextChanged.connect(self.reset_force)
|
|
213
|
+
self.destination_changed.connect(self.reset_force)
|
|
214
|
+
self.remote_radio.toggled.connect(self._mode_changed)
|
|
215
|
+
self.url_edit.textChanged.connect(lambda _text: self.destination_changed.emit() if not self.remote_radio.isChecked() else None)
|
|
216
|
+
self.url_edit.textChanged.connect(self.update_action)
|
|
217
|
+
self.tabs.currentChanged.connect(self.update_action)
|
|
218
|
+
self.tabs.currentChanged.connect(self.reset_force)
|
|
219
|
+
self.finished.connect(self._closed)
|
|
220
|
+
self.resize(PUSH_DIALOG_WIDTH, PUSH_DIALOG_HEIGHT)
|
|
221
|
+
self.refresh_metrics()
|
|
222
|
+
self.update_action()
|
|
223
|
+
|
|
224
|
+
def populate(self, snapshot: RemoteSnapshot, head_ref: str | None, tag_oid_by_ref: dict[str, str]) -> None:
|
|
225
|
+
"""Display captured refs and retain a prior draft without selecting a vanished remote."""
|
|
226
|
+
self.reset_force()
|
|
227
|
+
previous = self.snapshot
|
|
228
|
+
previous_endpoint = getattr(self, "endpoint", "")
|
|
229
|
+
remote_name = self.remote_selector.currentData() if previous else fetch_remote_name(snapshot, head_ref)
|
|
230
|
+
# A remote proposed by a tag creation that pinned this window before its first read is chosen once the remote
|
|
231
|
+
# list arrives; a later read keeps whatever remote the user chose since.
|
|
232
|
+
if not previous and self.created_tag is not None and self.created_tag.remote_name in snapshot.entry_by_name:
|
|
233
|
+
remote_name = self.created_tag.remote_name
|
|
234
|
+
source_ref = self.source_selector.currentData() if previous else head_ref
|
|
235
|
+
destination = self.destination_selector.currentText() if previous else ""
|
|
236
|
+
tag_selection = self.tag_selector.currentData(TAG_SELECTION_ROLE) if previous else ALL_TAG_SELECTION
|
|
237
|
+
if self.created_tag is not None:
|
|
238
|
+
tag_selection = self.created_tag.full_ref
|
|
239
|
+
self.loading = True
|
|
240
|
+
self.snapshot = snapshot
|
|
241
|
+
self.needs_reread = False
|
|
242
|
+
self.remote_selector.clear()
|
|
243
|
+
for name in sorted(snapshot.entry_by_name):
|
|
244
|
+
entry = snapshot.entry_by_name[name]
|
|
245
|
+
self.remote_selector.addItem(name + (" (inactive)" if not entry.active else ""), name)
|
|
246
|
+
self.remote_selector.setCurrentIndex(self.remote_selector.findData(remote_name))
|
|
247
|
+
self.source_selector.clear()
|
|
248
|
+
for full_ref in sorted(snapshot.tracking_by_ref):
|
|
249
|
+
self.source_selector.addItem(full_ref.removeprefix(LOCAL_PREFIX), full_ref)
|
|
250
|
+
self.source_selector.setCurrentIndex(self.source_selector.findData(source_ref))
|
|
251
|
+
if self.commit_push_intent is not None:
|
|
252
|
+
self.pin_commit(self.commit_push_intent, self.commit_push_message)
|
|
253
|
+
self.tag_selector.clear()
|
|
254
|
+
if tag_oid_by_ref:
|
|
255
|
+
self.tag_selector.addItem("[ All ]", tuple(sorted(tag_oid_by_ref.items())))
|
|
256
|
+
self.tag_selector.setItemData(self.tag_selector.count() - 1, ALL_TAG_SELECTION, TAG_SELECTION_ROLE)
|
|
257
|
+
for full_ref in sorted(tag_oid_by_ref):
|
|
258
|
+
self.tag_selector.addItem(full_ref.removeprefix(TAG_PREFIX), ((full_ref, tag_oid_by_ref[full_ref]),))
|
|
259
|
+
self.tag_selector.setItemData(self.tag_selector.count() - 1, full_ref, TAG_SELECTION_ROLE)
|
|
260
|
+
self.tag_selector.setCurrentIndex(self.tag_selector.findData(tag_selection, TAG_SELECTION_ROLE))
|
|
261
|
+
if self.created_tag is not None:
|
|
262
|
+
self._lock_pinned_tag()
|
|
263
|
+
self.loading = False
|
|
264
|
+
self._remote_changed()
|
|
265
|
+
self._source_changed()
|
|
266
|
+
if previous:
|
|
267
|
+
self.destination_selector.setEditText(destination)
|
|
268
|
+
self.endpoint_selector.setCurrentIndex(self.endpoint_selector.findData(previous_endpoint))
|
|
269
|
+
self._endpoint_changed()
|
|
270
|
+
self.status_label.setText("Choose the source and destination, then Push.")
|
|
271
|
+
self.update_action()
|
|
272
|
+
|
|
273
|
+
def pin_tag(self, full_ref: str, object_oid: str, remote_name: str = "") -> bool:
|
|
274
|
+
"""Restrict a creation follow-up to that exact tag object, never all tags, and choose the remote it proposed.
|
|
275
|
+
|
|
276
|
+
The remote is chosen only when the remote list already holds it; a list still being read gets it on arrival,
|
|
277
|
+
and a remote Push does not know is never added, so the choice stays Push's own.
|
|
278
|
+
"""
|
|
279
|
+
if self.busy or self.commit_push_intent is not None:
|
|
280
|
+
return False
|
|
281
|
+
self.created_tag = PinnedTag(full_ref, object_oid, remote_name)
|
|
282
|
+
remote_index = self.remote_selector.findData(remote_name) if remote_name else -1
|
|
283
|
+
if remote_index >= 0:
|
|
284
|
+
self.remote_selector.setCurrentIndex(remote_index)
|
|
285
|
+
self._lock_pinned_tag()
|
|
286
|
+
self.reset_force()
|
|
287
|
+
self.update_action()
|
|
288
|
+
return True
|
|
289
|
+
|
|
290
|
+
def _lock_pinned_tag(self) -> None:
|
|
291
|
+
"""Select the pinned tag object on the tag page and lock both the list and the branch page."""
|
|
292
|
+
index = self.tag_selector.findData(self.created_tag.full_ref, TAG_SELECTION_ROLE)
|
|
293
|
+
matches = index >= 0 and self.tag_selector.itemData(index) == self.created_tag.selection()
|
|
294
|
+
self.tag_selector.setCurrentIndex(index if matches else -1)
|
|
295
|
+
self.tag_selector.setEnabled(False)
|
|
296
|
+
self.tabs.setCurrentIndex(TAG_TAB_INDEX)
|
|
297
|
+
self.tabs.setTabEnabled(BRANCH_TAB_INDEX, False)
|
|
298
|
+
|
|
299
|
+
def pin_commit(self, intent: CommitPushIntent, message: str) -> None:
|
|
300
|
+
"""Make a created commit the sole source while leaving its destination editable.
|
|
301
|
+
|
|
302
|
+
The note arrives once the window is already open, above the form; the window follows it by the shared content
|
|
303
|
+
rule, so the note and the whole form fit without scrolling, within its screen.
|
|
304
|
+
"""
|
|
305
|
+
self.commit_push_intent = intent
|
|
306
|
+
self.commit_push_message = message
|
|
307
|
+
commit_text = self.text_filter.redact(message + "\nCommit: " + intent.created_oid)
|
|
308
|
+
reason_text = headline_first(intent.reason_headline, self.text_filter.redact(intent.reason) if intent.reason else "")
|
|
309
|
+
if reason_text:
|
|
310
|
+
commit_text += "\n" + reason_text
|
|
311
|
+
if intent.push_failed:
|
|
312
|
+
self.commit_result_label.show_failure(commit_text)
|
|
313
|
+
else:
|
|
314
|
+
self.commit_result_label.setText(commit_text)
|
|
315
|
+
self.commit_result_label.show()
|
|
316
|
+
self.source_selector.clear()
|
|
317
|
+
self.source_selector.addItem(intent.source_ref.removeprefix(LOCAL_PREFIX) + " — " + intent.created_oid, intent.source_ref or intent.created_oid)
|
|
318
|
+
self.source_selector.setEnabled(False)
|
|
319
|
+
self.tabs.setCurrentIndex(BRANCH_TAB_INDEX)
|
|
320
|
+
for index in range(self.tabs.count()):
|
|
321
|
+
self.tabs.setTabEnabled(index, index == BRANCH_TAB_INDEX)
|
|
322
|
+
self.reset_force()
|
|
323
|
+
|
|
324
|
+
def _remote_changed(self) -> None:
|
|
325
|
+
"""Show the exact effective Push URL and require correction for an absent destination."""
|
|
326
|
+
if self.loading or self.snapshot is None:
|
|
327
|
+
return
|
|
328
|
+
entry = self.snapshot.entry_by_name.get(self.remote_selector.currentData())
|
|
329
|
+
self.endpoint_selector.clear()
|
|
330
|
+
for endpoint in entry.push_urls if entry is not None else ():
|
|
331
|
+
self.endpoint_selector.addItem(self.text_filter.redact(endpoint), endpoint)
|
|
332
|
+
self.endpoint_selector.setCurrentIndex(0 if entry is not None and len(entry.push_urls) == 1 else -1)
|
|
333
|
+
self._endpoint_changed()
|
|
334
|
+
destination = self.destination_selector.currentText()
|
|
335
|
+
self.destination_selector.clear()
|
|
336
|
+
remote_prefix = REMOTE_PREFIX + (self.remote_selector.currentData() or "") + "/"
|
|
337
|
+
for record in self.snapshot.references.splitlines():
|
|
338
|
+
full_ref, _separator, _rest = record.partition(b"\0")
|
|
339
|
+
name = full_ref.decode("utf-8", "surrogateescape")
|
|
340
|
+
if name.startswith(remote_prefix):
|
|
341
|
+
self.destination_selector.addItem(name.removeprefix(remote_prefix))
|
|
342
|
+
self.destination_selector.setEditText(destination)
|
|
343
|
+
|
|
344
|
+
def _endpoint_changed(self) -> None:
|
|
345
|
+
"""Keep one full endpoint identity separate from its filtered display text."""
|
|
346
|
+
self.endpoint = self.endpoint_selector.currentData() or ""
|
|
347
|
+
if self.remote_radio.isChecked():
|
|
348
|
+
self.url_edit.setText(self.text_filter.redact(self.endpoint))
|
|
349
|
+
if not self.loading:
|
|
350
|
+
self.destination_changed.emit()
|
|
351
|
+
self.update_action()
|
|
352
|
+
|
|
353
|
+
def _source_changed(self) -> None:
|
|
354
|
+
"""Suggest the local name for a branch without tracking, otherwise use its matching upstream."""
|
|
355
|
+
if self.loading or self.snapshot is None:
|
|
356
|
+
return
|
|
357
|
+
self.destination_selector.setEditText(default_push_destination_name(self.snapshot, self.source_selector.currentData(), self.remote_selector.currentData()))
|
|
358
|
+
self.update_action()
|
|
359
|
+
|
|
360
|
+
def _mode_changed(self) -> None:
|
|
361
|
+
"""Switch the destination input without interpreting a displayed redacted URL as authority."""
|
|
362
|
+
remote_mode = self.remote_radio.isChecked()
|
|
363
|
+
self.remote_selector.setEnabled(remote_mode)
|
|
364
|
+
self.endpoint_selector.setEnabled(remote_mode)
|
|
365
|
+
self.url_edit.setEnabled(not remote_mode)
|
|
366
|
+
self.browse_button.setEnabled(not remote_mode)
|
|
367
|
+
if remote_mode:
|
|
368
|
+
self._remote_changed()
|
|
369
|
+
else:
|
|
370
|
+
self.url_edit.clear()
|
|
371
|
+
self.destination_changed.emit()
|
|
372
|
+
self.update_action()
|
|
373
|
+
|
|
374
|
+
def update_action(self, *_arguments: object) -> None:
|
|
375
|
+
"""Disable incomplete, loading and unsupported submissions with a visible reason.
|
|
376
|
+
|
|
377
|
+
The reason is ordinary text, except GitPane's refusal to send without an account, shown in bold orange: nothing
|
|
378
|
+
is wrong with the draft, the user only has to choose an account in Settings.
|
|
379
|
+
"""
|
|
380
|
+
if not hasattr(self, "push_button"):
|
|
381
|
+
return
|
|
382
|
+
reason = ""
|
|
383
|
+
if self.loading:
|
|
384
|
+
reason = "Reading source and destination…"
|
|
385
|
+
elif self.busy:
|
|
386
|
+
reason = "Push in progress…"
|
|
387
|
+
elif self.needs_reread:
|
|
388
|
+
reason = "Reread destination before another Push."
|
|
389
|
+
elif not self.available:
|
|
390
|
+
reason = OWNED_REPOSITORY_TEXT
|
|
391
|
+
elif self.created_tag is not None and self.tag_selector.currentData() != self.created_tag.selection():
|
|
392
|
+
reason = "The created tag changed or disappeared. Close Push and review the tag before sending."
|
|
393
|
+
elif self.tabs.currentIndex() != BRANCH_TAB_INDEX and not self.tag_selector.currentData():
|
|
394
|
+
reason = "Select a tag or explicitly choose [ All ] before pushing tags." if self.tag_selector.count() else "Create a tag from a commit before pushing tags."
|
|
395
|
+
elif self.tabs.currentIndex() == BRANCH_TAB_INDEX and not self.source_selector.currentData():
|
|
396
|
+
reason = "No local branch selected."
|
|
397
|
+
elif self.tabs.currentIndex() == BRANCH_TAB_INDEX and not self.typed_destination_branch():
|
|
398
|
+
reason = "Choose a remote branch destination."
|
|
399
|
+
elif not self.chosen_endpoint():
|
|
400
|
+
reason = "No remote configured. Use Manage remotes or Url to choose a destination."
|
|
401
|
+
elif self.target_error or self.push_target is None:
|
|
402
|
+
reason = self.target_error or "Choose a Push endpoint."
|
|
403
|
+
self.push_button.setEnabled(not reason and not self.closed)
|
|
404
|
+
if reason and is_no_account_refusal(reason):
|
|
405
|
+
self.status_label.show_warning(reason)
|
|
406
|
+
elif reason:
|
|
407
|
+
self.status_label.setText(reason)
|
|
408
|
+
else:
|
|
409
|
+
self.status_label.setText("Ready to push the selected branch." if self.tabs.currentIndex() == BRANCH_TAB_INDEX else "Ready to push the selected tags.")
|
|
410
|
+
self.url_edit.setEnabled(not self.remote_radio.isChecked())
|
|
411
|
+
self.browse_button.setEnabled(not self.remote_radio.isChecked())
|
|
412
|
+
|
|
413
|
+
def chosen_endpoint(self) -> str:
|
|
414
|
+
"""Return where Push sends: the chosen remote's endpoint, or the typed Url without the blanks around it.
|
|
415
|
+
|
|
416
|
+
In the typed Url, ``~`` alone or ``~/…`` at the start stands for the home folder where that is the usage; only a
|
|
417
|
+
local folder address can change, since no HTTPS, SSH or ``file://`` address starts with ``~/``. The button
|
|
418
|
+
state, the account bound to the destination, the captured target and the request all read the address here, so
|
|
419
|
+
they always agree on the same address; the Url field keeps the text as typed.
|
|
420
|
+
"""
|
|
421
|
+
if self.remote_radio.isChecked():
|
|
422
|
+
return getattr(self, "endpoint", "")
|
|
423
|
+
return typed_path_value(self.url_edit.text(), self.typed_path)
|
|
424
|
+
|
|
425
|
+
def typed_destination_branch(self) -> str:
|
|
426
|
+
"""Return the remote branch typed or chosen, without the blanks around it; the field keeps the text as typed."""
|
|
427
|
+
return typed_value(self.destination_selector.currentText())
|
|
428
|
+
|
|
429
|
+
def submit(self) -> None:
|
|
430
|
+
"""Emit the exact displayed draft once without executing Git from the widget."""
|
|
431
|
+
self.check_availability()
|
|
432
|
+
if not self.push_button.isEnabled() or self.snapshot is None:
|
|
433
|
+
return
|
|
434
|
+
endpoint = self.chosen_endpoint()
|
|
435
|
+
remote_name = self.remote_selector.currentData() if self.remote_radio.isChecked() else ""
|
|
436
|
+
branch_mode = self.tabs.currentIndex() == BRANCH_TAB_INDEX
|
|
437
|
+
tag_refs = () if branch_mode else tuple(self.tag_selector.currentData())
|
|
438
|
+
self.request = (self.snapshot, remote_name, endpoint, self.source_selector.currentData() if branch_mode else "",
|
|
439
|
+
LOCAL_PREFIX + self.typed_destination_branch() if branch_mode else "",
|
|
440
|
+
self.force_check.isChecked() if branch_mode else self.tag_force_check.isChecked(), self.push_target, tag_refs)
|
|
441
|
+
self.reset_force()
|
|
442
|
+
self.busy = True
|
|
443
|
+
self.content.setEnabled(False)
|
|
444
|
+
self.reread_button.setEnabled(False)
|
|
445
|
+
self.update_action()
|
|
446
|
+
self.push_requested.emit(self.request)
|
|
447
|
+
|
|
448
|
+
def cancel_send(self, force: bool) -> None:
|
|
449
|
+
"""Give the draft back as it was before Push was clicked, when the user answers Cancel to a question asked before sending.
|
|
450
|
+
|
|
451
|
+
Nothing was sent and nothing went wrong, so no error is shown; Force comes back checked when it was, since
|
|
452
|
+
submit cleared it for the send that did not happen.
|
|
453
|
+
"""
|
|
454
|
+
self.busy = False
|
|
455
|
+
self.request = None
|
|
456
|
+
self.content.setEnabled(True)
|
|
457
|
+
self.reread_button.setEnabled(True)
|
|
458
|
+
if self.tabs.currentIndex() == BRANCH_TAB_INDEX:
|
|
459
|
+
self.force_check.setChecked(force)
|
|
460
|
+
else:
|
|
461
|
+
self.tag_force_check.setChecked(force)
|
|
462
|
+
# check_availability ignores a busy window, so a repository taken by another owner meanwhile is only seen here.
|
|
463
|
+
self.available = self.is_available()
|
|
464
|
+
self.update_action()
|
|
465
|
+
|
|
466
|
+
def follow_operation(self, operation_id: str) -> None:
|
|
467
|
+
"""Treat a push started for this draft from elsewhere (Force push with lease chosen after a refusal) as this window's own send."""
|
|
468
|
+
self.operation_id = operation_id
|
|
469
|
+
self.busy = True
|
|
470
|
+
self.content.setEnabled(False)
|
|
471
|
+
self.reread_button.setEnabled(False)
|
|
472
|
+
self.update_action()
|
|
473
|
+
|
|
474
|
+
def check_availability(self) -> None:
|
|
475
|
+
"""Reflect another repository owner without replacing this operation's terminal details.
|
|
476
|
+
|
|
477
|
+
The transport controller connects it to each change of the repository operation register, so the window never polls.
|
|
478
|
+
"""
|
|
479
|
+
if self.busy or self.closed:
|
|
480
|
+
return
|
|
481
|
+
available = self.is_available()
|
|
482
|
+
if available != self.available:
|
|
483
|
+
self.available = available
|
|
484
|
+
if not available:
|
|
485
|
+
self.reset_force()
|
|
486
|
+
self.update_action()
|
|
487
|
+
|
|
488
|
+
def reset_force(self, *_arguments: object) -> None:
|
|
489
|
+
"""Require a fresh explicit choice for each target and each independent submission."""
|
|
490
|
+
self.force_check.setChecked(False)
|
|
491
|
+
self.tag_force_check.setChecked(False)
|
|
492
|
+
|
|
493
|
+
def show_error(self, message: str, headline: str = "") -> None:
|
|
494
|
+
"""Keep failed drafts available and show only filtered plain-text diagnostics, in bold red.
|
|
495
|
+
|
|
496
|
+
GitPane's refusal to send without an account is shown in bold orange instead, as when Push opens: nothing was
|
|
497
|
+
sent and the user only has to choose an account in Settings.
|
|
498
|
+
|
|
499
|
+
A ``headline`` built by GitPane from an address and an account label comes first and is not filtered, so the
|
|
500
|
+
account it names stays readable. The window follows its status line both ways by the shared content rule: it
|
|
501
|
+
grows so the whole message and the form fit without scrolling, within its screen, and takes back its height
|
|
502
|
+
when the reread replaces the message.
|
|
503
|
+
"""
|
|
504
|
+
self.reset_force()
|
|
505
|
+
self.busy = False
|
|
506
|
+
self.loading = False
|
|
507
|
+
self.content.setEnabled(True)
|
|
508
|
+
self.reread_button.setEnabled(True)
|
|
509
|
+
# check_availability ignores a busy window, so a repository released while this send was running is only seen here.
|
|
510
|
+
self.available = self.is_available()
|
|
511
|
+
self.update_action()
|
|
512
|
+
shown_text = headline_first(headline, self.text_filter.redact(message))
|
|
513
|
+
if is_no_account_refusal(shown_text):
|
|
514
|
+
self.status_label.show_warning(shown_text)
|
|
515
|
+
else:
|
|
516
|
+
self.status_label.show_failure(shown_text)
|
|
517
|
+
|
|
518
|
+
def show_outcome(self, outcome: TransportOutcome, account: TransferAccount | None = None) -> None:
|
|
519
|
+
"""Close after a verified success, like Git Extensions; otherwise keep the draft and its details without any implicit second push.
|
|
520
|
+
|
|
521
|
+
A failure starts with the same sentence as the Push console: the branches the server itself refused (a hook,
|
|
522
|
+
for instance) with its message and reason, or else the address and the ``account`` used when the server refused
|
|
523
|
+
it. It ends with Git's own lines without debug traces or stack lines. The console keeps the full result and
|
|
524
|
+
follows its own Keep dialog open preference.
|
|
525
|
+
"""
|
|
526
|
+
if outcome.status == OperationStatus.SUCCESS:
|
|
527
|
+
self.accept()
|
|
528
|
+
return
|
|
529
|
+
self.needs_reread = True
|
|
530
|
+
details = ["Push: " + outcome.status.value]
|
|
531
|
+
for result in outcome.remote_results:
|
|
532
|
+
details.append((result.remote_name or "Url") + " — " + result.endpoint + ": " + result.status.value + " (" + result.effect + ")")
|
|
533
|
+
for result in outcome.ref_results:
|
|
534
|
+
details.append(result.full_ref + ": " + result.status.value + " (" + result.effect + ")")
|
|
535
|
+
for step in outcome.steps:
|
|
536
|
+
details.append(step.operation + ": " + step.status.value)
|
|
537
|
+
if outcome.status == OperationStatus.UNKNOWN:
|
|
538
|
+
details.append("Outcome unknown. Review the same remote before another Push.")
|
|
539
|
+
elif outcome.status != OperationStatus.SUCCESS:
|
|
540
|
+
details.append("Push failed. Review the details and choose how to proceed.")
|
|
541
|
+
headline = ""
|
|
542
|
+
if outcome.diagnostic is not None:
|
|
543
|
+
headline = server_refusal_headline(outcome.server_refusals, outcome.diagnostic.stderr) or failure_headline(outcome.diagnostic, account)
|
|
544
|
+
details.append(failure_summary(outcome.diagnostic))
|
|
545
|
+
self.show_error("\n".join(details), headline)
|
|
546
|
+
self.push_button.setEnabled(False)
|
|
547
|
+
|
|
548
|
+
def browse(self) -> None:
|
|
549
|
+
"""Open a nonblocking directory selector from the folder the typed Url stands for, ``~`` read where that is the usage.
|
|
550
|
+
|
|
551
|
+
A Url that is not an absolute folder leaves the selector at its own start folder; cancelling changes nothing.
|
|
552
|
+
"""
|
|
553
|
+
self._open_folder_chooser(typed_start_dir(self.url_edit.text(), self.typed_path))
|
|
554
|
+
|
|
555
|
+
def _open_folder_chooser(self, start_dir: str) -> None:
|
|
556
|
+
"""Open the directory selector at ``start_dir``, tied to the current draft lifetime; cancelling changes nothing."""
|
|
557
|
+
dialog = QFileDialog(self, "Choose Push repository", start_dir)
|
|
558
|
+
dialog.setFileMode(QFileDialog.FileMode.Directory)
|
|
559
|
+
def selected(filename: str) -> None:
|
|
560
|
+
"""Keep the chosen directory in the draft without starting network work."""
|
|
561
|
+
if not self.closed:
|
|
562
|
+
self.url_edit.setText(filename)
|
|
563
|
+
self.destination_changed.emit()
|
|
564
|
+
dialog.fileSelected.connect(selected)
|
|
565
|
+
dialog.open()
|
|
566
|
+
|
|
567
|
+
def refresh_metrics(self, available_geometry: QRect | None = None) -> None:
|
|
568
|
+
"""Fit the fields and permanent Push action within the available screen."""
|
|
569
|
+
fit_form(self, available_geometry)
|
|
570
|
+
|
|
571
|
+
def showEvent(self, event: QShowEvent) -> None:
|
|
572
|
+
"""Apply current screen bounds without scaling logical coordinates twice."""
|
|
573
|
+
super().showEvent(event)
|
|
574
|
+
self.refresh_metrics()
|
|
575
|
+
|
|
576
|
+
def _closed(self) -> None:
|
|
577
|
+
"""Invalidate pending reads and return keyboard focus to the live opener."""
|
|
578
|
+
self.closed = True
|
|
579
|
+
if self.return_focus is not None and isValid(self.return_focus):
|
|
580
|
+
self.return_focus.setFocus()
|