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,179 @@
|
|
|
1
|
+
"""Reproduce the Git Extensions "Rename branch" window: one row with the new name, then a Rename button."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import QMargins, QRect, QSize, Qt, Signal
|
|
8
|
+
from PySide6.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QVBoxLayout, QWidget
|
|
9
|
+
|
|
10
|
+
from gitpane.config import LOCAL_PREFIX
|
|
11
|
+
from gitpane.operation.console_line import UNCHANGED_OUTCOME_STATUS_SET
|
|
12
|
+
from gitpane.operation.operation_status import OperationStatus
|
|
13
|
+
from gitpane.reference.reference_edit import BranchIntent, BranchMode
|
|
14
|
+
from gitpane.text.typed_text import typed_value
|
|
15
|
+
from gitpane.ui.look.theme_look import follow_theme
|
|
16
|
+
from gitpane.ui.look.windows_control_look import apply_windows_button_look, apply_windows_line_edit_look
|
|
17
|
+
from gitpane.ui.reference.reference_form import ReferenceFormDialog
|
|
18
|
+
from gitpane.ui.widget.dialog_layout import fit_caption_column, fit_capture_control, install_plain_form_palette
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from gitpane.operation.repository_operation import OperationOutcome
|
|
22
|
+
from gitpane.reference.reference_snapshot import ReferenceSnapshot
|
|
23
|
+
|
|
24
|
+
RENAME_BRANCH_TITLE = "Rename branch"
|
|
25
|
+
NEW_NAME_CAPTION = "New name"
|
|
26
|
+
RENAME_BUTTON_TEXT = "Rename"
|
|
27
|
+
# Client area of the Git Extensions capture, 484 x 42 inside the window borders and title bar.
|
|
28
|
+
RENAME_BRANCH_CLIENT_SIZE = QSize(484, 42)
|
|
29
|
+
# Measured on the capture in client pixels at 100 %, visible borders included: "New name" ink x 6-60, y 14-22; name
|
|
30
|
+
# field (72, 9, 344, 23), widened with the window; Rename button (423, 8, 57, 26), 4 px from the right edge.
|
|
31
|
+
NAME_FIELD_RECT = QRect(72, 9, 344, 23)
|
|
32
|
+
RENAME_BUTTON_RECT = QRect(423, 8, 57, 26)
|
|
33
|
+
RENAME_BUTTON_SIZE = RENAME_BUTTON_RECT.size()
|
|
34
|
+
NEW_NAME_CAPTION_LEFT = 6
|
|
35
|
+
# Width of the caption, from its left edge to the name field; a wider caption text widens it.
|
|
36
|
+
NEW_NAME_CAPTION_WIDTH = NAME_FIELD_RECT.left() - NEW_NAME_CAPTION_LEFT
|
|
37
|
+
# The row's margins: the caption's left edge, the button's top edge and its distance to the right and bottom edges.
|
|
38
|
+
RENAME_BRANCH_MARGINS = QMargins(NEW_NAME_CAPTION_LEFT, RENAME_BUTTON_RECT.top(), RENAME_BRANCH_CLIENT_SIZE.width() - RENAME_BUTTON_RECT.right() - 1,
|
|
39
|
+
RENAME_BRANCH_CLIENT_SIZE.height() - RENAME_BUTTON_RECT.bottom() - 1)
|
|
40
|
+
# The field sits 1 px lower than the button in the capture.
|
|
41
|
+
NAME_FIELD_DROP = NAME_FIELD_RECT.top() - RENAME_BUTTON_RECT.top()
|
|
42
|
+
# The caption's top below the button's top, fitted so that Qt's text lands on the captured ink rows (y 14-22) in the
|
|
43
|
+
# Fusion style; the capture's text sits a little above the field's middle.
|
|
44
|
+
CAPTION_DROP = 2
|
|
45
|
+
NAME_FIELD_TO_BUTTON_SPACE = RENAME_BUTTON_RECT.left() - NAME_FIELD_RECT.right() - 1
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RenameBranchDialog(ReferenceFormDialog):
|
|
49
|
+
"""Rename one local branch, like the Git Extensions Rename branch window.
|
|
50
|
+
|
|
51
|
+
The window opens on the branch's current name, selected so typing replaces it, and only freezes a request once
|
|
52
|
+
the name differs. It needs no commit details, so opening it reads nothing beyond the references. There is no
|
|
53
|
+
Cancel button: Escape and the title bar close the window. Git's refusal or failure appears in a message box, as
|
|
54
|
+
Git Extensions shows the error of ``git branch -m``.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
branch_requested = Signal(object)
|
|
58
|
+
|
|
59
|
+
def __init__(self, session_id: str, full_ref: str, target_oid: str, parent: QWidget | None = None) -> None:
|
|
60
|
+
"""Build the single row of the capture, with the branch's current name selected in the field."""
|
|
61
|
+
super().__init__(session_id, parent)
|
|
62
|
+
self.intent: BranchIntent | None = None
|
|
63
|
+
self.setWindowTitle(RENAME_BRANCH_TITLE)
|
|
64
|
+
self.full_ref = full_ref
|
|
65
|
+
self.target_oid = target_oid
|
|
66
|
+
self.current_name = full_ref.removeprefix(LOCAL_PREFIX)
|
|
67
|
+
install_plain_form_palette(self)
|
|
68
|
+
layout = QHBoxLayout(self)
|
|
69
|
+
layout.setContentsMargins(RENAME_BRANCH_MARGINS)
|
|
70
|
+
layout.setSpacing(0)
|
|
71
|
+
self.caption_label = QLabel(NEW_NAME_CAPTION)
|
|
72
|
+
self.name_edit = QLineEdit(self.current_name)
|
|
73
|
+
self.name_edit.setAccessibleName(NEW_NAME_CAPTION)
|
|
74
|
+
self.caption_label.setBuddy(self.name_edit)
|
|
75
|
+
caption_column = QVBoxLayout()
|
|
76
|
+
caption_column.setContentsMargins(0, CAPTION_DROP, 0, 0)
|
|
77
|
+
caption_column.setSpacing(0)
|
|
78
|
+
caption_column.addWidget(self.caption_label)
|
|
79
|
+
caption_column.addStretch(1)
|
|
80
|
+
name_column = QVBoxLayout()
|
|
81
|
+
name_column.setContentsMargins(0, NAME_FIELD_DROP, 0, 0)
|
|
82
|
+
name_column.setSpacing(0)
|
|
83
|
+
name_column.addWidget(self.name_edit)
|
|
84
|
+
name_column.addStretch(1)
|
|
85
|
+
self.submit_button.setText(RENAME_BUTTON_TEXT)
|
|
86
|
+
self.submit_button.setEnabled(False)
|
|
87
|
+
self.apply_theme_look()
|
|
88
|
+
follow_theme(self, self.apply_theme_look)
|
|
89
|
+
layout.addLayout(caption_column)
|
|
90
|
+
layout.addLayout(name_column, 1)
|
|
91
|
+
layout.addSpacing(NAME_FIELD_TO_BUTTON_SPACE)
|
|
92
|
+
layout.addWidget(self.submit_button, 0, Qt.AlignmentFlag.AlignTop)
|
|
93
|
+
# Enter runs Rename from anywhere in the form. The role is set only once the button belongs to the dialog, so Qt
|
|
94
|
+
# records it as the dialog's main default.
|
|
95
|
+
self.submit_button.setDefault(True)
|
|
96
|
+
self.submit_button.clicked.connect(self._submit)
|
|
97
|
+
self.name_edit.textChanged.connect(self._update_intent)
|
|
98
|
+
self.resize(RENAME_BRANCH_CLIENT_SIZE)
|
|
99
|
+
# Git Extensions gives this window the same minimum and maximum height: it only grows in width.
|
|
100
|
+
self.single_row_height = RENAME_BRANCH_CLIENT_SIZE.height()
|
|
101
|
+
self.refresh_metrics()
|
|
102
|
+
self.name_edit.selectAll()
|
|
103
|
+
self.name_edit.setFocus()
|
|
104
|
+
|
|
105
|
+
def apply_theme_look(self) -> None:
|
|
106
|
+
"""Lay the light or dark look of the name field and Rename, decided on the application palette; called again at each switch.
|
|
107
|
+
|
|
108
|
+
Only style sheets and palettes change: the typed name and its selection stay as they are.
|
|
109
|
+
"""
|
|
110
|
+
apply_windows_line_edit_look(self.name_edit)
|
|
111
|
+
apply_windows_button_look(self.submit_button)
|
|
112
|
+
|
|
113
|
+
def fit_capture_controls(self) -> None:
|
|
114
|
+
"""Give the caption and the Rename button the capture's sizes, or more when their text needs it; a wider caption moves the field right."""
|
|
115
|
+
fit_caption_column((self.caption_label,), NEW_NAME_CAPTION_WIDTH)
|
|
116
|
+
fit_capture_control(self.submit_button, RENAME_BUTTON_SIZE)
|
|
117
|
+
|
|
118
|
+
def populate(self, snapshot: ReferenceSnapshot, full_ref: str, target_oid: str) -> None:
|
|
119
|
+
"""Bind the window to the references it was opened on; a name the user already typed is kept."""
|
|
120
|
+
if self.closed or self.busy or self.completed:
|
|
121
|
+
return
|
|
122
|
+
self.snapshot = snapshot
|
|
123
|
+
self.full_ref = full_ref
|
|
124
|
+
self.target_oid = target_oid
|
|
125
|
+
self._update_intent()
|
|
126
|
+
|
|
127
|
+
def _update_intent(self) -> None:
|
|
128
|
+
"""Freeze the typed name into a rename request once it is not empty and differs from the current name.
|
|
129
|
+
|
|
130
|
+
The name is taken without the blanks around it; the field keeps what was typed.
|
|
131
|
+
"""
|
|
132
|
+
self.intent = None
|
|
133
|
+
name = typed_value(self.name_edit.text())
|
|
134
|
+
if self.snapshot is not None and name and name != self.current_name and not self.busy and not self.completed:
|
|
135
|
+
try:
|
|
136
|
+
self.intent = BranchIntent.capture(self.snapshot, self.target_oid, session_id=self.session_id, name=name, mode=BranchMode.RENAME, full_ref=self.full_ref)
|
|
137
|
+
except ValueError:
|
|
138
|
+
self.intent = None
|
|
139
|
+
self.submit_button.setToolTip("")
|
|
140
|
+
self.submit_button.setEnabled(self.intent is not None and not self.closed)
|
|
141
|
+
|
|
142
|
+
def _submit(self) -> None:
|
|
143
|
+
"""Freeze the form and hand its request to the reference form controller once."""
|
|
144
|
+
if self.closed or self.busy or self.completed or self.intent is None or not self.submit_button.isEnabled():
|
|
145
|
+
return
|
|
146
|
+
self.busy = True
|
|
147
|
+
self.name_edit.setEnabled(False)
|
|
148
|
+
self.submit_button.setEnabled(False)
|
|
149
|
+
self.branch_requested.emit(self.intent)
|
|
150
|
+
|
|
151
|
+
def show_outcome(self, outcome: OperationOutcome) -> None:
|
|
152
|
+
"""Close after a successful rename; otherwise explain Git's answer in a message box.
|
|
153
|
+
|
|
154
|
+
A refused or failed rename left the branch as it was, so the typed name stays editable for another try. After
|
|
155
|
+
a partial or unknown result the branch may already carry the new name, so the form only shows what was asked.
|
|
156
|
+
"""
|
|
157
|
+
self.busy = False
|
|
158
|
+
if outcome.status == OperationStatus.SUCCESS:
|
|
159
|
+
self.accept()
|
|
160
|
+
return
|
|
161
|
+
if outcome.status in UNCHANGED_OUTCOME_STATUS_SET:
|
|
162
|
+
self.name_edit.setEnabled(True)
|
|
163
|
+
self._update_intent()
|
|
164
|
+
else:
|
|
165
|
+
self.completed = True
|
|
166
|
+
self.name_edit.setEnabled(False)
|
|
167
|
+
self.show_error(outcome_text(outcome))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def outcome_text(outcome: OperationOutcome) -> str:
|
|
171
|
+
"""Write each step's result and Git's diagnostic, one per line, for the message box of an unsuccessful rename."""
|
|
172
|
+
lines: list[str] = []
|
|
173
|
+
for step in outcome.steps:
|
|
174
|
+
lines.append(step.operation + ": " + step.status.value + " — " + step.effect)
|
|
175
|
+
if outcome.diagnostic is not None:
|
|
176
|
+
lines.append(outcome.diagnostic.detail)
|
|
177
|
+
if not lines:
|
|
178
|
+
lines.append(RENAME_BRANCH_TITLE + ": " + outcome.status.value)
|
|
179
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""Reproduce the Git Extensions "Reset branch" window (Reset another branch to here): a local branch, the commit box, two options."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import QMargins, QRect, QSignalBlocker, QSize, Qt, Signal
|
|
8
|
+
from PySide6.QtWidgets import QApplication, QComboBox, QHBoxLayout, QLabel, QPushButton, QStyle, QVBoxLayout, QWidget
|
|
9
|
+
|
|
10
|
+
from gitpane.config import COMMIT_METADATA_UNAVAILABLE, OK_BUTTON_TEXT, fit_window
|
|
11
|
+
from gitpane.operation.console_line import UNCHANGED_OUTCOME_STATUS_SET
|
|
12
|
+
from gitpane.operation.operation_status import OperationStatus
|
|
13
|
+
from gitpane.reference.reference_edit import BranchIntent, BranchMode
|
|
14
|
+
from gitpane.reference.reference_snapshot import short_reference_name
|
|
15
|
+
from gitpane.ui.look.theme_color import is_dark_palette
|
|
16
|
+
from gitpane.ui.look.theme_look import follow_theme
|
|
17
|
+
from gitpane.ui.look.windows_control_look import WINDOWS_BUTTON_BORDER_WIDTH, WINDOWS_BUTTON_DISABLED_BORDER_COLOR, apply_windows_button_look, apply_windows_combo_look, windows_button_style_sheet
|
|
18
|
+
from gitpane.ui.reference.commit_summary import GIT_EXTENSIONS_COMPACT_LAST_ROW_HEIGHT, CommitSummary
|
|
19
|
+
from gitpane.ui.reference.reference_form import ReferenceFormDialog
|
|
20
|
+
from gitpane.ui.widget.dialog_layout import install_plain_form_palette
|
|
21
|
+
from gitpane.ui.widget.long_text import ElidedCheckBox
|
|
22
|
+
from gitpane.ui.widget.standard_icon_label import StandardIconLabel
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
from datetime import datetime
|
|
27
|
+
|
|
28
|
+
from gitpane.history.repository_value import Commit
|
|
29
|
+
from gitpane.operation.repository_operation import OperationOutcome
|
|
30
|
+
from gitpane.reference.reference_snapshot import ReferenceSnapshot
|
|
31
|
+
|
|
32
|
+
RESET_BRANCH_TITLE = "Reset branch"
|
|
33
|
+
WARNING_FIRST_LINE = "You can only reset a branch safely if there is a direct path from it to selected revision."
|
|
34
|
+
WARNING_SECOND_LINE = "Forcing a branch to reset if it has not been merged might leave some commits unreachable."
|
|
35
|
+
RESET_LOCAL_BRANCH_CAPTION = "Reset local branch:"
|
|
36
|
+
CHECKOUT_AFTER_RESET_LABEL = "Checkout branch after reset"
|
|
37
|
+
FORCE_RESET_LABEL = "Force reset for a non-fast-forward reset"
|
|
38
|
+
CANCEL_BUTTON_TEXT = "Cancel"
|
|
39
|
+
NO_AVAILABLE_BRANCH_CAUSE = "No other available local branches"
|
|
40
|
+
NON_FAST_FORWARD_CAUSE = "This reset is not a fast-forward. Check Force reset to continue."
|
|
41
|
+
# A disabled OK turns this red while the chosen reset is not a fast-forward and Force is unchecked, so the user sees
|
|
42
|
+
# why OK cannot be pressed. GitPane shows this state since its first Reset branch window; the capture, taken without a
|
|
43
|
+
# chosen branch, does not show it.
|
|
44
|
+
NON_FAST_FORWARD_BUTTON_COLOR = "#ff8080"
|
|
45
|
+
NON_FAST_FORWARD_BUTTON_TEXT_COLOR = "#000000"
|
|
46
|
+
# The border makes Qt fill the whole face with the red in a dark theme too, where the native button would only tint it.
|
|
47
|
+
NON_FAST_FORWARD_BUTTON_STYLE = (f" QPushButton:disabled {{ background-color: {NON_FAST_FORWARD_BUTTON_COLOR}; color: {NON_FAST_FORWARD_BUTTON_TEXT_COLOR};"
|
|
48
|
+
f" border: {WINDOWS_BUTTON_BORDER_WIDTH}px solid {WINDOWS_BUTTON_DISABLED_BORDER_COLOR}; }}")
|
|
49
|
+
# Client area of the Git Extensions capture, 545 x 347 inside the window borders and title bar.
|
|
50
|
+
RESET_BRANCH_CLIENT_SIZE = QSize(545, 347)
|
|
51
|
+
# Measured on the capture in client pixels at 100 %, visible borders included: warning icon 16 x 16 at (16, 16) and
|
|
52
|
+
# its two text lines from x 41, ink y 16-42; "Reset local branch:" ink x 17-114, y 58-66; branch list
|
|
53
|
+
# (19, 73, 507, 23); commit frame (12, 108, 521, 141) with its title ink from x 21; check box squares at (14, 256) and
|
|
54
|
+
# (14, 281); OK (342, 307, 89, 23) and Cancel (439, 307, 89, 23), 8 px apart, 17 px from the right and bottom edges.
|
|
55
|
+
WARNING_ICON_RECT = QRect(16, 16, 16, 16)
|
|
56
|
+
WARNING_TEXT_LEFT = 40
|
|
57
|
+
RESET_BRANCH_LIST_RECT = QRect(19, 73, 507, 23)
|
|
58
|
+
COMMIT_FRAME_RECT = QRect(12, 108, 521, 141)
|
|
59
|
+
CHECK_BOX_LEFT = 14
|
|
60
|
+
OK_BUTTON_RECT = QRect(342, 307, 89, 23)
|
|
61
|
+
CANCEL_BUTTON_RECT = QRect(439, 307, 89, 23)
|
|
62
|
+
RESET_BUTTON_SIZE = OK_BUTTON_RECT.size()
|
|
63
|
+
BUTTON_SPACING = CANCEL_BUTTON_RECT.left() - OK_BUTTON_RECT.right() - 1
|
|
64
|
+
# The body runs from the commit frame's left edge to its right edge; the buttons end 17 px above the bottom edge.
|
|
65
|
+
RESET_BRANCH_MARGINS = QMargins(COMMIT_FRAME_RECT.left(), 0, RESET_BRANCH_CLIENT_SIZE.width() - COMMIT_FRAME_RECT.right() - 1,
|
|
66
|
+
RESET_BRANCH_CLIENT_SIZE.height() - OK_BUTTON_RECT.bottom() - 1)
|
|
67
|
+
# Rows measured from the body's left edge.
|
|
68
|
+
WARNING_ICON_INDENT = WARNING_ICON_RECT.left() - COMMIT_FRAME_RECT.left()
|
|
69
|
+
WARNING_ICON_TO_TEXT_SPACE = WARNING_TEXT_LEFT - WARNING_ICON_RECT.right() - 1
|
|
70
|
+
# A label writes its text about 1 px after its own left edge, so the caption starts 1 px left of its captured ink at x 17.
|
|
71
|
+
CAPTION_LEFT = 16
|
|
72
|
+
RESET_BRANCH_CAPTION_INDENT = CAPTION_LEFT - COMMIT_FRAME_RECT.left()
|
|
73
|
+
BRANCH_LIST_INDENT = RESET_BRANCH_LIST_RECT.left() - COMMIT_FRAME_RECT.left()
|
|
74
|
+
CHECK_BOX_INDENT = CHECK_BOX_LEFT - COMMIT_FRAME_RECT.left()
|
|
75
|
+
BUTTON_ROW_RIGHT_INDENT = RESET_BRANCH_CLIENT_SIZE.width() - CANCEL_BUTTON_RECT.right() - 1 - RESET_BRANCH_MARGINS.right()
|
|
76
|
+
# Vertical positions, fitted so that Qt's text, list, frame and squares land where the capture has them in the Fusion
|
|
77
|
+
# style: the warning text's top row, then the spaces between the rows below it.
|
|
78
|
+
WARNING_TEXT_TOP = 12
|
|
79
|
+
WARNING_ICON_DROP = WARNING_ICON_RECT.top() - WARNING_TEXT_TOP
|
|
80
|
+
WARNING_TO_CAPTION_SPACE = 10
|
|
81
|
+
CAPTION_TO_BRANCH_LIST_SPACE = 3
|
|
82
|
+
BRANCH_LIST_TO_FRAME_SPACE = 3
|
|
83
|
+
FRAME_TO_CHECKOUT_SPACE = 4
|
|
84
|
+
CHECKOUT_TO_FORCE_SPACE = 4
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ResetOtherBranchDialog(ReferenceFormDialog):
|
|
88
|
+
"""Move another local branch to a commit, and optionally check it out, like the Git Extensions Reset branch window.
|
|
89
|
+
|
|
90
|
+
The window freezes a request once a branch is chosen. Whether that reset is a fast-forward is asked through
|
|
91
|
+
``ancestry_requested`` the first time each branch is chosen; the reference form controller answers from the graph in memory
|
|
92
|
+
when it can, and from Git otherwise. A reset that is not a fast-forward needs Force, and the disabled OK turns red
|
|
93
|
+
until Force is checked. Checkout branch after reset starts checked at every opening. Git still checks the
|
|
94
|
+
fast-forward itself when the reset starts, so the answer shown here only drives the form.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
branch_requested = Signal(object)
|
|
98
|
+
ancestry_requested = Signal(str, str)
|
|
99
|
+
|
|
100
|
+
def __init__(self, session_id: str, target_oid: str, parent: QWidget | None = None, *, clock: Callable[[], datetime] | None = None) -> None:
|
|
101
|
+
"""Build the warning, the branch list, the commit box, the two check boxes and the OK and Cancel buttons of the capture.
|
|
102
|
+
|
|
103
|
+
``clock`` dates the commit box, which reads the system time when it is absent.
|
|
104
|
+
"""
|
|
105
|
+
super().__init__(session_id, parent)
|
|
106
|
+
self.intent: BranchIntent | None = None
|
|
107
|
+
self.setWindowTitle(RESET_BRANCH_TITLE)
|
|
108
|
+
self.target_oid = target_oid
|
|
109
|
+
self.full_ref = ""
|
|
110
|
+
# The local branches the reference form controller offered, in the list's order.
|
|
111
|
+
self.candidate_refs: tuple[str, ...] = ()
|
|
112
|
+
# Whether resetting each branch already checked is a fast-forward, and the branches still waiting for the answer.
|
|
113
|
+
self.ancestry_by_ref: dict[str, bool] = {}
|
|
114
|
+
self.pending_ancestry_set: set[str] = set()
|
|
115
|
+
# True once the commit box shows the target's subject, author and date; OK waits for it.
|
|
116
|
+
self.metadata_ready = False
|
|
117
|
+
# Set when Git could not read the target commit: a branch is never reset to a commit nobody could show.
|
|
118
|
+
self.metadata_failed = False
|
|
119
|
+
# Whether OK currently carries the red non-fast-forward look, so its style sheet is only rewritten on a change.
|
|
120
|
+
self.blocked_look = False
|
|
121
|
+
install_plain_form_palette(self)
|
|
122
|
+
layout = QVBoxLayout(self)
|
|
123
|
+
layout.setContentsMargins(RESET_BRANCH_MARGINS)
|
|
124
|
+
layout.setSpacing(0)
|
|
125
|
+
layout.addSpacing(WARNING_TEXT_TOP)
|
|
126
|
+
layout.addLayout(self._warning_row())
|
|
127
|
+
layout.addSpacing(WARNING_TO_CAPTION_SPACE)
|
|
128
|
+
self.caption_label = QLabel(RESET_LOCAL_BRANCH_CAPTION)
|
|
129
|
+
layout.addLayout(self._indented_row(self.caption_label, RESET_BRANCH_CAPTION_INDENT))
|
|
130
|
+
layout.addSpacing(CAPTION_TO_BRANCH_LIST_SPACE)
|
|
131
|
+
self.branch_selector = QComboBox()
|
|
132
|
+
self.branch_selector.setAccessibleName(RESET_LOCAL_BRANCH_CAPTION.removesuffix(":"))
|
|
133
|
+
# The list takes the width the window gives it, whatever the length of the branch names.
|
|
134
|
+
self.branch_selector.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
|
|
135
|
+
self.branch_selector.setMinimumContentsLength(1)
|
|
136
|
+
self.caption_label.setBuddy(self.branch_selector)
|
|
137
|
+
branch_row = QHBoxLayout()
|
|
138
|
+
branch_row.setContentsMargins(BRANCH_LIST_INDENT, 0, RESET_BRANCH_CLIENT_SIZE.width() - RESET_BRANCH_LIST_RECT.right() - 1 - RESET_BRANCH_MARGINS.right(), 0)
|
|
139
|
+
branch_row.addWidget(self.branch_selector)
|
|
140
|
+
layout.addLayout(branch_row)
|
|
141
|
+
layout.addSpacing(BRANCH_LIST_TO_FRAME_SPACE)
|
|
142
|
+
# The v6.0 commit box: its last row is as high as the others and long values are cut instead of wrapped.
|
|
143
|
+
self.commit_summary = CommitSummary(target_oid, git_extensions_look=True, last_row_height=GIT_EXTENSIONS_COMPACT_LAST_ROW_HEIGHT, single_line=True, clock=clock)
|
|
144
|
+
layout.addWidget(self.commit_summary)
|
|
145
|
+
layout.addSpacing(FRAME_TO_CHECKOUT_SPACE)
|
|
146
|
+
# Checked at every opening and never remembered, unlike Git Extensions, whose capture shows it unchecked.
|
|
147
|
+
self.checkout_checkbox = ElidedCheckBox(CHECKOUT_AFTER_RESET_LABEL, open_whole=True)
|
|
148
|
+
self.checkout_checkbox.setChecked(True)
|
|
149
|
+
layout.addLayout(self._indented_row(self.checkout_checkbox, CHECK_BOX_INDENT, stretch=True))
|
|
150
|
+
layout.addSpacing(CHECKOUT_TO_FORCE_SPACE)
|
|
151
|
+
self.force_checkbox = ElidedCheckBox(FORCE_RESET_LABEL, open_whole=True)
|
|
152
|
+
layout.addLayout(self._indented_row(self.force_checkbox, CHECK_BOX_INDENT, stretch=True))
|
|
153
|
+
layout.addStretch(1)
|
|
154
|
+
self.submit_button.setText(OK_BUTTON_TEXT)
|
|
155
|
+
self.submit_button.setEnabled(False)
|
|
156
|
+
self.cancel_button = QPushButton(CANCEL_BUTTON_TEXT)
|
|
157
|
+
# Only OK may be the default button; see the Enter handling of the reference forms.
|
|
158
|
+
self.cancel_button.setAutoDefault(False)
|
|
159
|
+
button_row = QHBoxLayout()
|
|
160
|
+
button_row.setContentsMargins(0, 0, BUTTON_ROW_RIGHT_INDENT, 0)
|
|
161
|
+
button_row.setSpacing(BUTTON_SPACING)
|
|
162
|
+
button_row.addStretch(1)
|
|
163
|
+
button_row.addWidget(self.submit_button)
|
|
164
|
+
button_row.addWidget(self.cancel_button)
|
|
165
|
+
layout.addLayout(button_row)
|
|
166
|
+
self.apply_theme_look()
|
|
167
|
+
follow_theme(self, self.apply_theme_look)
|
|
168
|
+
# Enter runs OK from anywhere in the form. The role is set only once the button belongs to the dialog, so Qt
|
|
169
|
+
# records it as the dialog's main default.
|
|
170
|
+
self.submit_button.setDefault(True)
|
|
171
|
+
self.submit_button.clicked.connect(self._submit)
|
|
172
|
+
self.cancel_button.clicked.connect(self.reject)
|
|
173
|
+
self.branch_selector.currentIndexChanged.connect(self._branch_changed)
|
|
174
|
+
self.checkout_checkbox.toggled.connect(self._update_intent)
|
|
175
|
+
self.force_checkbox.toggled.connect(self._update_intent)
|
|
176
|
+
self.resize(RESET_BRANCH_CLIENT_SIZE)
|
|
177
|
+
self.refresh_metrics()
|
|
178
|
+
self.branch_selector.setFocus()
|
|
179
|
+
|
|
180
|
+
def apply_theme_look(self) -> None:
|
|
181
|
+
"""Lay the light or dark look of the branch list, OK and Cancel, decided on the application palette; called again at each switch.
|
|
182
|
+
|
|
183
|
+
Git Extensions draws the branch list like a field one can type into; GitPane keeps it a plain choice among the
|
|
184
|
+
offered branches, so no typed name can reach the reset. OK keeps its red look while a reset needs Force. Only
|
|
185
|
+
style sheets and palettes change: the branch, both check boxes and whether OK can be used stay as they are.
|
|
186
|
+
"""
|
|
187
|
+
apply_windows_combo_look(self.branch_selector, editable=True)
|
|
188
|
+
apply_windows_button_look(self.cancel_button)
|
|
189
|
+
self._lay_submit_button_look()
|
|
190
|
+
|
|
191
|
+
def fit_capture_controls(self) -> None:
|
|
192
|
+
"""Give OK and Cancel the capture's size, or more when the system text needs it; both keep the same size.
|
|
193
|
+
|
|
194
|
+
The capture's 89 x 23 holds Windows' Segoe UI 9 pt; a larger font would cut the button text at that height.
|
|
195
|
+
"""
|
|
196
|
+
size = QSize(RESET_BUTTON_SIZE)
|
|
197
|
+
for button in (self.submit_button, self.cancel_button):
|
|
198
|
+
size = size.expandedTo(button.sizeHint())
|
|
199
|
+
for button in (self.submit_button, self.cancel_button):
|
|
200
|
+
button.setFixedSize(size)
|
|
201
|
+
|
|
202
|
+
def refresh_metrics(self, available: QRect | None = None) -> None:
|
|
203
|
+
"""Size the capture's controls, then keep a minimum size at which both warning sentences stay whole.
|
|
204
|
+
|
|
205
|
+
Unlike the other reference forms, Reset branch keeps a minimum size, so the user can make it neither narrow nor
|
|
206
|
+
short enough to cut a sentence of the warning shown before a forced reset, as long as the usable screen area
|
|
207
|
+
holds that minimum. The minimum is laid here, when the font, palette, density or screen changes, never while the
|
|
208
|
+
window is resized, and fit_window brings it back to the usable screen area: with a large font on a small screen
|
|
209
|
+
the minimum is relaxed, and the sentences can lose their last lines again.
|
|
210
|
+
"""
|
|
211
|
+
super().refresh_metrics(available)
|
|
212
|
+
minimum = self._whole_warning_minimum_size()
|
|
213
|
+
fit_window(self, minimum.width(), minimum.height(), available)
|
|
214
|
+
|
|
215
|
+
def _whole_warning_minimum_size(self) -> QSize:
|
|
216
|
+
"""Return the minimum size, taken at the capture's width, at which every row, wrapped warning sentences included, gets the height it needs.
|
|
217
|
+
|
|
218
|
+
A top-level window keeps as its minimum height the one of its layout, where each wrapped sentence counts for a
|
|
219
|
+
single line, so a narrower window would leave the sentences less height than their extra lines need. The width
|
|
220
|
+
is the capture's, or the layout's minimum when that is wider: the capture's width keeps the opening height with
|
|
221
|
+
the captures' font, and at any larger width the sentences take fewer lines, so the height asked here is enough.
|
|
222
|
+
"""
|
|
223
|
+
layout = self.layout()
|
|
224
|
+
layout.activate()
|
|
225
|
+
layout_minimum = layout.totalMinimumSize()
|
|
226
|
+
width = max(RESET_BRANCH_CLIENT_SIZE.width(), layout_minimum.width())
|
|
227
|
+
height = layout_minimum.height()
|
|
228
|
+
if layout.hasHeightForWidth():
|
|
229
|
+
height = max(height, layout.totalMinimumHeightForWidth(width))
|
|
230
|
+
return QSize(width, height)
|
|
231
|
+
|
|
232
|
+
def _warning_row(self) -> QHBoxLayout:
|
|
233
|
+
"""Build the warning icon and its two sentences, each wrapped onto as many lines as the window's width asks, never cut.
|
|
234
|
+
|
|
235
|
+
The sentences are prose, like the other captions of the reference forms: plain text, neither selectable nor broken
|
|
236
|
+
in the middle of a word. GitPane fits every new window to its content at its first show, wrapped texts included,
|
|
237
|
+
so the window opens tall enough for the lines the sentences take; with the captures' font each sentence holds on
|
|
238
|
+
one line, as in the capture. After the opening, the window's minimum size keeps the sentences whole however small
|
|
239
|
+
the user makes the window, as long as the usable screen area holds that minimum.
|
|
240
|
+
"""
|
|
241
|
+
self.warning_icon = StandardIconLabel(WARNING_ICON_RECT.size(), QStyle.StandardPixmap.SP_MessageBoxWarning)
|
|
242
|
+
icon_column = QVBoxLayout()
|
|
243
|
+
icon_column.setContentsMargins(0, WARNING_ICON_DROP, 0, 0)
|
|
244
|
+
icon_column.addWidget(self.warning_icon)
|
|
245
|
+
icon_column.addStretch(1)
|
|
246
|
+
self.warning_labels = [QLabel(WARNING_FIRST_LINE), QLabel(WARNING_SECOND_LINE)]
|
|
247
|
+
for label in self.warning_labels:
|
|
248
|
+
label.setTextFormat(Qt.TextFormat.PlainText)
|
|
249
|
+
label.setWordWrap(True)
|
|
250
|
+
text_column = QVBoxLayout()
|
|
251
|
+
text_column.setContentsMargins(0, 0, 0, 0)
|
|
252
|
+
text_column.setSpacing(0)
|
|
253
|
+
for label in self.warning_labels:
|
|
254
|
+
text_column.addWidget(label)
|
|
255
|
+
row = QHBoxLayout()
|
|
256
|
+
row.setContentsMargins(WARNING_ICON_INDENT, 0, 0, 0)
|
|
257
|
+
row.setSpacing(WARNING_ICON_TO_TEXT_SPACE)
|
|
258
|
+
row.addLayout(icon_column)
|
|
259
|
+
row.addLayout(text_column, 1)
|
|
260
|
+
return row
|
|
261
|
+
|
|
262
|
+
def _indented_row(self, widget: QWidget, indent: int, *, stretch: bool = False) -> QHBoxLayout:
|
|
263
|
+
"""Place one control this far right of the body's left edge; ``stretch`` lets it take the rest of the width."""
|
|
264
|
+
row = QHBoxLayout()
|
|
265
|
+
row.setContentsMargins(indent, 0, 0, 0)
|
|
266
|
+
row.setSpacing(0)
|
|
267
|
+
row.addWidget(widget, 1 if stretch else 0)
|
|
268
|
+
if not stretch:
|
|
269
|
+
row.addStretch(1)
|
|
270
|
+
return row
|
|
271
|
+
|
|
272
|
+
def populate(self, snapshot: ReferenceSnapshot, target_oid: str, candidate_refs: tuple[str, ...]) -> None:
|
|
273
|
+
"""Offer the resettable branches of the references the window was opened on, keeping a branch the user already chose.
|
|
274
|
+
|
|
275
|
+
No branch is chosen at first, as in Git Extensions. Each new capture drops the fast-forward answers and the Force
|
|
276
|
+
consent, which were given for the previous one.
|
|
277
|
+
"""
|
|
278
|
+
if self.closed or self.busy or self.completed:
|
|
279
|
+
return
|
|
280
|
+
self.snapshot = snapshot
|
|
281
|
+
self.target_oid = target_oid
|
|
282
|
+
self.candidate_refs = candidate_refs
|
|
283
|
+
self.ancestry_by_ref.clear()
|
|
284
|
+
self.pending_ancestry_set.clear()
|
|
285
|
+
with QSignalBlocker(self.force_checkbox):
|
|
286
|
+
self.force_checkbox.setChecked(False)
|
|
287
|
+
self.commit_summary.show_references(snapshot)
|
|
288
|
+
selected_ref = self.branch_selector.currentData()
|
|
289
|
+
with QSignalBlocker(self.branch_selector):
|
|
290
|
+
self.branch_selector.clear()
|
|
291
|
+
for full_ref in candidate_refs:
|
|
292
|
+
self.branch_selector.addItem(short_reference_name(full_ref), full_ref)
|
|
293
|
+
self.branch_selector.setItemData(self.branch_selector.count() - 1, full_ref, Qt.ItemDataRole.ToolTipRole)
|
|
294
|
+
self.branch_selector.setCurrentIndex(self.branch_selector.findData(selected_ref) if selected_ref else -1)
|
|
295
|
+
self.branch_selector.setEnabled(bool(candidate_refs))
|
|
296
|
+
self._show_branch_tooltip()
|
|
297
|
+
self._update_intent()
|
|
298
|
+
|
|
299
|
+
def _show_branch_tooltip(self) -> None:
|
|
300
|
+
"""Show the chosen branch's full name as the list's tooltip, or why nothing can be chosen."""
|
|
301
|
+
if not self.branch_selector.count():
|
|
302
|
+
self.branch_selector.setToolTip(NO_AVAILABLE_BRANCH_CAUSE)
|
|
303
|
+
return
|
|
304
|
+
self.branch_selector.setToolTip(self.branch_selector.currentData() or "")
|
|
305
|
+
|
|
306
|
+
def _branch_changed(self) -> None:
|
|
307
|
+
"""Ask for fresh Force consent after choosing another branch, then freeze the new request."""
|
|
308
|
+
with QSignalBlocker(self.force_checkbox):
|
|
309
|
+
self.force_checkbox.setChecked(False)
|
|
310
|
+
self._show_branch_tooltip()
|
|
311
|
+
self._update_intent()
|
|
312
|
+
|
|
313
|
+
def show_ancestry(self, full_ref: str, fast_forward: bool | None, cause: str = "") -> None:
|
|
314
|
+
"""Record whether resetting a branch is a fast-forward, or explain why it could not be told for the chosen branch."""
|
|
315
|
+
self.pending_ancestry_set.discard(full_ref)
|
|
316
|
+
if self.closed or self.busy or self.completed:
|
|
317
|
+
return
|
|
318
|
+
if fast_forward is not None:
|
|
319
|
+
self.ancestry_by_ref[full_ref] = fast_forward
|
|
320
|
+
self._update_intent()
|
|
321
|
+
return
|
|
322
|
+
if full_ref == self.full_ref:
|
|
323
|
+
self.show_error(cause)
|
|
324
|
+
|
|
325
|
+
def show_loaded_commit(self, commit: Commit) -> None:
|
|
326
|
+
"""Fill the commit box from a commit the graph already holds, without asking Git."""
|
|
327
|
+
if self.closed or commit.oid != self.commit_summary.target_oid:
|
|
328
|
+
return
|
|
329
|
+
self.commit_summary.show_loaded_commit(commit)
|
|
330
|
+
self.metadata_ready = True
|
|
331
|
+
self._update_intent()
|
|
332
|
+
|
|
333
|
+
def show_metadata(self, fields: tuple[str, ...] | None, cause: str = "") -> None:
|
|
334
|
+
"""Fill the commit box from Git's answer, or refuse the target Git could not read and explain why."""
|
|
335
|
+
if self.closed:
|
|
336
|
+
return
|
|
337
|
+
if fields is None:
|
|
338
|
+
self.metadata_ready = False
|
|
339
|
+
self.metadata_failed = True
|
|
340
|
+
self.commit_summary.subject_label.setText(COMMIT_METADATA_UNAVAILABLE)
|
|
341
|
+
self._update_intent()
|
|
342
|
+
self.show_error(cause)
|
|
343
|
+
return
|
|
344
|
+
oid_field_index = 0
|
|
345
|
+
if fields[oid_field_index] != self.commit_summary.target_oid:
|
|
346
|
+
return
|
|
347
|
+
self.commit_summary.show_commit(fields)
|
|
348
|
+
self.metadata_ready = True
|
|
349
|
+
self._update_intent()
|
|
350
|
+
|
|
351
|
+
def _update_intent(self) -> None:
|
|
352
|
+
"""Freeze the chosen branch and both options into the request OK would send, and ask whether it is a fast-forward.
|
|
353
|
+
|
|
354
|
+
The request exists as soon as a branch is chosen, so a reset that needs Force is still refused by the business
|
|
355
|
+
rules if it is sent without it; OK only becomes usable once the commit is shown and the reset is a fast-forward
|
|
356
|
+
or forced.
|
|
357
|
+
"""
|
|
358
|
+
if self.busy or self.completed or self.closed:
|
|
359
|
+
return
|
|
360
|
+
self.intent = None
|
|
361
|
+
self.full_ref = self.branch_selector.currentData() or ""
|
|
362
|
+
fast_forward = None
|
|
363
|
+
if self.snapshot is not None and self.full_ref:
|
|
364
|
+
old_oid = self.snapshot.object_oid_by_name.get(self.full_ref)
|
|
365
|
+
fast_forward = self.ancestry_by_ref.get(self.full_ref)
|
|
366
|
+
if fast_forward is None and old_oid and self.full_ref not in self.pending_ancestry_set:
|
|
367
|
+
self.pending_ancestry_set.add(self.full_ref)
|
|
368
|
+
self.ancestry_requested.emit(self.full_ref, old_oid)
|
|
369
|
+
# The graph in memory answers at once, while the signal is still being delivered.
|
|
370
|
+
fast_forward = self.ancestry_by_ref.get(self.full_ref)
|
|
371
|
+
blocked = fast_forward is False and not self.force_checkbox.isChecked()
|
|
372
|
+
self._paint_submit_button(blocked)
|
|
373
|
+
self.submit_button.setToolTip(NON_FAST_FORWARD_CAUSE if blocked else "")
|
|
374
|
+
if self.snapshot is not None and self.full_ref and not self.metadata_failed:
|
|
375
|
+
try:
|
|
376
|
+
self.intent = BranchIntent.capture(self.snapshot, self.target_oid, session_id=self.session_id, checkout=self.checkout_checkbox.isChecked(),
|
|
377
|
+
mode=BranchMode.MOVE, full_ref=self.full_ref, force_reset=self.force_checkbox.isChecked())
|
|
378
|
+
except ValueError as error:
|
|
379
|
+
self.submit_button.setToolTip(self.text_filter.redact(str(error)))
|
|
380
|
+
self.submit_button.setEnabled(self.intent is not None and self.metadata_ready and fast_forward is not None and not blocked)
|
|
381
|
+
|
|
382
|
+
def _paint_submit_button(self, blocked: bool) -> None:
|
|
383
|
+
"""Turn OK red while it is disabled for a missing Force, or back to its usual look; nothing is rewritten when that does not change."""
|
|
384
|
+
if blocked == self.blocked_look:
|
|
385
|
+
return
|
|
386
|
+
self.blocked_look = blocked
|
|
387
|
+
self._lay_submit_button_look()
|
|
388
|
+
|
|
389
|
+
def _lay_submit_button_look(self) -> None:
|
|
390
|
+
"""Give OK the Windows look of the light theme, or the palette's in a dark one, red while disabled for a missing Force.
|
|
391
|
+
|
|
392
|
+
The theme is the application palette's: at a switch this runs before Qt hands the new palette to the button.
|
|
393
|
+
"""
|
|
394
|
+
base_style = "" if is_dark_palette(QApplication.palette()) else windows_button_style_sheet()
|
|
395
|
+
self.submit_button.setStyleSheet(base_style + (NON_FAST_FORWARD_BUTTON_STYLE if self.blocked_look else ""))
|
|
396
|
+
|
|
397
|
+
def _submit(self) -> None:
|
|
398
|
+
"""Freeze the form and hand its request to the reference form controller once; Cancel stays disabled until the result."""
|
|
399
|
+
if self.closed or self.busy or self.completed or self.intent is None or not self.submit_button.isEnabled():
|
|
400
|
+
return
|
|
401
|
+
self.busy = True
|
|
402
|
+
self._set_choices_enabled(False)
|
|
403
|
+
self.submit_button.setEnabled(False)
|
|
404
|
+
self.cancel_button.setEnabled(False)
|
|
405
|
+
self.branch_requested.emit(self.intent)
|
|
406
|
+
|
|
407
|
+
def show_outcome(self, outcome: OperationOutcome) -> None:
|
|
408
|
+
"""Close after a successful reset; keep the form after any other result while the console explains it.
|
|
409
|
+
|
|
410
|
+
A refused or failed reset moved nothing, so every choice stays as it was and editable for another try. After a
|
|
411
|
+
partial or unknown result the branch may have moved, so the form only shows what was asked and can be closed.
|
|
412
|
+
"""
|
|
413
|
+
self.busy = False
|
|
414
|
+
self.cancel_button.setEnabled(True)
|
|
415
|
+
if outcome.status == OperationStatus.SUCCESS:
|
|
416
|
+
self.accept()
|
|
417
|
+
return
|
|
418
|
+
if outcome.status in UNCHANGED_OUTCOME_STATUS_SET:
|
|
419
|
+
self._set_choices_enabled(True)
|
|
420
|
+
self._update_intent()
|
|
421
|
+
return
|
|
422
|
+
self.completed = True
|
|
423
|
+
self._set_choices_enabled(False)
|
|
424
|
+
self.submit_button.setEnabled(False)
|
|
425
|
+
|
|
426
|
+
def _set_choices_enabled(self, enabled: bool) -> None:
|
|
427
|
+
"""Enable or disable the branch list and both options together; an empty list always stays disabled."""
|
|
428
|
+
self.branch_selector.setEnabled(enabled and self.branch_selector.count() > 0)
|
|
429
|
+
self.checkout_checkbox.setEnabled(enabled)
|
|
430
|
+
self.force_checkbox.setEnabled(enabled)
|