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,797 @@
|
|
|
1
|
+
"""Run an interactive rebase from a repository window, through its pauses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from PySide6.QtCore import QObject, QSignalBlocker, Signal
|
|
10
|
+
from shiboken6 import isValid
|
|
11
|
+
|
|
12
|
+
from gitpane.config import LOCAL_PREFIX, REBASE_REFRESH_INTERVAL_MS, SHORT_OID_LENGTH, ConsoleOperation, ProcessPriority, SharedText
|
|
13
|
+
from gitpane.controller.refresh_controller import RebaseRefreshPublication
|
|
14
|
+
from gitpane.history.file_history import WindowCapability
|
|
15
|
+
from gitpane.history.repository_value import GIT_CONFLICT_RETURN_CODE
|
|
16
|
+
from gitpane.operation.console_line import COMPLETED_STATUS_SET, END_TEXT_TEMPLATE, REBASE_LABEL, REPOSITORY_HEADER_LABEL, ConsoleHeader, operation_end_text
|
|
17
|
+
from gitpane.operation.diagnostic import Diagnostic
|
|
18
|
+
from gitpane.operation.operation_status import OperationStatus
|
|
19
|
+
from gitpane.operation.operation_trace import OperationTrace, publish_after_trace
|
|
20
|
+
from gitpane.operation.repository_operation import REBASE_PAUSE_OBSERVATION, OperationAction, OperationIntent, OperationOutcome, WriteScope
|
|
21
|
+
from gitpane.preference.preference_store import REBASE_PROMPT_PREFERENCE_KEY
|
|
22
|
+
from gitpane.sequencer.conflict import conflict_path_label
|
|
23
|
+
from gitpane.sequencer.rebase import RebaseIntent, RebaseOutcome
|
|
24
|
+
from gitpane.sequencer.rebase_state import REBASE_ENDED_OUTSIDE_MESSAGE, REBASE_STOPPED_STATE_SET, RebaseSnapshot, native_observations_unchanged, read_rebase_commits, state_dirs_from_pause_paths
|
|
25
|
+
from gitpane.ui.console.operation_dialog import OperationDialog
|
|
26
|
+
from gitpane.ui.sequencer.rebase_dialog import RebaseConfirmation, RebaseDialog
|
|
27
|
+
from gitpane.ui.sequencer.rebase_editor_window import EditorValidation
|
|
28
|
+
from gitpane.ui.window_control.dialog_modality import open_window_modal
|
|
29
|
+
from gitpane.worktree.command_sequence import FileTask, GitCommandError
|
|
30
|
+
from gitpane.worktree.worktree_snapshot import WorktreeKey
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from collections.abc import Callable, Generator
|
|
34
|
+
|
|
35
|
+
from gitpane.controller.console_registry import ConsoleRegistry
|
|
36
|
+
from gitpane.controller.history_read_controller import HistoryReadController
|
|
37
|
+
from gitpane.controller.operation_policy import OperationPolicy
|
|
38
|
+
from gitpane.controller.rebase_editor_controller import RebaseEditorController
|
|
39
|
+
from gitpane.controller.reference_read_controller import ReferenceReadController
|
|
40
|
+
from gitpane.controller.refresh_controller import FullRefreshRead, RefreshController, ReplayRefreshPublication
|
|
41
|
+
from gitpane.controller.replay_controller import ReplayController
|
|
42
|
+
from gitpane.controller.session_registry import SessionRegistry
|
|
43
|
+
from gitpane.controller.staging_controller import StagingController
|
|
44
|
+
from gitpane.controller.staging_session_registry import StagingSessionRegistry
|
|
45
|
+
from gitpane.controller.window_activation import WindowActivationFilter
|
|
46
|
+
from gitpane.controller.worktree_state_controller import WorktreeStateController
|
|
47
|
+
from gitpane.history.repository import GitRepository
|
|
48
|
+
from gitpane.history.repository_value import RepositoryContext
|
|
49
|
+
from gitpane.operation.diagnostic import DiagnosticLog
|
|
50
|
+
from gitpane.operation.repository_operation import RepositoryOperationService
|
|
51
|
+
from gitpane.preference.preference_store import PreferenceStore
|
|
52
|
+
from gitpane.reference.reference_snapshot import ReferenceSnapshot
|
|
53
|
+
from gitpane.sequencer.mergetool import MergetoolService
|
|
54
|
+
from gitpane.sequencer.rebase import RebaseService
|
|
55
|
+
from gitpane.sequencer.rebase_state import RebaseCommit, RebaseStep
|
|
56
|
+
from gitpane.tool.editor import EditorRequest
|
|
57
|
+
from gitpane.worktree.worktree_service import WorktreeService
|
|
58
|
+
|
|
59
|
+
# Console line of a rebase that stopped on a conflict, followed by the conflicting files separated by ", ".
|
|
60
|
+
REBASE_CONFLICT_PAUSE_PREFIX = "Rebase paused: new conflict in "
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def outcome_instruction_error(outcome: OperationOutcome) -> str:
|
|
64
|
+
"""Answer the error lines of the instructions Git refused and kept for a correction, or an empty text for any other result.
|
|
65
|
+
|
|
66
|
+
Only a rebase command reads them; a refusal before any command, or an Abort, never carries them.
|
|
67
|
+
"""
|
|
68
|
+
return outcome.instruction_error if isinstance(outcome, RebaseOutcome) else ""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def rebase_end_notice(snapshot: ReferenceSnapshot | None) -> str:
|
|
72
|
+
"""Say that a rebase ended outside GitPane and where HEAD is now, as the references just read tell it."""
|
|
73
|
+
if snapshot is None or snapshot.head_oid is None:
|
|
74
|
+
return REBASE_ENDED_OUTSIDE_MESSAGE
|
|
75
|
+
short_oid = snapshot.head_oid[:SHORT_OID_LENGTH]
|
|
76
|
+
if snapshot.head_ref is not None and snapshot.head_ref.startswith(LOCAL_PREFIX):
|
|
77
|
+
return f"{REBASE_ENDED_OUTSIDE_MESSAGE} HEAD is on {snapshot.head_ref.removeprefix(LOCAL_PREFIX)} at {short_oid}."
|
|
78
|
+
return f"{REBASE_ENDED_OUTSIDE_MESSAGE} HEAD is detached at {short_oid}."
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def rebase_conflict_pause_text(outcome: OperationOutcome, state: RebaseSnapshot | None) -> str | None:
|
|
82
|
+
"""Return the line naming the files of the new conflict a rebase command stopped on, or None when it did not stop on one.
|
|
83
|
+
|
|
84
|
+
Editing the instructions during a conflict also ends paused, but Git did not stop on a new conflict then: only a
|
|
85
|
+
command Git ended with its conflict exit status is named that way.
|
|
86
|
+
"""
|
|
87
|
+
stopped_on_conflict = any(step.return_code == GIT_CONFLICT_RETURN_CODE for step in outcome.steps)
|
|
88
|
+
if outcome.status != OperationStatus.PAUSED or not stopped_on_conflict or state is None or state.state != "conflict" or not state.conflict_paths:
|
|
89
|
+
return None
|
|
90
|
+
labels = []
|
|
91
|
+
for path in state.conflict_paths:
|
|
92
|
+
labels.append(conflict_path_label(path))
|
|
93
|
+
return REBASE_CONFLICT_PAUSE_PREFIX + ", ".join(labels)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def rebase_result_line(outcome: OperationOutcome, state: RebaseSnapshot | None) -> str:
|
|
97
|
+
"""Write the console line ending a rebase command, such as ``Rebase: done``; a stop on a new conflict names its files instead."""
|
|
98
|
+
pause_text = rebase_conflict_pause_text(outcome, state)
|
|
99
|
+
if pause_text is not None:
|
|
100
|
+
return pause_text
|
|
101
|
+
return operation_end_text(REBASE_LABEL, outcome.status)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def rebase_result_log_text(outcome: OperationOutcome, state: RebaseSnapshot | None) -> str:
|
|
105
|
+
"""Write the ``--log`` line ending a rebase command: the bare status, as the log always kept it, or the same conflict line as the console."""
|
|
106
|
+
pause_text = rebase_conflict_pause_text(outcome, state)
|
|
107
|
+
if pause_text is not None:
|
|
108
|
+
return pause_text
|
|
109
|
+
return END_TEXT_TEMPLATE.format(label=REBASE_LABEL, word=outcome.status.value)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class RebaseController(QObject):
|
|
113
|
+
"""The interactive rebase of every repository window: confirmation, launch, console, Rebase window with Continue, Amend and Abort, and the observation of a pause or of a rebase ended outside GitPane."""
|
|
114
|
+
|
|
115
|
+
# Asks to show, in place of the rebase progress, that a repository window waits for its work to end before it
|
|
116
|
+
# closes or GitPane quits: the session id of that window.
|
|
117
|
+
close_wait_requested = Signal(str)
|
|
118
|
+
# Tells the rebase state a repository window has just shown, for the conflict window of that window: the session id
|
|
119
|
+
# and the RebaseSnapshot.
|
|
120
|
+
rebase_state_shown = Signal(str, object)
|
|
121
|
+
# Asks to open the conflict list of a repository window from its Rebase window: the session id.
|
|
122
|
+
conflict_list_requested = Signal(str)
|
|
123
|
+
|
|
124
|
+
def __init__(self, *, session_registry_controller: SessionRegistry, operation_policy_controller: OperationPolicy, staging_session_registry_controller: StagingSessionRegistry, window_activation_controller: WindowActivationFilter,
|
|
125
|
+
console_registry_controller: ConsoleRegistry, reference_read_controller: ReferenceReadController, refresh_controller: RefreshController, worktree_state_controller: WorktreeStateController,
|
|
126
|
+
history_read_controller: HistoryReadController, staging_controller: StagingController, replay_controller: ReplayController, rebase_editor_controller: RebaseEditorController, preference: PreferenceStore,
|
|
127
|
+
repository: GitRepository, worktree_service: WorktreeService, repository_operation: RepositoryOperationService, rebase: RebaseService, mergetool: MergetoolService, diagnostic_log: DiagnosticLog | None,
|
|
128
|
+
parent: QObject | None = None) -> None:
|
|
129
|
+
"""Receive the services and the lower controllers a rebase works with; the wiring builds it after the replay controller, and the conflict, the close readiness and the repository windows reach it through the wiring.
|
|
130
|
+
|
|
131
|
+
Every edit Git asks for during one of its commands is served by the rebase editor controller, told before each
|
|
132
|
+
command which repository window parents the editor windows and whether their button continues or saves.
|
|
133
|
+
"""
|
|
134
|
+
super().__init__(parent)
|
|
135
|
+
self.session_registry_controller = session_registry_controller
|
|
136
|
+
self.operation_policy_controller = operation_policy_controller
|
|
137
|
+
self.staging_session_registry_controller = staging_session_registry_controller
|
|
138
|
+
self.window_activation_controller = window_activation_controller
|
|
139
|
+
self.console_registry_controller = console_registry_controller
|
|
140
|
+
self.reference_read_controller = reference_read_controller
|
|
141
|
+
self.refresh_controller = refresh_controller
|
|
142
|
+
self.worktree_state_controller = worktree_state_controller
|
|
143
|
+
self.history_read_controller = history_read_controller
|
|
144
|
+
self.staging_controller = staging_controller
|
|
145
|
+
self.replay_controller = replay_controller
|
|
146
|
+
self.rebase_editor_controller = rebase_editor_controller
|
|
147
|
+
self.preference = preference
|
|
148
|
+
self.repository = repository
|
|
149
|
+
self.worktree_service = worktree_service
|
|
150
|
+
self.repository_operation = repository_operation
|
|
151
|
+
self.rebase = rebase
|
|
152
|
+
self.mergetool = mergetool
|
|
153
|
+
self.diagnostic_log = diagnostic_log
|
|
154
|
+
self.rebase_dialog_by_session: dict[str, RebaseDialog] = {}
|
|
155
|
+
self.rebase_confirmation_by_session: dict[str, RebaseConfirmation] = {}
|
|
156
|
+
self.rebase_intent_by_id: dict[str, RebaseIntent] = {}
|
|
157
|
+
self.rebase_final_oid_by_session: dict[str, str] = {}
|
|
158
|
+
# Windows owing the line that tells a rebase ended outside GitPane: a refresh of theirs is running, and the line
|
|
159
|
+
# is written once its history is published, so it names the HEAD that refresh read and is not hidden by it.
|
|
160
|
+
self.rebase_end_notice_session_set: set[str] = set()
|
|
161
|
+
self.rebase_observed_at_by_key: dict[WorktreeKey, float] = {}
|
|
162
|
+
# The validating button each command's editor windows were given, so instructions Git refuses reopen with the same one.
|
|
163
|
+
self.validation_by_operation_id: dict[str, EditorValidation] = {}
|
|
164
|
+
# The list edits reopened after Git refused a command that was to continue the rebase: once Git accepts the
|
|
165
|
+
# corrected list, the rebase continues.
|
|
166
|
+
self.continue_after_correction_operation_id_set: set[str] = set()
|
|
167
|
+
|
|
168
|
+
def start_rebase(self, session_id: str, target_oid: str, snapshot: ReferenceSnapshot | None) -> RebaseConfirmation | None:
|
|
169
|
+
"""Prepare a graph target against the loaded HEAD before one confirmation."""
|
|
170
|
+
if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE) or snapshot is None:
|
|
171
|
+
return None
|
|
172
|
+
session = self.session_registry_controller.session_by_id[session_id]
|
|
173
|
+
window = self.session_registry_controller.window_by_session[session_id]
|
|
174
|
+
if session.repository_context.is_bare:
|
|
175
|
+
return None
|
|
176
|
+
existing = self.rebase_confirmation_by_session.get(session_id)
|
|
177
|
+
if existing is not None and isValid(existing) and existing.isVisible():
|
|
178
|
+
existing.raise_()
|
|
179
|
+
return existing
|
|
180
|
+
operation = OperationIntent.capture(uuid.uuid4().hex, session.repository_context, self.repository.git_filename,
|
|
181
|
+
session_id=session_id, generation=self.session_registry_controller.generation_by_session[session_id], operation=REBASE_LABEL, action=OperationAction.REBASE_START,
|
|
182
|
+
scope=WriteScope.capture(session.repository_context, (snapshot.head_ref,) if snapshot.head_ref else ()))
|
|
183
|
+
intent = RebaseIntent(operation, target_oid, snapshot.head_oid, snapshot.head_ref)
|
|
184
|
+
|
|
185
|
+
def current() -> bool:
|
|
186
|
+
"""Keep a pending launch bound to the original repository session."""
|
|
187
|
+
return (self.operation_policy_controller.operation_allowed(operation) and self.repository.git_filename == operation.git_filename and snapshot.context == operation.context
|
|
188
|
+
and snapshot.git_filename == operation.git_filename and snapshot.generation == operation.generation)
|
|
189
|
+
|
|
190
|
+
def prepared(value: RebaseIntent | None, error: Exception | None) -> None:
|
|
191
|
+
"""Only a fully resolved source and target may reach confirmation."""
|
|
192
|
+
if session.is_closed or self.session_registry_controller.session_by_id.get(session_id) is not session:
|
|
193
|
+
return
|
|
194
|
+
if error is not None or value is None or not current():
|
|
195
|
+
self._rebase_refused(intent, error or ValueError("The rebase capture changed. Refresh before rebasing."))
|
|
196
|
+
return
|
|
197
|
+
source = (value.source_symbolic_ref or "Detached HEAD") + " (" + value.source_head_oid + ")"
|
|
198
|
+
previous = self.rebase_confirmation_by_session.get(session_id)
|
|
199
|
+
if previous is not None and isValid(previous):
|
|
200
|
+
# A confirmation answered earlier stays parented to the window until replaced; the newest request is the only one that may still launch.
|
|
201
|
+
previous.close()
|
|
202
|
+
previous.deleteLater()
|
|
203
|
+
dialog = RebaseConfirmation(source, value.target_oid, window, preference=self.preference)
|
|
204
|
+
self.rebase_confirmation_by_session[session_id] = dialog
|
|
205
|
+
dialog.submit_requested.connect(lambda: self._confirm_rebase(value, current, dialog))
|
|
206
|
+
if self.preference.read(REBASE_PROMPT_PREFERENCE_KEY):
|
|
207
|
+
dialog.dont_show_again.setChecked(True)
|
|
208
|
+
self._confirm_rebase(value, current, dialog)
|
|
209
|
+
else:
|
|
210
|
+
open_window_modal(dialog)
|
|
211
|
+
|
|
212
|
+
self.rebase.prepare(intent, is_current=current, callback=prepared)
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
def _confirm_rebase(self, intent: RebaseIntent, current: Callable[[], bool], dialog: RebaseConfirmation) -> None:
|
|
216
|
+
"""Launch the intention prepared before confirmation; the edits Git asks for open in GitPane, so no editor choice is needed.
|
|
217
|
+
|
|
218
|
+
The form is not prepared again: the start validates the target, HEAD and the absence of a native rebase inside
|
|
219
|
+
its reservation, and checks references once more at launch, so a change made since the confirmation opened is
|
|
220
|
+
refused there.
|
|
221
|
+
"""
|
|
222
|
+
if dialog.submitting or not isValid(dialog) or dialog.cancelled:
|
|
223
|
+
return
|
|
224
|
+
dialog.submitting = True
|
|
225
|
+
dialog.confirm_button.setEnabled(False)
|
|
226
|
+
if not current():
|
|
227
|
+
self._rebase_refused(intent, ValueError("The rebase capture changed. Refresh before rebasing."))
|
|
228
|
+
dialog.reject()
|
|
229
|
+
return
|
|
230
|
+
dialog.accept()
|
|
231
|
+
self._launch_rebase(intent, current)
|
|
232
|
+
|
|
233
|
+
def _create_rebase_output(self, intent: RebaseIntent) -> OperationDialog:
|
|
234
|
+
"""Keep rebase diagnostics visible while leaving the main window's deferred Close action accessible."""
|
|
235
|
+
operation = intent.operation
|
|
236
|
+
trace = OperationTrace(operation.operation_id, self.repository.runner, self.diagnostic_log, header=ConsoleHeader(REPOSITORY_HEADER_LABEL, operation.context.repository_dir))
|
|
237
|
+
output = OperationDialog(trace, REBASE_LABEL, self.session_registry_controller.window_by_session[operation.session_id], preference=self.preference, console_operation=ConsoleOperation.REBASE)
|
|
238
|
+
output.finished.connect(trace.close)
|
|
239
|
+
output.finished.connect(lambda: self._dispose_rebase_output(operation.operation_id))
|
|
240
|
+
self.rebase_intent_by_id[operation.operation_id] = intent
|
|
241
|
+
self.console_registry_controller.rebase_output_by_id[operation.operation_id] = output
|
|
242
|
+
output.show()
|
|
243
|
+
return output
|
|
244
|
+
|
|
245
|
+
def _dispose_rebase_output(self, operation_id: str) -> None:
|
|
246
|
+
"""Release a dismissed diagnostic window with its trace instead of retaining one per command for the whole session."""
|
|
247
|
+
output = self.console_registry_controller.rebase_output_by_id.pop(operation_id, None)
|
|
248
|
+
if output is not None and isValid(output):
|
|
249
|
+
output.deleteLater()
|
|
250
|
+
|
|
251
|
+
def _rebase_refused(self, intent: RebaseIntent, error: Exception) -> None:
|
|
252
|
+
"""Expose a preflight cause without creating an editor or repairing native state."""
|
|
253
|
+
operation = intent.operation
|
|
254
|
+
if not self.operation_policy_controller.allows(operation.session_id, WindowCapability.MANAGE):
|
|
255
|
+
return
|
|
256
|
+
diagnostic = error.diagnostic if isinstance(error, GitCommandError) else Diagnostic(REBASE_LABEL, operation.context.repository_dir, operation.git_filename, [], str(error)).prepare()
|
|
257
|
+
self._create_rebase_output(intent)
|
|
258
|
+
self.rebase_finished(OperationOutcome(operation.operation_id, OperationStatus.REFUSED, diagnostic=diagnostic))
|
|
259
|
+
|
|
260
|
+
def _launch_rebase(self, intent: RebaseIntent, current: Callable[[], bool]) -> None:
|
|
261
|
+
"""Create shared diagnostics and run one captured command whose edits open in GitPane, each with Continue."""
|
|
262
|
+
operation = intent.operation
|
|
263
|
+
if not current():
|
|
264
|
+
self._rebase_refused(intent, ValueError("The rebase capture changed. Refresh before rebasing."))
|
|
265
|
+
return
|
|
266
|
+
output = self._create_rebase_output(intent)
|
|
267
|
+
state = RebaseSnapshot(operation.key, operation.generation, head_oid=intent.source_head_oid, state="starting")
|
|
268
|
+
dialog = self._create_rebase_dialog(operation.session_id, state)
|
|
269
|
+
dialog.show()
|
|
270
|
+
self.rebase_progress(operation.operation_id, SharedText.REBASE_APPLYING_COMMITS.value)
|
|
271
|
+
output.set_running()
|
|
272
|
+
self._expect_edits(operation, EditorValidation.CONTINUE)
|
|
273
|
+
self.rebase.start(intent, is_current=current)
|
|
274
|
+
|
|
275
|
+
def _expect_edits(self, operation: OperationIntent, validation: EditorValidation, error_text: str = "") -> None:
|
|
276
|
+
"""Tell the rebase editor controller to serve the edits of this command in windows parented to its repository window.
|
|
277
|
+
|
|
278
|
+
An error text, such as the instructions Git refused, is shown above the text of the first window.
|
|
279
|
+
"""
|
|
280
|
+
self.validation_by_operation_id[operation.operation_id] = validation
|
|
281
|
+
self.rebase_editor_controller.expect_edits(operation.operation_id, operation.context, operation.git_filename, validation,
|
|
282
|
+
self.session_registry_controller.window_by_session.get(operation.session_id), error_text)
|
|
283
|
+
|
|
284
|
+
def rebase_editor_waiting(self, request: EditorRequest) -> None:
|
|
285
|
+
"""Say which file Git waits for while its GitPane editor window is open."""
|
|
286
|
+
self._rebase_editor_changed(request.operation_id, request.kind.progress_text)
|
|
287
|
+
|
|
288
|
+
def _rebase_editor_changed(self, operation_id: str, message: str) -> None:
|
|
289
|
+
"""Reread Git's edited list during message waits without confusing it with completion."""
|
|
290
|
+
intent = self.rebase_intent_by_id.get(operation_id)
|
|
291
|
+
if intent is None:
|
|
292
|
+
return
|
|
293
|
+
self.rebase_progress(operation_id, message)
|
|
294
|
+
|
|
295
|
+
def complete(state: RebaseSnapshot) -> None:
|
|
296
|
+
"""Apply a current command observation to its still-open worktree views."""
|
|
297
|
+
for session_id, session in tuple(self.session_registry_controller.session_by_id.items()):
|
|
298
|
+
if session.is_closed or WorktreeKey.from_context(session.repository_context) != intent.operation.key:
|
|
299
|
+
continue
|
|
300
|
+
dialog = self.rebase_dialog_by_session.get(session_id)
|
|
301
|
+
if dialog is not None and isValid(dialog):
|
|
302
|
+
dialog.show_state(state)
|
|
303
|
+
self.rebase_progress(operation_id, message)
|
|
304
|
+
|
|
305
|
+
self.rebase.observe(intent.operation.context, intent.operation.git_filename,
|
|
306
|
+
is_current=lambda: self.rebase_intent_by_id.get(operation_id) is intent, callback=complete)
|
|
307
|
+
|
|
308
|
+
def edit_rebase_todo(self, session_id: str) -> None:
|
|
309
|
+
"""Capture one explicit instruction correction and leave Git paused after its editor exits."""
|
|
310
|
+
# Save writes the list and leaves the rebase paused; the user resumes it with Continue rebase.
|
|
311
|
+
self._edit_rebase_todo(session_id, EditorValidation.SAVE, "")
|
|
312
|
+
|
|
313
|
+
def reopen_rebase_todo(self, session_id: str, validation: EditorValidation, error_text: str) -> None:
|
|
314
|
+
"""Open the editor again on the instructions Git refused and kept, with Git's error above what remains to do.
|
|
315
|
+
|
|
316
|
+
The correction reaches Git by git rebase --edit-todo, on the list Git holds: nothing done is replayed. With
|
|
317
|
+
Continue, the rebase continues once Git accepts the corrected list; with Save, it stays paused.
|
|
318
|
+
"""
|
|
319
|
+
self._edit_rebase_todo(session_id, validation, error_text)
|
|
320
|
+
|
|
321
|
+
def _edit_rebase_todo(self, session_id: str, validation: EditorValidation, error_text: str) -> None:
|
|
322
|
+
"""Submit one git rebase --edit-todo whose editor window validates with the given button and shows the given error first.
|
|
323
|
+
|
|
324
|
+
An edit that validates with Continue is remembered, so the rebase continues once Git accepts the edited list.
|
|
325
|
+
"""
|
|
326
|
+
if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
|
|
327
|
+
return
|
|
328
|
+
session = self.session_registry_controller.session_by_id[session_id]
|
|
329
|
+
state = self.rebase.state_by_key.get(WorktreeKey.from_context(session.repository_context))
|
|
330
|
+
if state is None or not state.fresh or state.identity is None or state.state not in REBASE_STOPPED_STATE_SET:
|
|
331
|
+
self.observe_rebase(session_id)
|
|
332
|
+
return
|
|
333
|
+
operation = OperationIntent.capture(uuid.uuid4().hex, session.repository_context, self.repository.git_filename,
|
|
334
|
+
session_id=session_id, generation=self.session_registry_controller.generation_by_session[session_id], operation="Edit rebase instructions",
|
|
335
|
+
action=OperationAction.REBASE_CONTINUE)
|
|
336
|
+
intent = RebaseIntent(operation, state.identity.onto_oid, state.head_oid, state.identity.head_name)
|
|
337
|
+
output = self._create_rebase_output(intent)
|
|
338
|
+
output.set_running()
|
|
339
|
+
self.rebase_progress(operation.operation_id, "Editing rebase instructions…")
|
|
340
|
+
self._expect_edits(operation, validation, error_text)
|
|
341
|
+
if validation == EditorValidation.CONTINUE:
|
|
342
|
+
self.continue_after_correction_operation_id_set.add(operation.operation_id)
|
|
343
|
+
self.rebase.edit_todo(operation, state, is_current=lambda: self.operation_policy_controller.operation_allowed(operation), error_before_edit=error_text)
|
|
344
|
+
|
|
345
|
+
def rebase_progress(self, operation_id: str, message: str) -> None:
|
|
346
|
+
"""Update tracking and banners without replacing native observation facts."""
|
|
347
|
+
intent = self.rebase_intent_by_id.get(operation_id)
|
|
348
|
+
if intent is None:
|
|
349
|
+
return
|
|
350
|
+
for session_id, session in tuple(self.session_registry_controller.session_by_id.items()):
|
|
351
|
+
if session.is_closed or WorktreeKey.from_context(session.repository_context) != intent.operation.key:
|
|
352
|
+
continue
|
|
353
|
+
window = self.session_registry_controller.window_by_session[session_id]
|
|
354
|
+
if self.session_registry_controller.quit_pending or session_id in self.session_registry_controller.close_session_set:
|
|
355
|
+
self.close_wait_requested.emit(session_id)
|
|
356
|
+
else:
|
|
357
|
+
window.rebase_label.setText(message)
|
|
358
|
+
window.rebase_banner.show()
|
|
359
|
+
dialog = self.rebase_dialog_by_session.get(session_id)
|
|
360
|
+
if dialog is not None and isValid(dialog):
|
|
361
|
+
dialog.status_label.setText(message)
|
|
362
|
+
dialog.command_message = ""
|
|
363
|
+
dialog.set_busy(True)
|
|
364
|
+
|
|
365
|
+
def follow_rebase_ended_outside(self, state: RebaseSnapshot) -> None:
|
|
366
|
+
"""Follow a rebase that ended without a command of GitPane, with no banner: close its Rebase window and say so in one status line.
|
|
367
|
+
|
|
368
|
+
The line names where HEAD is as the references tell it: a refresh already running, or the one a finishing
|
|
369
|
+
rebase command starts, reads them and the line is completed once its history is published; otherwise only the
|
|
370
|
+
references are read, which costs nothing when their files did not change. A HEAD that moved is brought to the
|
|
371
|
+
graph by the worktree read that sees it.
|
|
372
|
+
"""
|
|
373
|
+
if not state.ended_outside:
|
|
374
|
+
return
|
|
375
|
+
command_finishing = any(intent.operation.key == state.key for intent in self.rebase_intent_by_id.values())
|
|
376
|
+
for session_id, window in self.worktree_state_controller.managed_windows_of_worktree(state.key):
|
|
377
|
+
window.show_rebase_state(state)
|
|
378
|
+
dialog = self.rebase_dialog_by_session.get(session_id)
|
|
379
|
+
if dialog is not None and isValid(dialog):
|
|
380
|
+
dialog.show_state(state)
|
|
381
|
+
dialog.close()
|
|
382
|
+
self.rebase_end_notice_session_set.add(session_id)
|
|
383
|
+
window.statusBar().showMessage(REBASE_ENDED_OUTSIDE_MESSAGE)
|
|
384
|
+
if self.session_registry_controller.session_by_id[session_id].refresh_active or command_finishing:
|
|
385
|
+
continue
|
|
386
|
+
self.reference_read_controller.read_references(session_id, previous=self.reference_read_controller.reference_snapshot_by_session.get(session_id),
|
|
387
|
+
on_complete=lambda snapshot, _error, session_id=session_id: self.show_rebase_end_notice(session_id, snapshot))
|
|
388
|
+
|
|
389
|
+
def show_rebase_end_notice(self, session_id: str, snapshot: ReferenceSnapshot | None, *, after_history: bool = False) -> None:
|
|
390
|
+
"""Write, once, the line telling a window that a rebase ended outside GitPane, after the status line of a just published history if asked."""
|
|
391
|
+
window = self.session_registry_controller.window_by_session.get(session_id)
|
|
392
|
+
if session_id not in self.rebase_end_notice_session_set or window is None or not isValid(window):
|
|
393
|
+
return
|
|
394
|
+
self.rebase_end_notice_session_set.discard(session_id)
|
|
395
|
+
notice = rebase_end_notice(snapshot)
|
|
396
|
+
window.statusBar().showMessage(window.statusBar().currentMessage() + " — " + notice if after_history else notice)
|
|
397
|
+
|
|
398
|
+
def reveal_rebase_result(self, session_id: str) -> None:
|
|
399
|
+
"""Select the commit a finished rebase ended on once the history read after it is shown, then close its Rebase window."""
|
|
400
|
+
final_oid = self.rebase_final_oid_by_session.pop(session_id, None)
|
|
401
|
+
if final_oid is None:
|
|
402
|
+
return
|
|
403
|
+
self.session_registry_controller.window_by_session[session_id].reveal_commit(final_oid)
|
|
404
|
+
dialog = self.rebase_dialog_by_session.get(session_id)
|
|
405
|
+
if dialog is not None and isValid(dialog):
|
|
406
|
+
dialog.close()
|
|
407
|
+
|
|
408
|
+
def forget_rebase_end_notice(self, session_id: str) -> None:
|
|
409
|
+
"""Drop the line owed about a rebase that ended outside GitPane when the refresh that was to complete it failed."""
|
|
410
|
+
self.rebase_end_notice_session_set.discard(session_id)
|
|
411
|
+
|
|
412
|
+
def rebase_actions_changed(self) -> None:
|
|
413
|
+
"""Apply the shared writer availability to every existing tracking window."""
|
|
414
|
+
for session_id, dialog in tuple(self.rebase_dialog_by_session.items()):
|
|
415
|
+
session = self.session_registry_controller.session_by_id.get(session_id)
|
|
416
|
+
if session is None or session.is_closed or not isValid(dialog):
|
|
417
|
+
continue
|
|
418
|
+
operation = OperationIntent.capture("rebase-availability", session.repository_context, self.repository.git_filename,
|
|
419
|
+
action=OperationAction.REBASE_ABORT)
|
|
420
|
+
dialog.set_busy(bool(self.repository_operation.availability_diagnostic(operation)) or self.mergetool.owns_repository(operation.common_git_dir))
|
|
421
|
+
|
|
422
|
+
def rebase_finished(self, outcome: OperationOutcome) -> None:
|
|
423
|
+
"""Keep refusals visible and reveal successful HEAD only after graph publication.
|
|
424
|
+
|
|
425
|
+
Instructions Git refused reopen the editor once the result is shown; a list corrected after a refused Continue
|
|
426
|
+
continues the rebase.
|
|
427
|
+
"""
|
|
428
|
+
self.rebase_editor_controller.forget(outcome.operation_id)
|
|
429
|
+
validation = self.validation_by_operation_id.pop(outcome.operation_id, None)
|
|
430
|
+
continue_after_correction = outcome.operation_id in self.continue_after_correction_operation_id_set
|
|
431
|
+
self.continue_after_correction_operation_id_set.discard(outcome.operation_id)
|
|
432
|
+
intent = self.rebase_intent_by_id.pop(outcome.operation_id, None)
|
|
433
|
+
if intent is None:
|
|
434
|
+
return
|
|
435
|
+
operation = intent.operation
|
|
436
|
+
self.rebase.outcome_by_id[outcome.operation_id] = outcome
|
|
437
|
+
if outcome.status == OperationStatus.UNKNOWN:
|
|
438
|
+
# Only a command that could not read its own result asks a pending close to reread; observation failures never do.
|
|
439
|
+
self.session_registry_controller.close_reread_count_by_key[operation.key] = 0
|
|
440
|
+
else:
|
|
441
|
+
self.session_registry_controller.close_reread_count_by_key.pop(operation.key, None)
|
|
442
|
+
state = self.rebase.state_by_key.get(operation.key)
|
|
443
|
+
publish_after_trace(lambda: self._publish_rebase_console(outcome, operation, state), self)
|
|
444
|
+
if outcome.status in COMPLETED_STATUS_SET:
|
|
445
|
+
self.console_registry_controller.settle_paused_consoles(operation.key)
|
|
446
|
+
shared_by_key: dict[WorktreeKey, FullRefreshRead] = {}
|
|
447
|
+
for session_id, session in tuple(self.session_registry_controller.session_by_id.items()):
|
|
448
|
+
if session.is_closed or WorktreeKey.from_context(session.repository_context) != operation.key:
|
|
449
|
+
continue
|
|
450
|
+
window = self.session_registry_controller.window_by_session[session_id]
|
|
451
|
+
dialog = self.rebase_dialog_by_session.get(session_id)
|
|
452
|
+
if state is not None:
|
|
453
|
+
window.show_rebase_state(state)
|
|
454
|
+
if dialog is not None and isValid(dialog):
|
|
455
|
+
dialog.set_busy(False)
|
|
456
|
+
dialog.show_state(state)
|
|
457
|
+
if outcome.diagnostic is not None:
|
|
458
|
+
dialog.show_command_result(outcome.status, outcome.diagnostic.cause)
|
|
459
|
+
if outcome.status == OperationStatus.ABORTED:
|
|
460
|
+
# Nothing is left to track once the rebase is abandoned; its console keeps the result.
|
|
461
|
+
dialog.close()
|
|
462
|
+
if outcome.status == OperationStatus.SUCCESS and state.head_oid:
|
|
463
|
+
self.rebase_final_oid_by_session[session_id] = state.head_oid
|
|
464
|
+
if session_id == operation.session_id:
|
|
465
|
+
self.history_read_controller.history_top_session_set.add(session_id)
|
|
466
|
+
session.filter_text = ""
|
|
467
|
+
with QSignalBlocker(window.filter_edit):
|
|
468
|
+
window.filter_edit.clear()
|
|
469
|
+
self.refresh_controller.refresh(session_id, full_refresh_read=self.refresh_controller.full_refresh_read_for(session, shared_by_key, explicit=True))
|
|
470
|
+
shared = shared_by_key.get(operation.key)
|
|
471
|
+
if shared is not None:
|
|
472
|
+
shared.rebase_publication = self._rebase_finish_publication(operation.key)
|
|
473
|
+
refreshed_key_set = self.refresh_controller.observe_full_refresh_pauses(shared_by_key)
|
|
474
|
+
self.rebase_actions_changed()
|
|
475
|
+
staging = self.staging_session_registry_controller.staging_by_key.get(operation.key)
|
|
476
|
+
# A preparation session without any open repository window still shows the result.
|
|
477
|
+
if staging is not None and operation.key not in refreshed_key_set:
|
|
478
|
+
staging.refresh(explicit=True)
|
|
479
|
+
self._follow_instruction_correction(outcome, operation, validation, continue_after_correction)
|
|
480
|
+
|
|
481
|
+
def _follow_instruction_correction(self, outcome: OperationOutcome, operation: OperationIntent, validation: EditorValidation | None, continue_after_correction: bool) -> None:
|
|
482
|
+
"""Reopen the editor on instructions Git refused, or continue the rebase once Git accepted a list a refused Continue had to correct.
|
|
483
|
+
|
|
484
|
+
Each step starts only from a result of Git that answers a click of the user in an editor window, so the loop
|
|
485
|
+
turns at the user's pace; Abort in the editor ends it with the whole rebase. Nothing is chained while the
|
|
486
|
+
repository window is closing or changing repository, or while GitPane is quitting: the rebase stays paused.
|
|
487
|
+
"""
|
|
488
|
+
session = self.session_registry_controller.session_by_id.get(operation.session_id)
|
|
489
|
+
if session is None or session.is_closed:
|
|
490
|
+
return
|
|
491
|
+
if self.replay_controller.replay_session_leaving(operation.session_id):
|
|
492
|
+
# The rebase is paused, a stable state where the close, the quit or the repository change can end; the error
|
|
493
|
+
# stays in the console and the Rebase window lets the user go on later.
|
|
494
|
+
return
|
|
495
|
+
instruction_error = outcome_instruction_error(outcome)
|
|
496
|
+
if instruction_error and validation is not None:
|
|
497
|
+
self.reopen_rebase_todo(operation.session_id, validation, instruction_error)
|
|
498
|
+
return
|
|
499
|
+
if not continue_after_correction or outcome.status != OperationStatus.PAUSED:
|
|
500
|
+
return
|
|
501
|
+
state = self.rebase.state_by_key.get(operation.key)
|
|
502
|
+
if state is not None and state.fresh and state.identity is not None:
|
|
503
|
+
self.continue_rebase(operation.session_id, state)
|
|
504
|
+
|
|
505
|
+
def _publish_rebase_console(self, outcome: OperationOutcome, operation: OperationIntent, state: RebaseSnapshot | None) -> None:
|
|
506
|
+
"""Write the result of a rebase in its console, then settle what depends on how that console ended.
|
|
507
|
+
|
|
508
|
+
A console that closed on an expected pause hands the user over to the Rebase window; a console that stays open
|
|
509
|
+
on a failure while the rebase still waits for the user is kept until the rebase ends.
|
|
510
|
+
"""
|
|
511
|
+
output = self.console_registry_controller.rebase_output_by_id.get(outcome.operation_id)
|
|
512
|
+
expected_stop = False
|
|
513
|
+
if output is not None and isValid(output):
|
|
514
|
+
if outcome.diagnostic is not None:
|
|
515
|
+
output.trace.append_diagnostic(outcome.operation_id, "status", outcome.diagnostic, outcome.status)
|
|
516
|
+
failure_reported = outcome.diagnostic is not None
|
|
517
|
+
output.trace.append_end_line(outcome.operation_id, "status", rebase_result_log_text(outcome, state), rebase_result_line(outcome, state), outcome.status,
|
|
518
|
+
failure_reported=failure_reported)
|
|
519
|
+
output.show_result(outcome.status, failure_reported=failure_reported)
|
|
520
|
+
expected_stop = outcome.status == OperationStatus.PAUSED and output.completed
|
|
521
|
+
if outcome.status not in COMPLETED_STATUS_SET and state is not None and state.fresh and state.identity is not None and not expected_stop:
|
|
522
|
+
# A step Git refused while the rebase still waits for the user keeps its console open on the failure; that
|
|
523
|
+
# console is settled once the rebase ends.
|
|
524
|
+
self.console_registry_controller.paused_console_ids_by_key.setdefault(operation.key, []).append(outcome.operation_id)
|
|
525
|
+
if expected_stop and not outcome_instruction_error(outcome) and (output is None or not isValid(output) or not output.isVisible()):
|
|
526
|
+
# The console closed on the pause: what waits for the user is the Rebase window, so it comes to the front.
|
|
527
|
+
# Instructions Git refused reopen the editor instead, which must stay in front.
|
|
528
|
+
self._bring_rebase_dialog_forward(operation.session_id)
|
|
529
|
+
|
|
530
|
+
def _bring_rebase_dialog_forward(self, session_id: str) -> None:
|
|
531
|
+
"""Show the Rebase window of a session in front and active, opening it as the Open rebase banner does when needed."""
|
|
532
|
+
session = self.session_registry_controller.session_by_id.get(session_id)
|
|
533
|
+
if session is None or session.is_closed:
|
|
534
|
+
return
|
|
535
|
+
dialog = self.rebase_dialog_by_session.get(session_id)
|
|
536
|
+
if dialog is None or not isValid(dialog) or not dialog.isVisible():
|
|
537
|
+
dialog = self.open_rebase_dialog(session_id)
|
|
538
|
+
if dialog is None or not isValid(dialog):
|
|
539
|
+
return
|
|
540
|
+
dialog.raise_()
|
|
541
|
+
dialog.activateWindow()
|
|
542
|
+
|
|
543
|
+
def observe_rebase(self, session_id: str, *, open_paused: bool = False, automatic: bool = False,
|
|
544
|
+
replay_publication: ReplayRefreshPublication | None = None, rebase_publication: RebaseRefreshPublication | None = None) -> None:
|
|
545
|
+
"""Observe one worktree without allowing a late response to reach a replaced session.
|
|
546
|
+
|
|
547
|
+
``replay_publication`` is the state a Cherry-pick or Revert just published when it ended, shown in place of a
|
|
548
|
+
replay read while it is still current. ``rebase_publication`` is the state a rebase command just published: it
|
|
549
|
+
is shown in place of a rebase read once its native files are found unchanged, and read through Git otherwise.
|
|
550
|
+
"""
|
|
551
|
+
self.replay_controller.observe_replay(session_id, automatic=automatic, publication=replay_publication)
|
|
552
|
+
if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
|
|
553
|
+
return
|
|
554
|
+
session = self.session_registry_controller.session_by_id[session_id]
|
|
555
|
+
context = session.repository_context
|
|
556
|
+
if context.is_bare:
|
|
557
|
+
return
|
|
558
|
+
identity = self.session_registry_controller.identity_by_session[session_id]
|
|
559
|
+
key = WorktreeKey.from_context(context)
|
|
560
|
+
# Checked before the delay below on purpose: returning here stamps no moment, so the first observation asked
|
|
561
|
+
# for once the worktree is free is not held back by a timestamp it never wrote.
|
|
562
|
+
if automatic and self.repository_operation.owns_worktree(key):
|
|
563
|
+
return
|
|
564
|
+
now = time.monotonic()
|
|
565
|
+
if automatic and (now - self.rebase_observed_at_by_key.get(key, 0)) * 1000 < REBASE_REFRESH_INTERVAL_MS:
|
|
566
|
+
return
|
|
567
|
+
self.rebase_observed_at_by_key[key] = now
|
|
568
|
+
existing_dialog = self.rebase_dialog_by_session.get(session_id)
|
|
569
|
+
if existing_dialog is not None and isValid(existing_dialog):
|
|
570
|
+
existing_dialog.refresh_button.setEnabled(False)
|
|
571
|
+
recipients = []
|
|
572
|
+
for target_id, target_session in self.session_registry_controller.session_by_id.items():
|
|
573
|
+
if (not target_session.is_closed and WindowCapability.MANAGE in target_session.capabilities
|
|
574
|
+
and WorktreeKey.from_context(target_session.repository_context) == key):
|
|
575
|
+
recipients.append((target_id, target_session, self.session_registry_controller.identity_by_session.get(target_id)))
|
|
576
|
+
|
|
577
|
+
def current() -> bool:
|
|
578
|
+
"""Require both the original request and its independent worktree to remain current."""
|
|
579
|
+
return (self.session_registry_controller.identity_by_session.get(session_id) == identity and self.session_registry_controller.session_by_id.get(session_id) is session
|
|
580
|
+
and not session.is_closed and WorktreeKey.from_context(session.repository_context) == key)
|
|
581
|
+
|
|
582
|
+
def complete(state: RebaseSnapshot) -> None:
|
|
583
|
+
"""Publish to the captured current views sharing this exact worktree."""
|
|
584
|
+
self.repository_operation.register_context(key, context)
|
|
585
|
+
for target_id, target_session, target_identity in recipients:
|
|
586
|
+
window = self.session_registry_controller.window_by_session.get(target_id)
|
|
587
|
+
if (self.session_registry_controller.session_by_id.get(target_id) is not target_session or self.session_registry_controller.identity_by_session.get(target_id) != target_identity
|
|
588
|
+
or target_session.is_closed or window is None or not isValid(window) or window.is_closed):
|
|
589
|
+
continue
|
|
590
|
+
window.show_rebase_state(state)
|
|
591
|
+
dialog = self.rebase_dialog_by_session.get(target_id)
|
|
592
|
+
self.rebase_state_shown.emit(target_id, state)
|
|
593
|
+
if dialog is not None and isValid(dialog):
|
|
594
|
+
dialog.show_state(state)
|
|
595
|
+
dialog.refresh_button.setEnabled(True)
|
|
596
|
+
elif open_paused and state.state != "absent":
|
|
597
|
+
self.open_rebase_dialog(target_id)
|
|
598
|
+
|
|
599
|
+
if rebase_publication is not None and not automatic:
|
|
600
|
+
self._show_rebase_publication(key, context, rebase_publication, current, complete)
|
|
601
|
+
return
|
|
602
|
+
self.worktree_state_controller.mark_pause_observed(key, REBASE_PAUSE_OBSERVATION)
|
|
603
|
+
# An automatic look waits behind every request somebody is waiting for; a click reads as a user action.
|
|
604
|
+
priority = ProcessPriority.BACKGROUND if automatic else ProcessPriority.USER
|
|
605
|
+
source = self.replay_controller.passive_observation_source(key) if automatic else None
|
|
606
|
+
if source is None:
|
|
607
|
+
self.rebase.observe(context, self.repository.git_filename, is_current=current, callback=complete, priority=priority)
|
|
608
|
+
return
|
|
609
|
+
self.rebase.observe_passive(context, self.repository.git_filename, source, is_current=current, callback=complete, priority=priority)
|
|
610
|
+
|
|
611
|
+
def _rebase_finish_publication(self, key: WorktreeKey) -> RebaseRefreshPublication | None:
|
|
612
|
+
"""Answer the state the rebase command that just ended published for this worktree, while nothing newer replaced it.
|
|
613
|
+
|
|
614
|
+
Only a fresh state that still carries the native files its capture proved stable is offered: a refusal, an
|
|
615
|
+
error or a message of the command's own leaves nothing to show without reading again. A read that started before
|
|
616
|
+
the publication, such as the one an editor wait asked for, never replaces it, so it does not stand in the way.
|
|
617
|
+
"""
|
|
618
|
+
state = self.rebase.state_by_key.get(key)
|
|
619
|
+
if (state is None or not state.fresh or state.native_observations is None or state.generation != self.rebase.published_generation_by_key.get(key)
|
|
620
|
+
or state.generation != self.rebase.generation_by_key.get(key)):
|
|
621
|
+
return None
|
|
622
|
+
return RebaseRefreshPublication(state, self.repository.git_filename)
|
|
623
|
+
|
|
624
|
+
def _rebase_publication_current(self, key: WorktreeKey, publication: RebaseRefreshPublication) -> bool:
|
|
625
|
+
"""Tell whether a rebase finish publication may still be shown: the latest state of this worktree, same Git, and no read started since.
|
|
626
|
+
|
|
627
|
+
Every read started after the publication takes a newer generation; one started before it cannot replace it.
|
|
628
|
+
"""
|
|
629
|
+
state = publication.state
|
|
630
|
+
return (state.key == key and publication.git_filename == self.repository.git_filename and self.rebase.state_by_key.get(key) is state
|
|
631
|
+
and self.rebase.generation_by_key.get(key) == state.generation)
|
|
632
|
+
|
|
633
|
+
def _show_rebase_publication(self, key: WorktreeKey, context: RepositoryContext, publication: RebaseRefreshPublication, current: Callable[[], bool],
|
|
634
|
+
complete: Callable[[RebaseSnapshot], None]) -> None:
|
|
635
|
+
"""Show a rebase command's published state after proving, on a worker, that its native files are still the captured ones.
|
|
636
|
+
|
|
637
|
+
The pause is acknowledged only once that proof holds, so the periodic refresh still looks at anything that moves
|
|
638
|
+
afterwards. A publication that stopped being current, pause paths not known yet, a changed or unreadable native
|
|
639
|
+
file: each reads the rebase state through Git instead.
|
|
640
|
+
"""
|
|
641
|
+
git_filename = self.repository.git_filename
|
|
642
|
+
staging = self.staging_session_registry_controller.staging_by_key.get(key)
|
|
643
|
+
filename_by_git_path = staging.pause_probe.filename_by_git_path() if staging is not None else None
|
|
644
|
+
|
|
645
|
+
def read_through_git() -> None:
|
|
646
|
+
"""Observe the rebase as any refresh does."""
|
|
647
|
+
self.worktree_state_controller.mark_pause_observed(key, REBASE_PAUSE_OBSERVATION)
|
|
648
|
+
self.rebase.observe(context, git_filename, is_current=current, callback=complete)
|
|
649
|
+
|
|
650
|
+
if filename_by_git_path is None or not self._rebase_publication_current(key, publication):
|
|
651
|
+
read_through_git()
|
|
652
|
+
return
|
|
653
|
+
|
|
654
|
+
def prove() -> Generator[FileTask, object, bool]:
|
|
655
|
+
"""Compare the native rebase files with the ones the command's capture proved, off the Qt thread."""
|
|
656
|
+
state_dirs = state_dirs_from_pause_paths(key, filename_by_git_path)
|
|
657
|
+
unchanged = yield FileTask(lambda: native_observations_unchanged(state_dirs, publication.state.native_observations))
|
|
658
|
+
return unchanged
|
|
659
|
+
|
|
660
|
+
def proven(unchanged: bool | None, error: Exception | None) -> None:
|
|
661
|
+
"""Show the publication only if it is still the latest state and its files did not move; otherwise read."""
|
|
662
|
+
if not current():
|
|
663
|
+
return
|
|
664
|
+
if error is None and unchanged and self._rebase_publication_current(key, publication) and git_filename == self.repository.git_filename:
|
|
665
|
+
self.worktree_state_controller.mark_pause_observed(key, REBASE_PAUSE_OBSERVATION)
|
|
666
|
+
complete(publication.state)
|
|
667
|
+
return
|
|
668
|
+
read_through_git()
|
|
669
|
+
|
|
670
|
+
self.worktree_service.run(context, git_filename, prove(), proven)
|
|
671
|
+
|
|
672
|
+
def open_rebase_dialog(self, session_id: str) -> RebaseDialog | None:
|
|
673
|
+
"""Show the shared observation without resuming or modifying the native operation."""
|
|
674
|
+
if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
|
|
675
|
+
return None
|
|
676
|
+
window = self.session_registry_controller.window_by_session[session_id]
|
|
677
|
+
state = self.rebase.state_by_key.get(WorktreeKey.from_context(window.context))
|
|
678
|
+
if window.context.is_bare or state is None or state.state == "absent":
|
|
679
|
+
return None
|
|
680
|
+
dialog = self.rebase_dialog_by_session.get(session_id)
|
|
681
|
+
if dialog is None or not isValid(dialog):
|
|
682
|
+
dialog = self._create_rebase_dialog(session_id, state)
|
|
683
|
+
dialog.show_state(state)
|
|
684
|
+
dialog.show()
|
|
685
|
+
dialog.raise_()
|
|
686
|
+
return dialog
|
|
687
|
+
|
|
688
|
+
def _create_rebase_dialog(self, session_id: str, state: RebaseSnapshot) -> RebaseDialog:
|
|
689
|
+
"""Create the single tracking window wiring so launch and reopen behave identically, releasing the window it replaces."""
|
|
690
|
+
previous = self.rebase_dialog_by_session.get(session_id)
|
|
691
|
+
if previous is not None and isValid(previous):
|
|
692
|
+
# A finished rebase leaves its tracking window parented to the repository window; a new launch replaces it rather than stacking another one.
|
|
693
|
+
previous.close()
|
|
694
|
+
previous.deleteLater()
|
|
695
|
+
dialog = RebaseDialog(state, self.session_registry_controller.window_by_session[session_id])
|
|
696
|
+
dialog.installEventFilter(self.window_activation_controller)
|
|
697
|
+
requested_oid_set: set[str] = set()
|
|
698
|
+
|
|
699
|
+
def read_commit_details(steps: tuple[RebaseStep, ...]) -> None:
|
|
700
|
+
"""Load each original commit once without delaying or replacing native rebase observations."""
|
|
701
|
+
missing_steps = []
|
|
702
|
+
for step in steps:
|
|
703
|
+
if step.oid and step.oid not in requested_oid_set:
|
|
704
|
+
requested_oid_set.add(step.oid)
|
|
705
|
+
missing_steps.append(step)
|
|
706
|
+
if not missing_steps:
|
|
707
|
+
return
|
|
708
|
+
|
|
709
|
+
def complete(commits: tuple[RebaseCommit, ...] | None, error: Exception | None) -> None:
|
|
710
|
+
"""Discard late display results when their tracking window has been replaced or closed."""
|
|
711
|
+
if isValid(dialog) and self.rebase_dialog_by_session.get(session_id) is dialog and error is None and commits:
|
|
712
|
+
dialog.show_commits(commits)
|
|
713
|
+
|
|
714
|
+
self.worktree_service.run(self.session_registry_controller.window_by_session[session_id].context, self.repository.git_filename, read_rebase_commits(tuple(missing_steps)), complete)
|
|
715
|
+
|
|
716
|
+
dialog.commit_details_requested.connect(read_commit_details)
|
|
717
|
+
# The window showed its first state before this connection existed, and an equal state keeps its rows without asking again.
|
|
718
|
+
dialog.request_commit_details()
|
|
719
|
+
dialog.refresh_requested.connect(requested_oid_set.clear)
|
|
720
|
+
dialog.refresh_requested.connect(lambda: self.observe_rebase(session_id))
|
|
721
|
+
dialog.edit_todo_requested.connect(lambda: self.edit_rebase_todo(session_id))
|
|
722
|
+
dialog.amend_requested.connect(lambda state: self.amend_rebase_commit(session_id, state))
|
|
723
|
+
dialog.continue_requested.connect(lambda state: self.continue_rebase(session_id, state))
|
|
724
|
+
dialog.abort_requested.connect(lambda state: self.abort_rebase(session_id, state))
|
|
725
|
+
dialog.solve_conflicts_requested.connect(lambda: self.conflict_list_requested.emit(session_id))
|
|
726
|
+
self.rebase_dialog_by_session[session_id] = dialog
|
|
727
|
+
dialog.show_state(state)
|
|
728
|
+
# No periodic recomputation comes after this one: a window created while another operation owns the
|
|
729
|
+
# repository has to show it at once.
|
|
730
|
+
self.rebase_actions_changed()
|
|
731
|
+
return dialog
|
|
732
|
+
|
|
733
|
+
def amend_rebase_commit(self, session_id: str, state: RebaseSnapshot) -> None:
|
|
734
|
+
"""Open the existing staging view for one explicitly selected native edit pause."""
|
|
735
|
+
if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
|
|
736
|
+
return
|
|
737
|
+
context = self.session_registry_controller.session_by_id[session_id].repository_context
|
|
738
|
+
if state.key != WorktreeKey.from_context(context) or state.state != "edit" or not state.fresh:
|
|
739
|
+
return
|
|
740
|
+
staging = self.staging_session_registry_controller.staging_session(context)
|
|
741
|
+
if staging.action_diagnostic(OperationAction.AMEND):
|
|
742
|
+
self.observe_rebase(session_id)
|
|
743
|
+
return
|
|
744
|
+
if self.staging_controller.open_staging(session_id) is not None:
|
|
745
|
+
staging.request_rebase_amend(state)
|
|
746
|
+
|
|
747
|
+
def abort_rebase(self, session_id: str, state: RebaseSnapshot) -> None:
|
|
748
|
+
"""Submit an immediate native Abort independently of the launch confirmation."""
|
|
749
|
+
if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
|
|
750
|
+
return
|
|
751
|
+
session = self.session_registry_controller.session_by_id[session_id]
|
|
752
|
+
if state.key != WorktreeKey.from_context(session.repository_context) or state.identity is None:
|
|
753
|
+
return
|
|
754
|
+
for active in self.repository_operation.active_by_id.values():
|
|
755
|
+
if active.action == OperationAction.REBASE_ABORT and active.key == state.key:
|
|
756
|
+
return
|
|
757
|
+
operation = OperationIntent.capture(uuid.uuid4().hex, session.repository_context, self.repository.git_filename,
|
|
758
|
+
session_id=session_id, generation=self.session_registry_controller.generation_by_session[session_id], operation="Abort rebase",
|
|
759
|
+
action=OperationAction.REBASE_ABORT)
|
|
760
|
+
intent = RebaseIntent(operation, state.identity.onto_oid, state.head_oid, state.identity.head_name)
|
|
761
|
+
if self.mergetool.owns_repository(operation.common_git_dir):
|
|
762
|
+
self._rebase_refused(intent, ValueError("Wait for the merge tool and its series to finish before aborting the rebase."))
|
|
763
|
+
return
|
|
764
|
+
output = self._create_rebase_output(intent)
|
|
765
|
+
output.set_running()
|
|
766
|
+
self.rebase_progress(operation.operation_id, "Aborting rebase…")
|
|
767
|
+
try:
|
|
768
|
+
self.rebase.abort(operation, state, is_current=lambda: self.operation_policy_controller.operation_allowed(operation)
|
|
769
|
+
and not self.mergetool.owns_repository(operation.common_git_dir))
|
|
770
|
+
except (ValueError, OSError) as error:
|
|
771
|
+
diagnostic = Diagnostic("Abort rebase", operation.context.repository_dir, operation.git_filename, [], str(error)).prepare()
|
|
772
|
+
self.rebase_finished(OperationOutcome(operation.operation_id, OperationStatus.REFUSED, diagnostic=diagnostic))
|
|
773
|
+
|
|
774
|
+
def continue_rebase(self, session_id: str, state: RebaseSnapshot) -> None:
|
|
775
|
+
"""Reserve a distinct explicit Continue and route its native result to the tracking view."""
|
|
776
|
+
if not self.operation_policy_controller.allows(session_id, WindowCapability.MANAGE):
|
|
777
|
+
return
|
|
778
|
+
session = self.session_registry_controller.session_by_id[session_id]
|
|
779
|
+
if state.key != WorktreeKey.from_context(session.repository_context) or state.identity is None:
|
|
780
|
+
return
|
|
781
|
+
operation = OperationIntent.capture(uuid.uuid4().hex, session.repository_context, self.repository.git_filename,
|
|
782
|
+
session_id=session_id, generation=self.session_registry_controller.generation_by_session[session_id], operation="Continue rebase",
|
|
783
|
+
action=OperationAction.REBASE_CONTINUE)
|
|
784
|
+
intent = RebaseIntent(operation, state.identity.onto_oid, state.head_oid, state.identity.head_name)
|
|
785
|
+
if self.mergetool.owns_repository(operation.common_git_dir):
|
|
786
|
+
self._rebase_refused(intent, ValueError("Wait for the merge tool and its series to finish before continuing the rebase."))
|
|
787
|
+
return
|
|
788
|
+
output = self._create_rebase_output(intent)
|
|
789
|
+
output.set_running()
|
|
790
|
+
self.rebase_progress(operation.operation_id, SharedText.REBASE_APPLYING_COMMITS.value)
|
|
791
|
+
self._expect_edits(operation, EditorValidation.CONTINUE)
|
|
792
|
+
try:
|
|
793
|
+
self.rebase.continue_rebase(operation, state,
|
|
794
|
+
is_current=lambda: self.operation_policy_controller.operation_allowed(operation) and not self.mergetool.owns_repository(operation.common_git_dir))
|
|
795
|
+
except (ValueError, OSError) as error:
|
|
796
|
+
diagnostic = Diagnostic("Continue rebase", operation.context.repository_dir, operation.git_filename, [], str(error)).prepare()
|
|
797
|
+
self.rebase_finished(OperationOutcome(operation.operation_id, OperationStatus.REFUSED, diagnostic=diagnostic))
|