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,565 @@
|
|
|
1
|
+
"""Present the consequences of a prepared history removal proposal."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from PySide6.QtCore import QEvent, QMargins, QModelIndex, QSize, Qt, Signal
|
|
10
|
+
from PySide6.QtGui import QFont, QPalette
|
|
11
|
+
from PySide6.QtWidgets import QAbstractItemView, QApplication, QDialog, QHBoxLayout, QHeaderView, QLabel, QPlainTextEdit, QPushButton, QScrollArea, QSplitter, QStyle, QStyleOptionViewItem, QTableView, QVBoxLayout, QWidget
|
|
12
|
+
|
|
13
|
+
from gitpane.config import ICON_SIDE, LOCAL_PREFIX, SHORT_OID_LENGTH, SPACING_MD, SPACING_SM, fit_window, history_row_height
|
|
14
|
+
from gitpane.history_rewrite.value import RewriteStatus, RewriteTargetKind
|
|
15
|
+
from gitpane.ui.history_rewrite.details import DetailsText, HistoryRewriteDetails, commit_details
|
|
16
|
+
from gitpane.ui.history_rewrite.preview_model import BranchColumn, BranchPreviewModel, CommitColumn, CommitPreviewModel, ModelText, StatusDelegate, author_date, safe_text, status_color
|
|
17
|
+
from gitpane.ui.look.theme_look import follow_theme
|
|
18
|
+
from gitpane.ui.look.theme_palette import desktop_prefers_high_contrast
|
|
19
|
+
from gitpane.ui.widget.dialog_layout import apply_form_metrics, install_banded_layout
|
|
20
|
+
from gitpane.ui.widget.long_text import WrappingLabel
|
|
21
|
+
from gitpane.ui.widget.result_label import ResultLabel
|
|
22
|
+
from gitpane.ui.widget.standard_icon_label import StandardIconLabel
|
|
23
|
+
from gitpane.ui.window_control.dialog_modality import open_window_modal
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from PySide6.QtCore import QObject
|
|
27
|
+
from PySide6.QtGui import QCloseEvent, QKeyEvent
|
|
28
|
+
|
|
29
|
+
from gitpane.history_rewrite.value import RewritePreparation, RewriteRefusal, RewriteTarget
|
|
30
|
+
|
|
31
|
+
PREVIEW_WIDTH, PREVIEW_HEIGHT = 960, 720
|
|
32
|
+
PREVIEW_MINIMUM_WIDTH, PREVIEW_MINIMUM_HEIGHT = 640, 480
|
|
33
|
+
INITIAL_BRANCH_ROW_COUNT = 4
|
|
34
|
+
MINIMUM_WARNING_LINE_COUNT = 4
|
|
35
|
+
TARGET_LINE_COUNT = 3
|
|
36
|
+
MESSAGE_MINIMUM_CHARACTERS = 24
|
|
37
|
+
AUTHOR_MINIMUM_CHARACTERS = 16
|
|
38
|
+
PREVIEW_BRANCH_MINIMUM_CHARACTERS = 20
|
|
39
|
+
BRANCH_TABLE_STRETCH, COMMIT_TABLE_STRETCH = 1, 3
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PreviewState(Enum):
|
|
43
|
+
"""Identify which actions are meaningful for the current proposal."""
|
|
44
|
+
|
|
45
|
+
PREPARING = "preparing"
|
|
46
|
+
READY = "ready"
|
|
47
|
+
EMPTY = "empty"
|
|
48
|
+
ERROR = "error"
|
|
49
|
+
STALE = "stale"
|
|
50
|
+
STOPPING = "stopping"
|
|
51
|
+
EXECUTING = "executing"
|
|
52
|
+
CLOSED = "closed"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class PreviewText(Enum):
|
|
56
|
+
"""Name the preview regions and its explicit action states."""
|
|
57
|
+
|
|
58
|
+
TITLE = "Remove from history — Preview"
|
|
59
|
+
TARGET = "Selected path"
|
|
60
|
+
FILE = "File: {path}"
|
|
61
|
+
FOLDER = "Folder and all files below it: {path}"
|
|
62
|
+
REPOSITORY = "Repository: {repository}"
|
|
63
|
+
BRANCH = "Current branch: {branch}"
|
|
64
|
+
PREPARING = "Preparing preview… No changes have been made to this repository."
|
|
65
|
+
STOPPING = "Stopping preview…"
|
|
66
|
+
EMPTY = "No history changes found"
|
|
67
|
+
EMPTY_BODY = "{path} has no history changes to remove from the affected local branches. Choose Abort and select another path."
|
|
68
|
+
ERROR = "Could not build the preview: {cause}. No changes have been made to this repository. Resolve the problem, then choose Recompute preview."
|
|
69
|
+
INCOMPLETE = "The preview could not be completed: {cause}. Confirm is unavailable. Inspect the diagnostic and resolve the limit before recomputing; no history was changed."
|
|
70
|
+
MISSING_METADATA = "Required branch or commit metadata is incomplete or unreadable"
|
|
71
|
+
DIFFERENT_TARGET = "The proposal belongs to another selected path"
|
|
72
|
+
STALE = "Preview is out of date"
|
|
73
|
+
STALE_BODY = "The repository changed after this preview was built. Choose Recompute preview to review the current branches and commits before confirming."
|
|
74
|
+
BRANCHES = "Affected local branches"
|
|
75
|
+
ONE_BRANCH = "1 local branch will move."
|
|
76
|
+
MANY_BRANCHES = "{count} local branches will move."
|
|
77
|
+
COMMITS = "Affected commit changes"
|
|
78
|
+
ONE_MODIFIED = "1 modified commit"
|
|
79
|
+
MANY_MODIFIED = "{count} modified commits"
|
|
80
|
+
ONE_DELETED = "1 deleted commit"
|
|
81
|
+
MANY_DELETED = "{count} deleted commits"
|
|
82
|
+
MODIFIED_LEGEND = "— Modified commit, kept in history."
|
|
83
|
+
DELETED_LEGEND = "— Commit deleted because removing the path makes it empty."
|
|
84
|
+
DESCENDANTS = "Descendants may also receive new commit hashes even when their own changes do not involve this path. They are not listed here."
|
|
85
|
+
WARNINGS = "Before you confirm"
|
|
86
|
+
CONFIRM = "Confirm"
|
|
87
|
+
ABORT = "Abort"
|
|
88
|
+
RECOMPUTE = "Recompute preview"
|
|
89
|
+
EXECUTING = "Checking repository…"
|
|
90
|
+
PUBLICATION = "Publication information uses remote references from the last fetch. No fetch was performed; the server may have changed since then."
|
|
91
|
+
PUBLISHED = "Published history will differ from these local branches. Coordinate with other users before updating remote branches. GitPane will not push these changes."
|
|
92
|
+
ONE_SIGNATURE = "1 affected signed commit will lose its signature, including rewritten commits not listed above. Confirm accepts this loss."
|
|
93
|
+
MANY_SIGNATURES = "{count} affected signed commits will lose their signatures, including rewritten commits not listed above. Confirm accepts this loss."
|
|
94
|
+
NO_SIGNATURE = "No affected signed commits were found."
|
|
95
|
+
ABSENT = "This path is absent from the current branch version. Historical versions will be removed; no file will be recreated on disk."
|
|
96
|
+
DISK_INDEX = ("Current files on disk, including unstaged changes, will be kept exactly as they are. The target will be removed from the index. "
|
|
97
|
+
"Other staged changes will be kept. No ignore rule will be added.")
|
|
98
|
+
FINAL = ("Confirm removes {path} from the history of every local branch listed above. Other local branches, tags and remote references will not move. "
|
|
99
|
+
"Review the consequences below before confirming.")
|
|
100
|
+
RETAINED = ("This is not a global purge. Other references, reflogs, clones and servers may retain the old data. "
|
|
101
|
+
"Revoke or rotate compromised credentials; removing a path does not make them safe again.")
|
|
102
|
+
SUBMODULE = "This repository is a submodule"
|
|
103
|
+
SUBMODULE_PARENT = ("Parent repositories record exact submodule commit hashes. They will still point to the old commits, not their rewritten replacements. "
|
|
104
|
+
"If an old commit remains available, it can still expose the removed file. "
|
|
105
|
+
"If it is no longer available locally or retrievable, restoring that parent version may fail.")
|
|
106
|
+
SUBMODULE_CONSUMERS = ("Other repositories using these commits may also be affected. GitPane cannot identify every consumer. "
|
|
107
|
+
"Repair could require rewriting those repositories' histories, branches and tags, then their own parents. GitPane will not update any parent repository.")
|
|
108
|
+
SUBMODULE_ACCEPT = "Confirm accepts that some old parent versions may no longer be restorable."
|
|
109
|
+
KNOWN_PARENT = "Known parent repository: {repository}"
|
|
110
|
+
REFUSAL_PATH = "Path: {path}"
|
|
111
|
+
REFUSAL_COMMIT = "Commit: {commit}"
|
|
112
|
+
REFUSAL_BRANCH = "Branch: {branch}"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class HistoryRewritePreview(QDialog):
|
|
116
|
+
"""Expose a proposal through typed signals without performing repository work."""
|
|
117
|
+
|
|
118
|
+
confirm_requested = Signal(object)
|
|
119
|
+
abort_requested = Signal()
|
|
120
|
+
recompute_requested = Signal()
|
|
121
|
+
|
|
122
|
+
def __init__(self, target: RewriteTarget, branch: str, repository_dir: str, parent: QWidget | None = None, *, active_submodule: bool = False, parent_repository_dir: str = "") -> None:
|
|
123
|
+
"""Start visibly preparing with Abort as the only default action."""
|
|
124
|
+
super().__init__(parent)
|
|
125
|
+
self.target = target
|
|
126
|
+
self.active_branch = branch if branch.startswith(LOCAL_PREFIX) else LOCAL_PREFIX + branch
|
|
127
|
+
self.repository_dir = repository_dir
|
|
128
|
+
self.active_submodule = active_submodule
|
|
129
|
+
self.parent_repository_dir = parent_repository_dir
|
|
130
|
+
self.preparation: RewritePreparation | None = None
|
|
131
|
+
self.details_dialog: HistoryRewriteDetails | None = None
|
|
132
|
+
self.state = PreviewState.PREPARING
|
|
133
|
+
self.setWindowTitle(PreviewText.TITLE.value)
|
|
134
|
+
self.resize(PREVIEW_WIDTH, PREVIEW_HEIGHT)
|
|
135
|
+
self.setWindowFlag(Qt.WindowType.WindowContextHelpButtonHint, False)
|
|
136
|
+
body = QVBoxLayout()
|
|
137
|
+
body.setSpacing(SPACING_SM)
|
|
138
|
+
self.target_text = QPlainTextEdit()
|
|
139
|
+
self.target_text.setReadOnly(True)
|
|
140
|
+
self.target_text.setAccessibleName(PreviewText.TARGET.value)
|
|
141
|
+
self._show_target()
|
|
142
|
+
body.addWidget(self.target_text)
|
|
143
|
+
self.status_label = ResultLabel(PreviewText.PREPARING.value)
|
|
144
|
+
self.status_scroll = QScrollArea()
|
|
145
|
+
self.status_scroll.setWidgetResizable(True)
|
|
146
|
+
self.status_scroll.setWidget(self.status_label)
|
|
147
|
+
self.status_scroll.setFrameShape(QScrollArea.Shape.NoFrame)
|
|
148
|
+
body.addWidget(self.status_scroll)
|
|
149
|
+
self.branch_model = BranchPreviewModel()
|
|
150
|
+
self.commit_model = CommitPreviewModel()
|
|
151
|
+
self.branch_table = self._table(PreviewText.BRANCHES)
|
|
152
|
+
self.commit_table = self._table(PreviewText.COMMITS)
|
|
153
|
+
self.branch_table.setModel(self.branch_model)
|
|
154
|
+
self.commit_table.setModel(self.commit_model)
|
|
155
|
+
self.status_delegate = StatusDelegate(self.commit_table)
|
|
156
|
+
self.commit_table.setItemDelegateForColumn(CommitColumn.STATUS.index, self.status_delegate)
|
|
157
|
+
self.commit_table.viewport().installEventFilter(self)
|
|
158
|
+
self.branch_count_label = self._label("")
|
|
159
|
+
self.commit_count_label = self._label("")
|
|
160
|
+
self.table_splitter = QSplitter(Qt.Orientation.Vertical)
|
|
161
|
+
self.table_splitter.addWidget(self._table_section(PreviewText.BRANCHES, self.branch_count_label, self.branch_table))
|
|
162
|
+
self.table_splitter.addWidget(self._table_section(PreviewText.COMMITS, self.commit_count_label, self.commit_table))
|
|
163
|
+
branch_section_index, commit_section_index = 0, 1
|
|
164
|
+
self.table_splitter.setStretchFactor(branch_section_index, BRANCH_TABLE_STRETCH)
|
|
165
|
+
self.table_splitter.setStretchFactor(commit_section_index, COMMIT_TABLE_STRETCH)
|
|
166
|
+
self.table_splitter.setChildrenCollapsible(False)
|
|
167
|
+
self.table_splitter.setHandleWidth(SPACING_MD)
|
|
168
|
+
body.addWidget(self.table_splitter, 1)
|
|
169
|
+
legend = QHBoxLayout()
|
|
170
|
+
legend.setSpacing(SPACING_SM)
|
|
171
|
+
self.legend_marker_by_status: dict[RewriteStatus, QLabel] = {}
|
|
172
|
+
for status, caption in ((RewriteStatus.MODIFIED, PreviewText.MODIFIED_LEGEND), (RewriteStatus.DELETED, PreviewText.DELETED_LEGEND)):
|
|
173
|
+
marker = QLabel(status.value)
|
|
174
|
+
marker.setTextFormat(Qt.TextFormat.PlainText)
|
|
175
|
+
marker.setBackgroundRole(QPalette.ColorRole.Base)
|
|
176
|
+
marker.setForegroundRole(QPalette.ColorRole.Text)
|
|
177
|
+
marker.setAutoFillBackground(True)
|
|
178
|
+
marker_font = QFont()
|
|
179
|
+
marker_font.setBold(status == RewriteStatus.DELETED)
|
|
180
|
+
marker.setFont(marker_font)
|
|
181
|
+
self.legend_marker_by_status[status] = marker
|
|
182
|
+
legend.addWidget(marker, 0, Qt.AlignmentFlag.AlignTop)
|
|
183
|
+
legend.addWidget(self._label(caption.value), 1, Qt.AlignmentFlag.AlignTop)
|
|
184
|
+
body.addLayout(legend)
|
|
185
|
+
body.addWidget(self._label(PreviewText.DESCENDANTS.value))
|
|
186
|
+
body.addSpacing(SPACING_MD - body.spacing())
|
|
187
|
+
warning_heading = QHBoxLayout()
|
|
188
|
+
self.warning_icon = StandardIconLabel(QSize(ICON_SIDE, ICON_SIDE), QStyle.StandardPixmap.SP_MessageBoxWarning)
|
|
189
|
+
warning_heading.addWidget(self.warning_icon, 0, Qt.AlignmentFlag.AlignVCenter)
|
|
190
|
+
warning_heading.addWidget(self._heading(PreviewText.WARNINGS), 1)
|
|
191
|
+
body.addLayout(warning_heading)
|
|
192
|
+
self.warning_scroll = QScrollArea()
|
|
193
|
+
self.warning_scroll.setWidgetResizable(True)
|
|
194
|
+
self.warning_scroll.setAccessibleName(PreviewText.WARNINGS.value)
|
|
195
|
+
self.warning_scroll.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
|
196
|
+
self.warning_scroll.installEventFilter(self)
|
|
197
|
+
self.warning_label = self._label("")
|
|
198
|
+
self.warning_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse | Qt.TextInteractionFlag.TextSelectableByKeyboard)
|
|
199
|
+
self.warning_scroll.setWidget(self.warning_label)
|
|
200
|
+
body.addWidget(self.warning_scroll, 1)
|
|
201
|
+
self.confirm_button = QPushButton(PreviewText.CONFIRM.value)
|
|
202
|
+
self.abort_button = QPushButton(PreviewText.ABORT.value)
|
|
203
|
+
self.recompute_button = QPushButton(PreviewText.RECOMPUTE.value)
|
|
204
|
+
for button in (self.confirm_button, self.abort_button, self.recompute_button):
|
|
205
|
+
button.setAutoDefault(False)
|
|
206
|
+
self.button_band = install_banded_layout(self, body, [self.confirm_button, self.abort_button],
|
|
207
|
+
body_margins=QMargins(SPACING_MD, SPACING_MD, SPACING_MD, SPACING_MD), leading_buttons=[self.recompute_button])
|
|
208
|
+
self.abort_button.setDefault(True)
|
|
209
|
+
self.confirm_button.clicked.connect(self._confirm)
|
|
210
|
+
self.abort_button.clicked.connect(self.reject)
|
|
211
|
+
self.recompute_button.clicked.connect(self._recompute)
|
|
212
|
+
self.branch_table.doubleClicked.connect(self._branch_details)
|
|
213
|
+
self.commit_table.doubleClicked.connect(self._commit_details)
|
|
214
|
+
self._set_state(PreviewState.PREPARING)
|
|
215
|
+
self.apply_metrics()
|
|
216
|
+
self.table_splitter.setSizes([INITIAL_BRANCH_ROW_COUNT * history_row_height(self.fontMetrics().height()),
|
|
217
|
+
INITIAL_BRANCH_ROW_COUNT * history_row_height(self.fontMetrics().height()) * COMMIT_TABLE_STRETCH])
|
|
218
|
+
follow_theme(self, self._refresh_theme)
|
|
219
|
+
self._refresh_theme()
|
|
220
|
+
previous = self.target_text
|
|
221
|
+
for widget in (self.branch_table, self.commit_table, self.warning_scroll, self.warning_label, self.recompute_button, self.confirm_button, self.abort_button):
|
|
222
|
+
QWidget.setTabOrder(previous, widget)
|
|
223
|
+
previous = widget
|
|
224
|
+
fit_window(self, PREVIEW_MINIMUM_WIDTH, PREVIEW_MINIMUM_HEIGHT)
|
|
225
|
+
|
|
226
|
+
def _label(self, text: str) -> WrappingLabel:
|
|
227
|
+
"""Keep repository text selectable without Qt markup detection."""
|
|
228
|
+
label = WrappingLabel(text)
|
|
229
|
+
label.setTextFormat(Qt.TextFormat.PlainText)
|
|
230
|
+
label.setWordWrap(True)
|
|
231
|
+
label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
|
232
|
+
return label
|
|
233
|
+
|
|
234
|
+
def _table(self, caption: PreviewText) -> QTableView:
|
|
235
|
+
"""Use native scrolling and selection without editors or per-cell widgets."""
|
|
236
|
+
table = QTableView()
|
|
237
|
+
table.setAccessibleName(caption.value)
|
|
238
|
+
table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
|
239
|
+
table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
|
240
|
+
table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
|
241
|
+
table.setSortingEnabled(False)
|
|
242
|
+
table.setWordWrap(False)
|
|
243
|
+
table.setTextElideMode(Qt.TextElideMode.ElideRight)
|
|
244
|
+
table.setShowGrid(False)
|
|
245
|
+
table.verticalHeader().hide()
|
|
246
|
+
table.horizontalHeader().setSectionsClickable(False)
|
|
247
|
+
table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
|
|
248
|
+
table.installEventFilter(self)
|
|
249
|
+
return table
|
|
250
|
+
|
|
251
|
+
def _table_section(self, title: PreviewText, count: WrappingLabel, table: QTableView) -> QWidget:
|
|
252
|
+
"""Keep headings outside the table viewport as its rows scroll."""
|
|
253
|
+
section = QWidget()
|
|
254
|
+
layout = QVBoxLayout(section)
|
|
255
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
256
|
+
layout.setSpacing(SPACING_SM)
|
|
257
|
+
layout.addWidget(self._heading(title))
|
|
258
|
+
layout.addWidget(count)
|
|
259
|
+
layout.addWidget(table, 1)
|
|
260
|
+
return section
|
|
261
|
+
|
|
262
|
+
def _heading(self, title: PreviewText) -> WrappingLabel:
|
|
263
|
+
"""Emphasize section titles while inheriting the native family and size."""
|
|
264
|
+
label = self._label(title.value)
|
|
265
|
+
font = QFont()
|
|
266
|
+
font.setBold(True)
|
|
267
|
+
label.setFont(font)
|
|
268
|
+
return label
|
|
269
|
+
|
|
270
|
+
def _hash_column_width(self, table: QTableView, column_index: int) -> int:
|
|
271
|
+
"""Reserve a full short hash plus native cell padding and header space."""
|
|
272
|
+
option = QStyleOptionViewItem()
|
|
273
|
+
table.initViewItemOption(option)
|
|
274
|
+
widest_digit = max("0123456789abcdef", key=lambda digit: option.fontMetrics.horizontalAdvance(digit * SHORT_OID_LENGTH))
|
|
275
|
+
option.text = widest_digit * SHORT_OID_LENGTH
|
|
276
|
+
if table == self.branch_table and column_index == BranchColumn.NEW_TIP.index:
|
|
277
|
+
option.text += f" — {ModelText.TIP_DELETED.value}"
|
|
278
|
+
option.features |= QStyleOptionViewItem.ViewItemFeature.HasDisplay
|
|
279
|
+
cell_width = table.style().sizeFromContents(QStyle.ContentsType.CT_ItemViewItem, option, QSize(), table).width()
|
|
280
|
+
return max(cell_width, table.horizontalHeader().sectionSizeHint(column_index))
|
|
281
|
+
|
|
282
|
+
def apply_metrics(self) -> None:
|
|
283
|
+
"""Size columns from the current native font without scanning every row."""
|
|
284
|
+
metrics = self.fontMetrics()
|
|
285
|
+
row_height = history_row_height(metrics.height())
|
|
286
|
+
self.target_text.setMaximumHeight(row_height * TARGET_LINE_COUNT)
|
|
287
|
+
self.status_scroll.setMaximumHeight(row_height * TARGET_LINE_COUNT)
|
|
288
|
+
self.warning_scroll.setMinimumHeight(metrics.lineSpacing() * MINIMUM_WARNING_LINE_COUNT + SPACING_MD)
|
|
289
|
+
for table in (self.branch_table, self.commit_table):
|
|
290
|
+
table.verticalHeader().setDefaultSectionSize(row_height)
|
|
291
|
+
table.verticalHeader().setMinimumSectionSize(row_height)
|
|
292
|
+
table.horizontalHeader().setMinimumSectionSize(SPACING_MD)
|
|
293
|
+
for column in CommitColumn:
|
|
294
|
+
sample = column.value
|
|
295
|
+
if column == CommitColumn.COMMIT:
|
|
296
|
+
self.commit_table.setColumnWidth(column.index, self._hash_column_width(self.commit_table, column.index))
|
|
297
|
+
continue
|
|
298
|
+
elif column == CommitColumn.MESSAGE:
|
|
299
|
+
sample = "M" * MESSAGE_MINIMUM_CHARACTERS
|
|
300
|
+
elif column == CommitColumn.AUTHOR:
|
|
301
|
+
sample = "M" * AUTHOR_MINIMUM_CHARACTERS
|
|
302
|
+
elif column == CommitColumn.DATE:
|
|
303
|
+
sample = "2000-12-31 23:59 +1200"
|
|
304
|
+
self.commit_table.setColumnWidth(column.index, metrics.horizontalAdvance(sample) + SPACING_MD)
|
|
305
|
+
for column in BranchColumn:
|
|
306
|
+
if column in (BranchColumn.CURRENT_TIP, BranchColumn.NEW_TIP):
|
|
307
|
+
self.branch_table.setColumnWidth(column.index, self._hash_column_width(self.branch_table, column.index))
|
|
308
|
+
continue
|
|
309
|
+
sample = "M" * PREVIEW_BRANCH_MINIMUM_CHARACTERS
|
|
310
|
+
self.branch_table.setColumnWidth(column.index, metrics.horizontalAdvance(sample) + SPACING_MD)
|
|
311
|
+
apply_form_metrics(self)
|
|
312
|
+
self._fit_message_column()
|
|
313
|
+
|
|
314
|
+
def _fit_message_column(self) -> None:
|
|
315
|
+
"""Give the subject remaining viewport space while preserving its readable minimum."""
|
|
316
|
+
minimum = self.fontMetrics().horizontalAdvance("M" * MESSAGE_MINIMUM_CHARACTERS) + SPACING_MD
|
|
317
|
+
other_width = 0
|
|
318
|
+
for column in CommitColumn:
|
|
319
|
+
if column != CommitColumn.MESSAGE:
|
|
320
|
+
other_width += self.commit_table.columnWidth(column.index)
|
|
321
|
+
width = max(minimum, self.commit_table.viewport().width() - other_width)
|
|
322
|
+
if self.commit_table.columnWidth(CommitColumn.MESSAGE.index) != width:
|
|
323
|
+
self.commit_table.setColumnWidth(CommitColumn.MESSAGE.index, width)
|
|
324
|
+
|
|
325
|
+
def _refresh_theme(self) -> None:
|
|
326
|
+
"""Follow native contrast preference without replacing the prepared models."""
|
|
327
|
+
self.status_delegate.high_contrast = desktop_prefers_high_contrast(QApplication.instance())
|
|
328
|
+
self.status_delegate.refresh_theme()
|
|
329
|
+
for status, marker in self.legend_marker_by_status.items():
|
|
330
|
+
palette = self.commit_table.palette()
|
|
331
|
+
for group in (QPalette.ColorGroup.Active, QPalette.ColorGroup.Inactive, QPalette.ColorGroup.Disabled):
|
|
332
|
+
palette.setCurrentColorGroup(group)
|
|
333
|
+
palette.setColor(group, QPalette.ColorRole.Text, status_color(status, palette, high_contrast=self.status_delegate.high_contrast))
|
|
334
|
+
marker.setPalette(palette)
|
|
335
|
+
self.commit_table.viewport().update()
|
|
336
|
+
|
|
337
|
+
def changeEvent(self, event: QEvent) -> None:
|
|
338
|
+
"""Recalculate text metrics only when fonts or styles change."""
|
|
339
|
+
super().changeEvent(event)
|
|
340
|
+
if event.type() in (QEvent.Type.FontChange, QEvent.Type.StyleChange) and hasattr(self, "button_band"):
|
|
341
|
+
self.apply_metrics()
|
|
342
|
+
self._refresh_theme()
|
|
343
|
+
|
|
344
|
+
def _set_state(self, state: PreviewState) -> None:
|
|
345
|
+
"""Derive available actions from one explicit lifecycle state."""
|
|
346
|
+
self.state = state
|
|
347
|
+
self.status_scroll.setVisible(state != PreviewState.READY)
|
|
348
|
+
self.confirm_button.setEnabled(state == PreviewState.READY)
|
|
349
|
+
self.abort_button.setEnabled(state not in (PreviewState.STOPPING, PreviewState.EXECUTING, PreviewState.CLOSED))
|
|
350
|
+
self.recompute_button.setVisible(state in (PreviewState.ERROR, PreviewState.STALE))
|
|
351
|
+
|
|
352
|
+
def set_preparation(self, preparation: RewritePreparation) -> None:
|
|
353
|
+
"""Show the complete result, refusing partial or unreadable proposals."""
|
|
354
|
+
if self.state in (PreviewState.STOPPING, PreviewState.EXECUTING, PreviewState.CLOSED):
|
|
355
|
+
return
|
|
356
|
+
self.preparation = preparation
|
|
357
|
+
if preparation.capture.head_ref:
|
|
358
|
+
self.active_branch = preparation.capture.head_ref
|
|
359
|
+
self._show_target()
|
|
360
|
+
old_branch_model, old_commit_model = self.branch_model, self.commit_model
|
|
361
|
+
self.branch_model = BranchPreviewModel(preparation.branches, preparation.capture.head_ref)
|
|
362
|
+
self.commit_model = CommitPreviewModel(preparation.rows)
|
|
363
|
+
self.branch_table.setModel(self.branch_model)
|
|
364
|
+
self.commit_table.setModel(self.commit_model)
|
|
365
|
+
old_branch_model.deleteLater()
|
|
366
|
+
old_commit_model.deleteLater()
|
|
367
|
+
self._update_counts()
|
|
368
|
+
self._update_warning(preparation)
|
|
369
|
+
if preparation.target != self.target:
|
|
370
|
+
self.set_error(PreviewText.DIFFERENT_TARGET.value)
|
|
371
|
+
return
|
|
372
|
+
if not preparation.complete or not preparation.capture.complete or preparation.refusal:
|
|
373
|
+
self.set_error(preparation.refusal or PreviewText.MISSING_METADATA.value)
|
|
374
|
+
return
|
|
375
|
+
if not preparation.rows and not preparation.branches:
|
|
376
|
+
self._set_state(PreviewState.EMPTY)
|
|
377
|
+
self.status_label.setText(PreviewText.EMPTY.value + "\n" + PreviewText.EMPTY_BODY.value.format(path=safe_text(self.target.path)))
|
|
378
|
+
return
|
|
379
|
+
try:
|
|
380
|
+
self._validate_metadata(preparation)
|
|
381
|
+
except (ValueError, OSError, OverflowError) as error:
|
|
382
|
+
self.set_error(str(error), incomplete=True)
|
|
383
|
+
return
|
|
384
|
+
self._set_state(PreviewState.READY)
|
|
385
|
+
self.status_label.setText("")
|
|
386
|
+
|
|
387
|
+
def _show_target(self) -> None:
|
|
388
|
+
"""Keep the branch heading consistent with the newly computed proposal."""
|
|
389
|
+
scope = PreviewText.FOLDER if self.target.kind == RewriteTargetKind.FOLDER else PreviewText.FILE
|
|
390
|
+
self.target_text.setPlainText("\n".join((scope.value.format(path=safe_text(self.target.path)),
|
|
391
|
+
PreviewText.REPOSITORY.value.format(repository=safe_text(self.repository_dir)),
|
|
392
|
+
PreviewText.BRANCH.value.format(branch=safe_text(self.active_branch.removeprefix(LOCAL_PREFIX))))))
|
|
393
|
+
|
|
394
|
+
def _validate_metadata(self, preparation: RewritePreparation) -> None:
|
|
395
|
+
"""Reject missing required metadata while keeping an actually empty message valid."""
|
|
396
|
+
if not preparation.rows or not preparation.branches:
|
|
397
|
+
raise ValueError(PreviewText.MISSING_METADATA.value)
|
|
398
|
+
message_size = None
|
|
399
|
+
if preparation.message_filename:
|
|
400
|
+
with open(preparation.message_filename, "rb") as stream:
|
|
401
|
+
message_size = os.fstat(stream.fileno()).st_size
|
|
402
|
+
for row in preparation.rows:
|
|
403
|
+
if not row.author or not row.oid:
|
|
404
|
+
raise ValueError(PreviewText.MISSING_METADATA.value)
|
|
405
|
+
author_date(row.date)
|
|
406
|
+
if row.message_offset is not None and (message_size is None or row.message_offset < 0 or row.message_length < 0 or row.message_offset + row.message_length > message_size):
|
|
407
|
+
raise ValueError(DetailsText.INCOMPLETE.value)
|
|
408
|
+
for branch in preparation.branches:
|
|
409
|
+
if not branch.name or not branch.old_oid or not branch.new_oid:
|
|
410
|
+
raise ValueError(PreviewText.MISSING_METADATA.value)
|
|
411
|
+
|
|
412
|
+
def _update_counts(self) -> None:
|
|
413
|
+
"""Count only displayed contributions while retaining every affected branch."""
|
|
414
|
+
branch_count = len(self.branch_model.branches)
|
|
415
|
+
self.branch_count_label.setText((PreviewText.ONE_BRANCH if branch_count == 1 else PreviewText.MANY_BRANCHES).value.format(count=branch_count))
|
|
416
|
+
modified_count = sum(row.status == RewriteStatus.MODIFIED for row in self.commit_model.rows)
|
|
417
|
+
deleted_count = len(self.commit_model.rows) - modified_count
|
|
418
|
+
modified = (PreviewText.ONE_MODIFIED if modified_count == 1 else PreviewText.MANY_MODIFIED).value.format(count=modified_count)
|
|
419
|
+
deleted = (PreviewText.ONE_DELETED if deleted_count == 1 else PreviewText.MANY_DELETED).value.format(count=deleted_count)
|
|
420
|
+
self.commit_count_label.setText(modified + "; " + deleted)
|
|
421
|
+
|
|
422
|
+
def _update_warning(self, preparation: RewritePreparation) -> None:
|
|
423
|
+
"""Explain all consequences using complete domain counts and already known context."""
|
|
424
|
+
paragraphs = [PreviewText.FINAL.value.format(path=safe_text(self.target.path)), PreviewText.PUBLICATION.value, PreviewText.PUBLISHED.value]
|
|
425
|
+
if preparation.signed_count == 0:
|
|
426
|
+
paragraphs.append(PreviewText.NO_SIGNATURE.value)
|
|
427
|
+
else:
|
|
428
|
+
caption = PreviewText.ONE_SIGNATURE if preparation.signed_count == 1 else PreviewText.MANY_SIGNATURES
|
|
429
|
+
paragraphs.append(caption.value.format(count=preparation.signed_count))
|
|
430
|
+
if preparation.current_absent:
|
|
431
|
+
paragraphs.append(PreviewText.ABSENT.value)
|
|
432
|
+
paragraphs.append(PreviewText.DISK_INDEX.value)
|
|
433
|
+
if self.active_submodule:
|
|
434
|
+
paragraphs.extend((PreviewText.SUBMODULE.value, PreviewText.SUBMODULE_PARENT.value, PreviewText.SUBMODULE_CONSUMERS.value, PreviewText.SUBMODULE_ACCEPT.value))
|
|
435
|
+
if self.parent_repository_dir:
|
|
436
|
+
paragraphs.append(PreviewText.KNOWN_PARENT.value.format(repository=safe_text(self.parent_repository_dir)))
|
|
437
|
+
paragraphs.append(PreviewText.RETAINED.value)
|
|
438
|
+
self.warning_label.setText("\n\n".join(paragraphs))
|
|
439
|
+
|
|
440
|
+
def set_refusal(self, refusal: RewriteRefusal) -> None:
|
|
441
|
+
"""Show every supplied refusal witness without guessing a new repository state."""
|
|
442
|
+
paragraphs = [refusal.detail]
|
|
443
|
+
if refusal.path is not None:
|
|
444
|
+
paragraphs.append(PreviewText.REFUSAL_PATH.value.format(path=refusal.path.decode("utf-8", "surrogateescape")))
|
|
445
|
+
if refusal.commit_oid:
|
|
446
|
+
paragraphs.append(PreviewText.REFUSAL_COMMIT.value.format(commit=refusal.commit_oid))
|
|
447
|
+
if refusal.branch:
|
|
448
|
+
paragraphs.append(PreviewText.REFUSAL_BRANCH.value.format(branch=refusal.branch))
|
|
449
|
+
self.set_error("\n".join(paragraphs))
|
|
450
|
+
|
|
451
|
+
def set_error(self, cause: str, *, incomplete: bool = False) -> None:
|
|
452
|
+
"""Keep an actionable preparation failure visible with Confirm unavailable."""
|
|
453
|
+
if self.state in (PreviewState.STOPPING, PreviewState.CLOSED):
|
|
454
|
+
return
|
|
455
|
+
self._set_state(PreviewState.ERROR)
|
|
456
|
+
self.status_label.show_warning((PreviewText.INCOMPLETE if incomplete else PreviewText.ERROR).value.format(cause=safe_text(cause, multiline=True)))
|
|
457
|
+
|
|
458
|
+
def mark_stale(self, cause: str = "") -> None:
|
|
459
|
+
"""Revoke consent as soon as the controller observes changed repository facts."""
|
|
460
|
+
if self.state in (PreviewState.STOPPING, PreviewState.CLOSED):
|
|
461
|
+
return
|
|
462
|
+
self._set_state(PreviewState.STALE)
|
|
463
|
+
self.status_label.setText(PreviewText.STALE.value + "\n" + PreviewText.STALE_BODY.value + ("\n" + safe_text(cause, multiline=True) if cause else ""))
|
|
464
|
+
|
|
465
|
+
def finish_publication(self) -> None:
|
|
466
|
+
"""Close an executed proposal without reporting a harmless preview cancellation."""
|
|
467
|
+
self._set_state(PreviewState.CLOSED)
|
|
468
|
+
super().accept()
|
|
469
|
+
|
|
470
|
+
def _recompute(self) -> None:
|
|
471
|
+
"""Request one fresh calculation without carrying forward prior approval."""
|
|
472
|
+
if self.state not in (PreviewState.ERROR, PreviewState.STALE):
|
|
473
|
+
return
|
|
474
|
+
self._set_state(PreviewState.PREPARING)
|
|
475
|
+
self.status_label.setText(PreviewText.PREPARING.value)
|
|
476
|
+
self.recompute_requested.emit()
|
|
477
|
+
|
|
478
|
+
def _confirm(self) -> None:
|
|
479
|
+
"""Disable repeated submission before handing the typed proposal to the controller."""
|
|
480
|
+
if self.state != PreviewState.READY or self.preparation is None:
|
|
481
|
+
return
|
|
482
|
+
self._set_state(PreviewState.EXECUTING)
|
|
483
|
+
self.status_label.setText(PreviewText.EXECUTING.value)
|
|
484
|
+
self.confirm_requested.emit(self.preparation)
|
|
485
|
+
|
|
486
|
+
def reject(self) -> None:
|
|
487
|
+
"""Ask the controller to stop private work before closing this preview."""
|
|
488
|
+
if self.state in (PreviewState.STOPPING, PreviewState.EXECUTING, PreviewState.CLOSED):
|
|
489
|
+
return
|
|
490
|
+
self._set_state(PreviewState.STOPPING)
|
|
491
|
+
self.status_label.setText(PreviewText.STOPPING.value)
|
|
492
|
+
self.abort_requested.emit()
|
|
493
|
+
|
|
494
|
+
def finish_abort(self) -> None:
|
|
495
|
+
"""Close only after the controller confirms that private work has stopped."""
|
|
496
|
+
self._set_state(PreviewState.CLOSED)
|
|
497
|
+
super().reject()
|
|
498
|
+
|
|
499
|
+
def closeEvent(self, event: QCloseEvent) -> None:
|
|
500
|
+
"""Make title-bar closure follow the same drain requirement as Abort."""
|
|
501
|
+
if self.state == PreviewState.CLOSED:
|
|
502
|
+
super().closeEvent(event)
|
|
503
|
+
return
|
|
504
|
+
event.ignore()
|
|
505
|
+
self.reject()
|
|
506
|
+
|
|
507
|
+
def keyPressEvent(self, event: QKeyEvent) -> None:
|
|
508
|
+
"""Require explicit button focus for Enter instead of activating a hidden default."""
|
|
509
|
+
if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
|
|
510
|
+
focused = self.focusWidget()
|
|
511
|
+
if focused in (self.confirm_button, self.abort_button, self.recompute_button):
|
|
512
|
+
focused.click()
|
|
513
|
+
event.accept()
|
|
514
|
+
return
|
|
515
|
+
super().keyPressEvent(event)
|
|
516
|
+
|
|
517
|
+
def eventFilter(self, watched: QObject, event: QEvent) -> bool:
|
|
518
|
+
"""Offer complete keyboard reading without treating navigation as consent."""
|
|
519
|
+
if event.type() == QEvent.Type.Resize and hasattr(self, "commit_table") and watched == self.commit_table.viewport():
|
|
520
|
+
self._fit_message_column()
|
|
521
|
+
# QScrollArea handles arrows and page keys but leaves Home/End unhandled.
|
|
522
|
+
if event.type() == QEvent.Type.KeyPress and watched == self.warning_scroll and event.key() in (Qt.Key.Key_Home, Qt.Key.Key_End):
|
|
523
|
+
scrollbar = self.warning_scroll.verticalScrollBar()
|
|
524
|
+
scrollbar.setValue(scrollbar.minimum() if event.key() == Qt.Key.Key_Home else scrollbar.maximum())
|
|
525
|
+
event.accept()
|
|
526
|
+
return True
|
|
527
|
+
if event.type() == QEvent.Type.KeyPress and event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
|
|
528
|
+
if watched == self.branch_table:
|
|
529
|
+
self._branch_details(self.branch_table.currentIndex())
|
|
530
|
+
elif watched == self.commit_table:
|
|
531
|
+
self._commit_details(self.commit_table.currentIndex())
|
|
532
|
+
else:
|
|
533
|
+
return super().eventFilter(watched, event)
|
|
534
|
+
return True
|
|
535
|
+
return super().eventFilter(watched, event)
|
|
536
|
+
|
|
537
|
+
def _branch_details(self, index: QModelIndex) -> None:
|
|
538
|
+
"""Open full branch identities without querying the repository."""
|
|
539
|
+
if index.isValid():
|
|
540
|
+
self._show_details(self.branch_model.details(index.row()))
|
|
541
|
+
|
|
542
|
+
def _commit_details(self, index: QModelIndex) -> None:
|
|
543
|
+
"""Load only the chosen message and refuse confirmation if its spool was lost."""
|
|
544
|
+
if not index.isValid() or self.preparation is None:
|
|
545
|
+
return
|
|
546
|
+
try:
|
|
547
|
+
text = commit_details(self.commit_model.rows[index.row()], self.preparation.message_filename)
|
|
548
|
+
except (ValueError, OSError, OverflowError) as error:
|
|
549
|
+
self.set_error(str(error), incomplete=True)
|
|
550
|
+
text = safe_text(str(error))
|
|
551
|
+
self._show_details(text)
|
|
552
|
+
|
|
553
|
+
def _show_details(self, text: str) -> None:
|
|
554
|
+
"""Retain one modal child until it closes."""
|
|
555
|
+
self.details_dialog = HistoryRewriteDetails(text, self)
|
|
556
|
+
self.details_dialog.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
|
|
557
|
+
self.details_dialog.finished.connect(self._detail_closed)
|
|
558
|
+
self.details_dialog.show_modal()
|
|
559
|
+
|
|
560
|
+
def _detail_closed(self, _result: int) -> None:
|
|
561
|
+
self.details_dialog = None
|
|
562
|
+
|
|
563
|
+
def show_modal(self) -> None:
|
|
564
|
+
"""Show through the native-window policy without beginning any Git work."""
|
|
565
|
+
open_window_modal(self)
|