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,461 @@
|
|
|
1
|
+
"""Display native rebase progress without owning any Git command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from PySide6.QtCore import Qt, Signal
|
|
9
|
+
from PySide6.QtGui import QPalette
|
|
10
|
+
from PySide6.QtWidgets import QAbstractItemView, QApplication, QCheckBox, QDialog, QHBoxLayout, QHeaderView, QPlainTextEdit, QPushButton, QTableWidget, QTableWidgetItem, QToolButton, QVBoxLayout, QWidget
|
|
11
|
+
|
|
12
|
+
from gitpane.config import DARK_BACKGROUND_LIGHTNESS_LIMIT, ERROR_DARK_COLOR, ERROR_LIGHT_COLOR, OPERATION_HEIGHT, OPERATION_WIDTH, SHORT_OID_LENGTH, SPACING_MD, SPACING_SM
|
|
13
|
+
from gitpane.operation.console_line import COMPLETED_STATUS_SET
|
|
14
|
+
from gitpane.preference.preference_store import REBASE_PROMPT_PREFERENCE_KEY
|
|
15
|
+
from gitpane.sequencer.rebase_state import REBASE_CONTINUABLE_STATE_SET, REBASE_STOPPED_STATE_SET, RebaseSnapshot, can_abort_rebase
|
|
16
|
+
from gitpane.ui.look.theme_look import follow_theme
|
|
17
|
+
from gitpane.ui.widget.commit_date_text import format_commit_date
|
|
18
|
+
from gitpane.ui.widget.content_growth import ContentGrowthMixin, OpeningFit
|
|
19
|
+
from gitpane.ui.widget.content_measure import LayoutContentMeasure
|
|
20
|
+
from gitpane.ui.widget.dialog_layout import content_height, fit_form
|
|
21
|
+
from gitpane.ui.widget.flow_layout import FlowLayout
|
|
22
|
+
from gitpane.ui.widget.long_text import WrappingLabel
|
|
23
|
+
from gitpane.ui.widget.scrolled_text import ScrolledText
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from collections.abc import Callable
|
|
27
|
+
|
|
28
|
+
from PySide6.QtCore import QRect
|
|
29
|
+
|
|
30
|
+
from gitpane.operation.operation_status import OperationStatus
|
|
31
|
+
from gitpane.preference.preference_store import PreferenceStore
|
|
32
|
+
from gitpane.sequencer.rebase_state import RebaseCommit, RebaseStep
|
|
33
|
+
|
|
34
|
+
REBASE_COLUMN_TITLES = ("Status", "Action", "Subject", "Author", "Date", "Commit hash")
|
|
35
|
+
REBASE_STATUS_COLUMN, REBASE_ACTION_COLUMN, REBASE_MESSAGE_COLUMN, REBASE_AUTHOR_COLUMN, REBASE_DATE_COLUMN, REBASE_COMMIT_COLUMN = range(len(REBASE_COLUMN_TITLES))
|
|
36
|
+
REBASE_READY_LIGHT_COLOR = "#176b35"
|
|
37
|
+
REBASE_READY_DARK_COLOR = "#75d69a"
|
|
38
|
+
REBASE_EDIT_LIGHT_COLOR = "#b45309"
|
|
39
|
+
REBASE_EDIT_DARK_COLOR = "#ffb454"
|
|
40
|
+
CONFLICT_BUTTON_BACKGROUND = "#ffe59a"
|
|
41
|
+
CONFLICT_BUTTON_HOVER_BACKGROUND = "#ffd166"
|
|
42
|
+
CONFLICT_BUTTON_PRESSED_BACKGROUND = "#e7b53d"
|
|
43
|
+
CONFLICT_BUTTON_FOREGROUND = "#332500"
|
|
44
|
+
CONFLICT_BUTTON_BORDER = "#b47700"
|
|
45
|
+
CONTINUE_BUTTON_BACKGROUND = "#c8efd4"
|
|
46
|
+
CONTINUE_BUTTON_HOVER_BACKGROUND = "#98e0ad"
|
|
47
|
+
CONTINUE_BUTTON_PRESSED_BACKGROUND = "#66c884"
|
|
48
|
+
CONTINUE_BUTTON_FOREGROUND = "#153c24"
|
|
49
|
+
CONTINUE_BUTTON_BORDER = "#408355"
|
|
50
|
+
REBASE_ACTION_BORDER_WIDTH = 1
|
|
51
|
+
REBASE_ACTION_CORNER_RADIUS = 2
|
|
52
|
+
DIAGNOSTIC_VISIBLE_LINE_COUNT = 4
|
|
53
|
+
TABLE_MINIMUM_ROW_COUNT = 3
|
|
54
|
+
# Width the Rebase current HEAD? confirmation opens at, so its warning reads as a paragraph instead of a narrow column
|
|
55
|
+
# of short lines; its height follows the wrapped text.
|
|
56
|
+
REBASE_CONFIRMATION_WIDTH = 520
|
|
57
|
+
REBASE_STATUS_BY_STATE = {
|
|
58
|
+
"conflict": "There are unresolved merge conflicts.",
|
|
59
|
+
"edit": "Rebase paused for editing. Amend the commit if needed, then continue.",
|
|
60
|
+
"paused": "Conflicts are resolved and staged. Continue the rebase.",
|
|
61
|
+
"intervention_required": "Rebase needs your attention. Review the diagnostics before continuing.",
|
|
62
|
+
"unknown": "Rebase state is unavailable. Refresh to try again.",
|
|
63
|
+
"external-change": "Rebase changed externally. Refresh to inspect the current state.",
|
|
64
|
+
"starting": "Rebase in progress.",
|
|
65
|
+
"absent": "Rebase finished.",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def rebase_command_sentence(status: OperationStatus) -> str:
|
|
70
|
+
"""Write the result of a rebase command as the window shows it: first line of its diagnostics and, once no rebase is left, its status."""
|
|
71
|
+
return f"Rebase {status.value}."
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def step_rows_signature(state: RebaseSnapshot) -> tuple[object, ...]:
|
|
75
|
+
"""Tell what the rows of the step table show of a state; two states with the same signature give the same rows."""
|
|
76
|
+
return (state.done_steps, state.current_step, state.remaining_steps, state.opaque_steps, state.fresh)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class RebaseDialog(ContentGrowthMixin, QDialog):
|
|
80
|
+
"""Show native instructions in one ordered table with explicit recovery actions.
|
|
81
|
+
|
|
82
|
+
The window has no scroll area: its height follows its own layout through the shared growth rule, so the
|
|
83
|
+
diagnostics field grows it when shown and gives the height back when hidden, while the table takes all the room
|
|
84
|
+
left above its minimum of three rows.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
refresh_requested = Signal()
|
|
88
|
+
edit_todo_requested = Signal()
|
|
89
|
+
amend_requested = Signal(object)
|
|
90
|
+
continue_requested = Signal(object)
|
|
91
|
+
abort_requested = Signal(object)
|
|
92
|
+
solve_conflicts_requested = Signal()
|
|
93
|
+
commit_details_requested = Signal(object)
|
|
94
|
+
message_column = REBASE_MESSAGE_COLUMN
|
|
95
|
+
|
|
96
|
+
def __init__(self, state: RebaseSnapshot, parent: QWidget | None = None, *, clock: Callable[[], datetime] | None = None) -> None:
|
|
97
|
+
"""Keep progress prominent and optional diagnostics collapsed by default.
|
|
98
|
+
|
|
99
|
+
The clock is injected so the relative dates of the step table can be reproduced, as the window gallery needs.
|
|
100
|
+
"""
|
|
101
|
+
super().__init__(parent)
|
|
102
|
+
self.clock = clock or (lambda: datetime.now().astimezone())
|
|
103
|
+
self.busy = False
|
|
104
|
+
self.command_message = ""
|
|
105
|
+
# Status of the last rebase command, None before any; forgotten once a rebase is in progress again.
|
|
106
|
+
self.command_status: OperationStatus | None = None
|
|
107
|
+
self.state = state
|
|
108
|
+
self.shown_step_signature: tuple[object, ...] | None = None
|
|
109
|
+
self.current_step_raw: str | None = None
|
|
110
|
+
self.row_keys: list[tuple[str, str, int]] = []
|
|
111
|
+
self.commit_by_oid: dict[str, RebaseCommit] = {}
|
|
112
|
+
self.setWindowTitle("Rebase")
|
|
113
|
+
self.resize(OPERATION_WIDTH, OPERATION_HEIGHT)
|
|
114
|
+
layout = QVBoxLayout(self)
|
|
115
|
+
self.repository_label = WrappingLabel(state.key.git_dir)
|
|
116
|
+
self.repository_label.setWordWrap(True)
|
|
117
|
+
layout.addWidget(self.repository_label)
|
|
118
|
+
self.step_table = QTableWidget()
|
|
119
|
+
self.step_table.setAccessibleName("Rebase steps")
|
|
120
|
+
self.step_table.setColumnCount(len(REBASE_COLUMN_TITLES))
|
|
121
|
+
self.step_table.setHorizontalHeaderLabels(REBASE_COLUMN_TITLES)
|
|
122
|
+
self.step_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
|
123
|
+
self.step_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
|
124
|
+
self.step_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
|
125
|
+
self.step_table.setAlternatingRowColors(True)
|
|
126
|
+
self.step_table.setWordWrap(False)
|
|
127
|
+
self.step_table.verticalHeader().hide()
|
|
128
|
+
self.step_table.horizontalHeader().setSectionResizeMode(REBASE_MESSAGE_COLUMN, QHeaderView.ResizeMode.Stretch)
|
|
129
|
+
layout.addWidget(self.step_table, 1)
|
|
130
|
+
self.status_label = WrappingLabel()
|
|
131
|
+
self.status_label.setWordWrap(True)
|
|
132
|
+
# A refusal written by Git or a hook can be longer than the screen: it scrolls past the window's growth cap, so
|
|
133
|
+
# the buttons under it stay inside the window.
|
|
134
|
+
self.status_area = ScrolledText(self.status_label)
|
|
135
|
+
layout.addWidget(self.status_area)
|
|
136
|
+
self.diagnostic_button = QToolButton()
|
|
137
|
+
self.diagnostic_button.setCheckable(True)
|
|
138
|
+
self.diagnostic_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
|
|
139
|
+
self.diagnostic_button.toggled.connect(self._toggle_diagnostic)
|
|
140
|
+
layout.addWidget(self.diagnostic_button)
|
|
141
|
+
self.diagnostic_edit = QPlainTextEdit()
|
|
142
|
+
self.diagnostic_edit.setReadOnly(True)
|
|
143
|
+
self.diagnostic_edit.setAccessibleName("Rebase diagnostics")
|
|
144
|
+
layout.addWidget(self.diagnostic_edit)
|
|
145
|
+
self._toggle_diagnostic(False)
|
|
146
|
+
self.button_flow = FlowLayout(spacing_px=SPACING_SM)
|
|
147
|
+
self.refresh_button = QPushButton("Refresh")
|
|
148
|
+
self.refresh_button.clicked.connect(self._refresh_clicked)
|
|
149
|
+
self.edit_todo_button = QPushButton("Edit rebase instructions…")
|
|
150
|
+
self.edit_todo_button.clicked.connect(self.edit_todo_requested)
|
|
151
|
+
self.amend_button = QPushButton("Amend commit…")
|
|
152
|
+
self.amend_button.clicked.connect(lambda: self.amend_requested.emit(self.state))
|
|
153
|
+
self.continue_button = QPushButton("Continue rebase")
|
|
154
|
+
self.continue_button.clicked.connect(lambda: self.continue_requested.emit(self.state))
|
|
155
|
+
self.abort_button = QPushButton("Abort")
|
|
156
|
+
self.abort_button.clicked.connect(lambda: self.abort_requested.emit(self.state))
|
|
157
|
+
self.solve_conflicts_button = QPushButton("Solve conflicts")
|
|
158
|
+
self.solve_conflicts_button.clicked.connect(self.solve_conflicts_requested)
|
|
159
|
+
self.close_button = QPushButton("Close")
|
|
160
|
+
self.close_button.clicked.connect(self.reject)
|
|
161
|
+
self.buttons = [self.refresh_button, self.edit_todo_button, self.amend_button, self.solve_conflicts_button, self.continue_button, self.abort_button, self.close_button]
|
|
162
|
+
for button in self.buttons:
|
|
163
|
+
# Enter must never trigger Abort or Continue from a table; each command stays an explicit click or Space on its focused button.
|
|
164
|
+
button.setAutoDefault(False)
|
|
165
|
+
self.button_flow.addWidget(button)
|
|
166
|
+
layout.addLayout(self.button_flow)
|
|
167
|
+
self.show_state(state)
|
|
168
|
+
self.refresh_metrics()
|
|
169
|
+
extra_row_count = max(0, self.step_table.rowCount() - TABLE_MINIMUM_ROW_COUNT)
|
|
170
|
+
initial_height = layout.totalMinimumHeightForWidth(self.width()) + extra_row_count * self.step_table.verticalHeader().defaultSectionSize()
|
|
171
|
+
self.resize(self.width(), min(OPERATION_HEIGHT, initial_height))
|
|
172
|
+
self.install_content_growth(LayoutContentMeasure(self), OpeningFit.GROW_ONLY, self.height())
|
|
173
|
+
|
|
174
|
+
def show_state(self, state: RebaseSnapshot) -> None:
|
|
175
|
+
"""Retain the latest state, set labels and buttons again, and rebuild the rows only when what they show changed.
|
|
176
|
+
|
|
177
|
+
The selection is preserved unless the current instruction advances. With no rebase left, a last command that
|
|
178
|
+
did not succeed gives the status ("Rebase failed."), the sentence its diagnostics already start with: a rebase
|
|
179
|
+
that failed to start or stopped on an error leaves no state behind, and "Rebase finished." would contradict the
|
|
180
|
+
console, which reports the failure.
|
|
181
|
+
"""
|
|
182
|
+
self.state = state
|
|
183
|
+
if state.state != "absent":
|
|
184
|
+
self.command_status = None
|
|
185
|
+
status = REBASE_STATUS_BY_STATE.get(state.state, "Rebase: " + state.state)
|
|
186
|
+
if state.state == "absent" and state.diagnostic:
|
|
187
|
+
status = state.diagnostic
|
|
188
|
+
elif state.state == "absent" and self.command_status is not None and self.command_status not in COMPLETED_STATUS_SET:
|
|
189
|
+
status = rebase_command_sentence(self.command_status)
|
|
190
|
+
if not state.fresh:
|
|
191
|
+
status = "State is not fresh. Refresh before taking action."
|
|
192
|
+
diagnostic_text = "\n".join(filter(None, (state.diagnostic, self.command_message)))
|
|
193
|
+
self.status_label.setText(status)
|
|
194
|
+
self.diagnostic_edit.setPlainText(diagnostic_text)
|
|
195
|
+
self.diagnostic_button.setVisible(bool(self.diagnostic_edit.toPlainText()))
|
|
196
|
+
self.diagnostic_edit.setVisible(self.diagnostic_button.isChecked() and bool(self.diagnostic_edit.toPlainText()))
|
|
197
|
+
# The state is published again at every observation. Rebuilding the table would spend time on the thread of the
|
|
198
|
+
# interface and ask the same commit details again, while the retained state must always be the last one
|
|
199
|
+
# received, because Continue and Abort send it.
|
|
200
|
+
signature = step_rows_signature(state)
|
|
201
|
+
rows_changed = signature != self.shown_step_signature
|
|
202
|
+
if rows_changed:
|
|
203
|
+
self._show_steps()
|
|
204
|
+
self.shown_step_signature = signature
|
|
205
|
+
self.set_busy(self.busy)
|
|
206
|
+
if rows_changed:
|
|
207
|
+
self.request_commit_details()
|
|
208
|
+
|
|
209
|
+
def request_commit_details(self) -> None:
|
|
210
|
+
"""Ask for the author and date of every shown step.
|
|
211
|
+
|
|
212
|
+
The window shows its first state while it is built, before anybody listens to this request, so whoever
|
|
213
|
+
listens afterwards asks once here; an equal state shown later keeps its rows and does not ask again.
|
|
214
|
+
"""
|
|
215
|
+
state = self.state
|
|
216
|
+
steps = state.done_steps + state.remaining_steps
|
|
217
|
+
if state.current_step is not None:
|
|
218
|
+
steps += (state.current_step,)
|
|
219
|
+
self.commit_details_requested.emit(steps)
|
|
220
|
+
|
|
221
|
+
def _refresh_clicked(self) -> None:
|
|
222
|
+
"""Ask to read the rebase again, and show the rows and the commit details again even when Git gives the same steps.
|
|
223
|
+
|
|
224
|
+
Showing the details again retries a details read that failed.
|
|
225
|
+
"""
|
|
226
|
+
self.shown_step_signature = None
|
|
227
|
+
self.refresh_requested.emit()
|
|
228
|
+
|
|
229
|
+
def show_commits(self, commits: tuple[RebaseCommit, ...]) -> None:
|
|
230
|
+
"""Fill display-only details without changing the observed rebase generation or action state."""
|
|
231
|
+
for commit in commits:
|
|
232
|
+
self.commit_by_oid[commit.oid] = commit
|
|
233
|
+
self._show_steps()
|
|
234
|
+
|
|
235
|
+
def show_command_result(self, status: OperationStatus, cause: str = "") -> None:
|
|
236
|
+
"""Retain command failures in expandable diagnostics and expose their cause immediately.
|
|
237
|
+
|
|
238
|
+
The status is kept too, so a failed command that left no rebase is not reported as finished.
|
|
239
|
+
"""
|
|
240
|
+
self.command_status = status
|
|
241
|
+
self.command_message = rebase_command_sentence(status) + (" " + cause if cause else "")
|
|
242
|
+
self.show_state(self.state)
|
|
243
|
+
if cause and self.state.state != "conflict":
|
|
244
|
+
self.diagnostic_button.setChecked(True)
|
|
245
|
+
|
|
246
|
+
def set_busy(self, busy: bool) -> None:
|
|
247
|
+
"""Keep all write actions disabled while another command or tool owns the worktree."""
|
|
248
|
+
self.busy = busy
|
|
249
|
+
state = self.state
|
|
250
|
+
available = not busy and state.fresh and state.identity is not None
|
|
251
|
+
self.amend_button.setEnabled(available and state.state == "edit")
|
|
252
|
+
self.continue_button.setEnabled(available and state.state in REBASE_CONTINUABLE_STATE_SET)
|
|
253
|
+
self.abort_button.setEnabled(not busy and can_abort_rebase(state))
|
|
254
|
+
self.solve_conflicts_button.setEnabled(state.fresh and state.identity is not None and state.state == "conflict")
|
|
255
|
+
self.edit_todo_button.setEnabled(available and state.identity.backend == "rebase-merge"
|
|
256
|
+
and state.state in REBASE_STOPPED_STATE_SET)
|
|
257
|
+
self._style_recovery()
|
|
258
|
+
|
|
259
|
+
def _show_steps(self) -> None:
|
|
260
|
+
"""Keep native order and distinguish unsupported instructions without inventing their progress."""
|
|
261
|
+
table = self.step_table
|
|
262
|
+
selected_row = table.currentRow()
|
|
263
|
+
selected_key = self.row_keys[selected_row] if 0 <= selected_row < len(self.row_keys) else None
|
|
264
|
+
selected_column = max(REBASE_STATUS_COLUMN, table.currentColumn())
|
|
265
|
+
scroll_value = table.verticalScrollBar().value()
|
|
266
|
+
rows: list[tuple[str, RebaseStep]] = []
|
|
267
|
+
for step in self.state.done_steps:
|
|
268
|
+
rows.append(("Dropped" if step.action == "drop" else "Applied", step))
|
|
269
|
+
current_row = len(rows) if self.state.current_step is not None else None
|
|
270
|
+
current_raw = self.state.current_step.raw if self.state.current_step is not None else None
|
|
271
|
+
if self.state.current_step is not None:
|
|
272
|
+
rows.append(("Applying…", self.state.current_step))
|
|
273
|
+
for step in self.state.remaining_steps:
|
|
274
|
+
rows.append(("Remaining", step))
|
|
275
|
+
for step in self.state.opaque_steps:
|
|
276
|
+
rows.append(("Unknown", step))
|
|
277
|
+
commit_by_oid: dict[str, RebaseCommit] = {}
|
|
278
|
+
abbreviation_length_set = set()
|
|
279
|
+
for _status, step in rows:
|
|
280
|
+
if step.oid:
|
|
281
|
+
abbreviation_length_set.add(len(step.oid))
|
|
282
|
+
for commit in self.commit_by_oid.values():
|
|
283
|
+
for length in abbreviation_length_set:
|
|
284
|
+
commit_by_oid[commit.oid[:length]] = commit
|
|
285
|
+
now = self.clock()
|
|
286
|
+
table.blockSignals(True)
|
|
287
|
+
table.clearContents()
|
|
288
|
+
table.setRowCount(len(rows))
|
|
289
|
+
self.row_keys = []
|
|
290
|
+
occurrence_by_key: dict[tuple[str, str], int] = {}
|
|
291
|
+
restored_row = None
|
|
292
|
+
for row, (status, step) in enumerate(rows):
|
|
293
|
+
key = (status, step.raw)
|
|
294
|
+
occurrence = occurrence_by_key.get(key, 0)
|
|
295
|
+
occurrence_by_key[key] = occurrence + 1
|
|
296
|
+
row_key = (status, step.raw, occurrence)
|
|
297
|
+
self.row_keys.append(row_key)
|
|
298
|
+
commit = commit_by_oid.get(step.oid)
|
|
299
|
+
date = format_commit_date(commit.author_timestamp, now, relative_only=True) if commit is not None else "—"
|
|
300
|
+
author = commit.author_name if commit is not None else "—"
|
|
301
|
+
texts = (status, step.action, step.message, author, date, step.oid[:SHORT_OID_LENGTH])
|
|
302
|
+
for column, text in enumerate(texts):
|
|
303
|
+
item = QTableWidgetItem(text)
|
|
304
|
+
tooltip = text
|
|
305
|
+
if column == REBASE_DATE_COLUMN and commit is not None:
|
|
306
|
+
tooltip = format_commit_date(commit.author_timestamp, now)
|
|
307
|
+
elif column == REBASE_COMMIT_COLUMN:
|
|
308
|
+
tooltip = commit.oid if commit is not None else step.oid
|
|
309
|
+
elif column == REBASE_STATUS_COLUMN and (step.opaque or not self.state.fresh):
|
|
310
|
+
tooltip = "Execution state unknown." if step.opaque else "Last observed state; refresh to verify progress."
|
|
311
|
+
item.setToolTip(tooltip)
|
|
312
|
+
font = item.font()
|
|
313
|
+
font.setBold(row == current_row)
|
|
314
|
+
item.setFont(font)
|
|
315
|
+
table.setItem(row, column, item)
|
|
316
|
+
if row_key == selected_key:
|
|
317
|
+
restored_row = row
|
|
318
|
+
current_changed = current_raw != self.current_step_raw
|
|
319
|
+
self.current_step_raw = current_raw
|
|
320
|
+
target_row = current_row if current_changed or restored_row is None else restored_row
|
|
321
|
+
if target_row is not None:
|
|
322
|
+
table.setCurrentCell(target_row, selected_column)
|
|
323
|
+
elif rows:
|
|
324
|
+
first_row = 0
|
|
325
|
+
table.setCurrentCell(first_row, selected_column)
|
|
326
|
+
table.blockSignals(False)
|
|
327
|
+
if current_changed and current_row is not None:
|
|
328
|
+
table.scrollToItem(table.item(current_row, REBASE_STATUS_COLUMN), QAbstractItemView.ScrollHint.PositionAtCenter)
|
|
329
|
+
else:
|
|
330
|
+
table.verticalScrollBar().setValue(scroll_value)
|
|
331
|
+
|
|
332
|
+
def _toggle_diagnostic(self, expanded: bool) -> None:
|
|
333
|
+
"""Reveal command diagnostics only when requested, without a second instruction display.
|
|
334
|
+
|
|
335
|
+
The window follows the field both ways through the shared growth rule: showing it grows the window until no
|
|
336
|
+
row covers its neighbour, hiding it brings back the height the window had before.
|
|
337
|
+
"""
|
|
338
|
+
self.diagnostic_button.setText("Hide diagnostics" if expanded else "Show diagnostics")
|
|
339
|
+
self.diagnostic_button.setArrowType(Qt.ArrowType.DownArrow if expanded else Qt.ArrowType.RightArrow)
|
|
340
|
+
self.diagnostic_edit.setVisible(expanded)
|
|
341
|
+
|
|
342
|
+
def _style_action_button(self, button: QPushButton, *, active: bool, background: str, hover_background: str, pressed_background: str, foreground: str, border: str) -> None:
|
|
343
|
+
"""Share normal button sizing and pointer feedback across highlighted recovery actions."""
|
|
344
|
+
button_style = (f"QPushButton {{ background-color: {background}; color: {foreground}; "
|
|
345
|
+
f"border: {REBASE_ACTION_BORDER_WIDTH}px solid {border}; border-radius: {REBASE_ACTION_CORNER_RADIUS}px; padding: 0 {SPACING_SM}px; }}"
|
|
346
|
+
f"QPushButton:hover {{ background-color: {hover_background}; }}"
|
|
347
|
+
f"QPushButton:pressed {{ background-color: {pressed_background}; }}"
|
|
348
|
+
"QPushButton:focus { border-style: dashed; }") if active else ""
|
|
349
|
+
if button.styleSheet() != button_style:
|
|
350
|
+
button.setStyleSheet(button_style)
|
|
351
|
+
|
|
352
|
+
def _style_recovery(self) -> None:
|
|
353
|
+
"""Highlight edit pauses, conflict resolution and readiness to continue after staging."""
|
|
354
|
+
conflict = self.solve_conflicts_button.isEnabled()
|
|
355
|
+
ready = self.state.state == "paused" and self.continue_button.isEnabled()
|
|
356
|
+
self._style_action_button(self.solve_conflicts_button, active=conflict, background=CONFLICT_BUTTON_BACKGROUND, hover_background=CONFLICT_BUTTON_HOVER_BACKGROUND,
|
|
357
|
+
pressed_background=CONFLICT_BUTTON_PRESSED_BACKGROUND, foreground=CONFLICT_BUTTON_FOREGROUND, border=CONFLICT_BUTTON_BORDER)
|
|
358
|
+
self._style_action_button(self.continue_button, active=ready, background=CONTINUE_BUTTON_BACKGROUND, hover_background=CONTINUE_BUTTON_HOVER_BACKGROUND,
|
|
359
|
+
pressed_background=CONTINUE_BUTTON_PRESSED_BACKGROUND, foreground=CONTINUE_BUTTON_FOREGROUND, border=CONTINUE_BUTTON_BORDER)
|
|
360
|
+
dark = self.palette().color(QPalette.ColorRole.Window).lightnessF() < DARK_BACKGROUND_LIGHTNESS_LIMIT
|
|
361
|
+
status_style = ""
|
|
362
|
+
if conflict:
|
|
363
|
+
color = ERROR_DARK_COLOR if dark else ERROR_LIGHT_COLOR
|
|
364
|
+
status_style = f"color: {color}; font-weight: bold;"
|
|
365
|
+
elif ready:
|
|
366
|
+
color = REBASE_READY_DARK_COLOR if dark else REBASE_READY_LIGHT_COLOR
|
|
367
|
+
status_style = f"color: {color}; font-weight: bold;"
|
|
368
|
+
elif self.amend_button.isEnabled():
|
|
369
|
+
color = REBASE_EDIT_DARK_COLOR if dark else REBASE_EDIT_LIGHT_COLOR
|
|
370
|
+
status_style = f"color: {color}; font-weight: bold;"
|
|
371
|
+
if self.status_label.styleSheet() != status_style:
|
|
372
|
+
self.status_label.setStyleSheet(status_style)
|
|
373
|
+
|
|
374
|
+
def refresh_metrics(self, available: QRect | None = None) -> None:
|
|
375
|
+
"""Give the table all remaining space and keep actions reachable at large font sizes."""
|
|
376
|
+
table = self.step_table
|
|
377
|
+
metrics = table.fontMetrics()
|
|
378
|
+
row_height = metrics.height() + SPACING_SM
|
|
379
|
+
table.verticalHeader().setDefaultSectionSize(row_height)
|
|
380
|
+
table.setMinimumHeight(row_height * TABLE_MINIMUM_ROW_COUNT + table.horizontalHeader().sizeHint().height() + table.frameWidth() * 2 + table.horizontalScrollBar().sizeHint().height())
|
|
381
|
+
for column, sample in ((REBASE_STATUS_COLUMN, "Applying…"), (REBASE_ACTION_COLUMN, "reword"), (REBASE_AUTHOR_COLUMN, "Author name here"), (REBASE_DATE_COLUMN, "12 months ago"), (REBASE_COMMIT_COLUMN, "a" * SHORT_OID_LENGTH)):
|
|
382
|
+
table.setColumnWidth(column, metrics.horizontalAdvance(sample) + SPACING_MD)
|
|
383
|
+
self.diagnostic_edit.setFixedHeight(metrics.height() * DIAGNOSTIC_VISIBLE_LINE_COUNT + SPACING_MD)
|
|
384
|
+
self._style_recovery()
|
|
385
|
+
fit_form(self, available)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class RebaseConfirmation(ContentGrowthMixin, QDialog):
|
|
389
|
+
"""Confirm rewriting the captured HEAD onto the selected graph target once.
|
|
390
|
+
|
|
391
|
+
The window has no scroll area: its height follows its own layout through the shared growth rule, so an error line
|
|
392
|
+
grows it to show every line and gives the height back once the line is empty.
|
|
393
|
+
"""
|
|
394
|
+
|
|
395
|
+
submit_requested = Signal()
|
|
396
|
+
|
|
397
|
+
def __init__(self, source: str, target: str, parent: QWidget | None = None, *, preference: PreferenceStore | None = None) -> None:
|
|
398
|
+
"""Name both endpoints in plain text while keeping cancellation non-writing."""
|
|
399
|
+
super().__init__(parent)
|
|
400
|
+
self.preference = preference
|
|
401
|
+
self.submitting = False
|
|
402
|
+
self.cancelled = False
|
|
403
|
+
self.rejected.connect(self._cancel)
|
|
404
|
+
self.setWindowTitle("Rebase current HEAD?")
|
|
405
|
+
layout = QVBoxLayout(self)
|
|
406
|
+
# Wrapping labels break the long commit IDs and branch names too, and report the height their text needs at
|
|
407
|
+
# the width they get, so the window can grow to show every line.
|
|
408
|
+
self.message_label = WrappingLabel(f"Rebase {source} onto {target}? This rewrites commit history. Merge commits are flattened; merge-only resolutions may not be preserved.")
|
|
409
|
+
self.message_label.setWordWrap(True)
|
|
410
|
+
layout.addWidget(self.message_label)
|
|
411
|
+
self.dont_show_again = QCheckBox("Don't show again")
|
|
412
|
+
layout.addWidget(self.dont_show_again)
|
|
413
|
+
self.error_label = WrappingLabel()
|
|
414
|
+
self.error_label.setWordWrap(True)
|
|
415
|
+
layout.addWidget(self.error_label)
|
|
416
|
+
buttons = QHBoxLayout()
|
|
417
|
+
self.cancel_button = QPushButton("Cancel rebase")
|
|
418
|
+
self.cancel_button.clicked.connect(self.reject)
|
|
419
|
+
self.confirm_button = QPushButton("Rebase")
|
|
420
|
+
self.confirm_button.clicked.connect(self.confirm)
|
|
421
|
+
buttons.addWidget(self.cancel_button)
|
|
422
|
+
buttons.addWidget(self.confirm_button)
|
|
423
|
+
layout.addLayout(buttons)
|
|
424
|
+
self.refresh_metrics()
|
|
425
|
+
self.resize(REBASE_CONFIRMATION_WIDTH, self.height())
|
|
426
|
+
self.resize(REBASE_CONFIRMATION_WIDTH, content_height(self))
|
|
427
|
+
self.install_content_growth(LayoutContentMeasure(self), OpeningFit.GROW_ONLY, self.height())
|
|
428
|
+
follow_theme(self, self.refresh_metrics)
|
|
429
|
+
|
|
430
|
+
def confirm(self) -> None:
|
|
431
|
+
"""Publish the suppression choice before requesting the captured rebase.
|
|
432
|
+
|
|
433
|
+
A choice that cannot be saved keeps the confirmation open with an error line, which the shared growth rule gives
|
|
434
|
+
room to; a new try that succeeds starts the rebase.
|
|
435
|
+
"""
|
|
436
|
+
if self.submitting:
|
|
437
|
+
return
|
|
438
|
+
try:
|
|
439
|
+
if self.preference is not None:
|
|
440
|
+
self.preference.update({REBASE_PROMPT_PREFERENCE_KEY: self.dont_show_again.isChecked()})
|
|
441
|
+
except (OSError, ValueError, TypeError):
|
|
442
|
+
self.error_label.setText("Could not save this choice. The confirmation remains enabled. Fix the settings error and try again.")
|
|
443
|
+
return
|
|
444
|
+
self.submit_requested.emit()
|
|
445
|
+
|
|
446
|
+
def _cancel(self) -> None:
|
|
447
|
+
"""Invalidate asynchronous validation even when confirmation was suppressed."""
|
|
448
|
+
self.cancelled = True
|
|
449
|
+
|
|
450
|
+
def refresh_metrics(self, available: QRect | None = None) -> None:
|
|
451
|
+
"""Keep the confirmation readable and its errors prominent on light and dark backgrounds, again after each appearance switch.
|
|
452
|
+
|
|
453
|
+
The error color is decided on the application palette: when the palette changes, the window's own palette can
|
|
454
|
+
still be the old one.
|
|
455
|
+
"""
|
|
456
|
+
dark = QApplication.palette().color(QPalette.ColorRole.Window).lightnessF() < DARK_BACKGROUND_LIGHTNESS_LIMIT
|
|
457
|
+
color = ERROR_DARK_COLOR if dark else ERROR_LIGHT_COLOR
|
|
458
|
+
error_style = f"color: {color}; font-weight: bold;"
|
|
459
|
+
if self.error_label.styleSheet() != error_style:
|
|
460
|
+
self.error_label.setStyleSheet(error_style)
|
|
461
|
+
fit_form(self, available)
|