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,671 @@
|
|
|
1
|
+
"""Read from Git what a repository window shows of its history."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import replace
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from PySide6.QtCore import QObject, QSignalBlocker, QTimer, Signal
|
|
9
|
+
from shiboken6 import isValid
|
|
10
|
+
|
|
11
|
+
from gitpane.config import SharedText
|
|
12
|
+
from gitpane.controller.session_registry import keep_streamed_commits
|
|
13
|
+
from gitpane.history.file_history import FileHistoryEngine, WindowCapability, visible_parent_map
|
|
14
|
+
from gitpane.history.history_filter import HistoryFilterMode
|
|
15
|
+
from gitpane.history.history_parse import summarize_commit
|
|
16
|
+
from gitpane.history.repository_value import FilePanel, FileReadIdentity, RequestIdentity
|
|
17
|
+
from gitpane.operation.diagnostic import Diagnostic
|
|
18
|
+
from gitpane.operation.operation_status import OperationStatus
|
|
19
|
+
from gitpane.text.text_query import TextQuery
|
|
20
|
+
from gitpane.transport.refusal_notice import failure_summary
|
|
21
|
+
from gitpane.ui.console.error_dialog import ErrorDialog
|
|
22
|
+
from gitpane.ui.welcome.welcome_window import WelcomeWindow
|
|
23
|
+
from gitpane.ui.window_control.dialog_modality import open_window_modal
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from gitpane.controller.operation_policy import OperationPolicy
|
|
27
|
+
from gitpane.controller.reference_read_controller import ReferenceReadController
|
|
28
|
+
from gitpane.controller.refresh_controller import RefreshController
|
|
29
|
+
from gitpane.controller.session_registry import RepositorySession, SessionRegistry
|
|
30
|
+
from gitpane.history.file_history import FileHistoryResult
|
|
31
|
+
from gitpane.history.repository import GitRepository
|
|
32
|
+
from gitpane.history.repository_value import BlobContent, Commit, CommitRelation, ComparisonGroup, HistorySnapshot, RepositoryFailure, RevisionEntry, TreeEntry
|
|
33
|
+
from gitpane.operation.diagnostic import DiagnosticLog
|
|
34
|
+
from gitpane.preference.preference_store import PreferenceStore
|
|
35
|
+
from gitpane.tool.system_tool import SystemToolService
|
|
36
|
+
from gitpane.transport.clone import CloneService
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class HistoryReadController(QObject):
|
|
40
|
+
"""The history reads of every repository window: the graph and its search, the selected commit with its relatives, its files, its tree and the diff of one file."""
|
|
41
|
+
|
|
42
|
+
# session id, generation of the history just shown in the window
|
|
43
|
+
history_published = Signal(str, int)
|
|
44
|
+
# session id, reference capture the refresh read (or None), once the published history wrote its status line
|
|
45
|
+
history_status_written = Signal(str, object)
|
|
46
|
+
# session id, prepared diagnostic of the read that ended a refresh
|
|
47
|
+
refresh_failed = Signal(str, object)
|
|
48
|
+
|
|
49
|
+
def __init__(self, *, session_registry_controller: SessionRegistry, operation_policy_controller: OperationPolicy, reference_read_controller: ReferenceReadController, refresh_controller: RefreshController, preference: PreferenceStore,
|
|
50
|
+
repository: GitRepository, clone: CloneService, diagnostic_log: DiagnosticLog | None, system_tool: SystemToolService, parent: QObject | None = None) -> None:
|
|
51
|
+
"""Receive the services and the lower controllers the reads go through; the wiring builds it after the worktree state controller, and the repository service, the windows and the operations ask it to read."""
|
|
52
|
+
super().__init__(parent)
|
|
53
|
+
self.session_registry_controller = session_registry_controller
|
|
54
|
+
self.repository = repository
|
|
55
|
+
self.operation_policy_controller = operation_policy_controller
|
|
56
|
+
self.preference = preference
|
|
57
|
+
self.diagnostic_log = diagnostic_log
|
|
58
|
+
self.system_tool = system_tool
|
|
59
|
+
self.reference_read_controller = reference_read_controller
|
|
60
|
+
self.refresh_controller = refresh_controller
|
|
61
|
+
self.clone = clone
|
|
62
|
+
# Windows whose graph goes back to its top once the history read after their operation is shown: an operation
|
|
63
|
+
# that adds commits or moves a branch to a new commit puts its result at the top of the graph. An operation that
|
|
64
|
+
# names a commit the user clicked, a tag or a reset of a branch, leaves the graph where it was.
|
|
65
|
+
self.history_top_session_set: set[str] = set()
|
|
66
|
+
self.reported_request_set: set[RequestIdentity] = set()
|
|
67
|
+
|
|
68
|
+
def read_selection(self, session_id: str, oid: str) -> None:
|
|
69
|
+
"""Read full text separately from lightweight table rows and traversal pages."""
|
|
70
|
+
session = self.session_registry_controller.session_by_id.get(session_id)
|
|
71
|
+
if session is None:
|
|
72
|
+
return
|
|
73
|
+
if session.mode == "file_history" and (session.search_pending or oid not in session.visible_oid_set):
|
|
74
|
+
return
|
|
75
|
+
identity = RequestIdentity(session_id, session.generation, session.selection_generation, oid)
|
|
76
|
+
window = self.session_registry_controller.window_by_session[session_id]
|
|
77
|
+
window.commit_panel.show_loading(oid, window.commit.short_oid if window.commit is not None and window.commit.oid == oid else "")
|
|
78
|
+
if session.refresh_active and session.view_anchor is not None:
|
|
79
|
+
session.view_anchor = replace(session.view_anchor, selected_oid=oid, selected_visible_index=window.commit_list.currentIndex().row())
|
|
80
|
+
if session.navigation_oid == oid:
|
|
81
|
+
self.repository.read_commit(oid, identity, verify_exists=True)
|
|
82
|
+
else:
|
|
83
|
+
self.repository.read_commit(oid, identity)
|
|
84
|
+
|
|
85
|
+
def file_is_current(self, identity: FileReadIdentity) -> bool:
|
|
86
|
+
"""Reject replies whose window, commit, selection, or panel has changed."""
|
|
87
|
+
request = identity.request_identity
|
|
88
|
+
session = self.session_registry_controller.session_by_id.get(request.session_id)
|
|
89
|
+
return (session is not None and not session.is_closed and session.generation == request.generation
|
|
90
|
+
and session.selection_generation == request.selection_generation and session.selected_oid == request.oid
|
|
91
|
+
and session.file_identity_by_kind.get(identity.panel_kind) == identity)
|
|
92
|
+
|
|
93
|
+
def read_diff(self, session_id: str, entry: RevisionEntry) -> None:
|
|
94
|
+
"""Start a new comparison only for an entry in the current commit's file list."""
|
|
95
|
+
session = self.session_registry_controller.session_by_id.get(session_id)
|
|
96
|
+
if session is None or entry.commit_oid != session.selected_oid or not any(entry in group.entries for group in session.diff_groups):
|
|
97
|
+
return
|
|
98
|
+
preserve_selection = session.selected_entry == entry
|
|
99
|
+
session.panel_generation += 1
|
|
100
|
+
session.selected_entry = entry
|
|
101
|
+
request = RequestIdentity(session_id, session.generation, session.selection_generation, session.selected_oid)
|
|
102
|
+
panel = self.session_registry_controller.window_by_session[session_id].diff_panel
|
|
103
|
+
identity = FileReadIdentity(request, FilePanel.DIFF, session.panel_generation, entry.parent_oid, entry.old_path, entry.new_path, panel.full_context_checkbox.isChecked())
|
|
104
|
+
session.file_identity_by_kind[FilePanel.DIFF] = identity
|
|
105
|
+
panel.selected_entry = entry
|
|
106
|
+
panel.full_context_checkbox.setEnabled(True)
|
|
107
|
+
panel.diff_view.clear(preserve_selection=preserve_selection)
|
|
108
|
+
panel.reader_toolbar.start_loading("Loading diff…")
|
|
109
|
+
self.repository.read_diff(entry, identity)
|
|
110
|
+
|
|
111
|
+
def change_diff_context(self, session_id: str, checked: bool) -> None:
|
|
112
|
+
"""Reload only the selected comparison while keeping graph and commit detail intact."""
|
|
113
|
+
session = self.session_registry_controller.session_by_id.get(session_id)
|
|
114
|
+
if session is not None and session.selected_entry is not None:
|
|
115
|
+
self.read_diff(session_id, session.selected_entry)
|
|
116
|
+
|
|
117
|
+
def diff_read(self, identity: FileReadIdentity, entry: RevisionEntry, patch: bytes) -> None:
|
|
118
|
+
"""Publish patch bytes only for the selected entry and panel generation."""
|
|
119
|
+
if not self.file_is_current(identity):
|
|
120
|
+
return
|
|
121
|
+
session_id = identity.request_identity.session_id
|
|
122
|
+
if self.session_registry_controller.session_by_id[session_id].selected_entry != entry:
|
|
123
|
+
return
|
|
124
|
+
panel = self.session_registry_controller.window_by_session[session_id].diff_panel
|
|
125
|
+
try:
|
|
126
|
+
panel.show_patch(entry, patch)
|
|
127
|
+
except ValueError as error:
|
|
128
|
+
panel.show_error(str(error))
|
|
129
|
+
|
|
130
|
+
def retry_diff(self, session_id: str) -> None:
|
|
131
|
+
"""Retry failed file or file-list reads without replacing commit details."""
|
|
132
|
+
session = self.session_registry_controller.session_by_id.get(session_id)
|
|
133
|
+
if session is None:
|
|
134
|
+
return
|
|
135
|
+
if session.selected_entry is not None:
|
|
136
|
+
self.read_diff(session_id, session.selected_entry)
|
|
137
|
+
return
|
|
138
|
+
session.invalidate_file_selection()
|
|
139
|
+
if session.selected_oid is not None:
|
|
140
|
+
self.read_selection(session_id, session.selected_oid)
|
|
141
|
+
|
|
142
|
+
def save_reading_preference(self, session_id: str, changed_value_by_key: dict[str, object]) -> None:
|
|
143
|
+
"""Publish only synchronized choices and restore the accepted view on refusal."""
|
|
144
|
+
window = self.session_registry_controller.window_by_session.get(session_id)
|
|
145
|
+
if window is None or window.is_closed:
|
|
146
|
+
return
|
|
147
|
+
try:
|
|
148
|
+
self.preference.update(changed_value_by_key)
|
|
149
|
+
except (OSError, ValueError, TypeError) as error:
|
|
150
|
+
window.apply_reading_preference(self.preference.value_by_key)
|
|
151
|
+
diagnostic = Diagnostic("Save reading preferences", window.context.repository_dir, "", [], str(error)).prepare()
|
|
152
|
+
window.statusBar().showMessage(diagnostic.cause)
|
|
153
|
+
if self.diagnostic_log is not None:
|
|
154
|
+
self.diagnostic_log.write(diagnostic)
|
|
155
|
+
|
|
156
|
+
def resume_file_history(self, session_id: str) -> None:
|
|
157
|
+
"""Resume only the current limited window's immutable file-history calculation."""
|
|
158
|
+
session = self.session_registry_controller.session_by_id.get(session_id)
|
|
159
|
+
if session is None or session.is_closed or session.file_history_engine is None:
|
|
160
|
+
return
|
|
161
|
+
self.session_registry_controller.window_by_session[session_id].file_history_resume_action.setEnabled(False)
|
|
162
|
+
session.file_history_engine.resume()
|
|
163
|
+
|
|
164
|
+
def change_filter(self, session_id: str, text: str) -> None:
|
|
165
|
+
"""Invalidate on every keystroke, before the delayed replacement scan starts."""
|
|
166
|
+
session = self.session_registry_controller.session_by_id.get(session_id)
|
|
167
|
+
if session is None:
|
|
168
|
+
return
|
|
169
|
+
session.search(text)
|
|
170
|
+
self.session_registry_controller.window_by_session[session_id].begin_search()
|
|
171
|
+
|
|
172
|
+
def read_navigation(self, session_id: str, oid: str) -> None:
|
|
173
|
+
"""Clear the previous row's active detail while verifying a requested relative."""
|
|
174
|
+
if not self.operation_policy_controller.allows(session_id, WindowCapability.CONSULT):
|
|
175
|
+
return
|
|
176
|
+
window = self.session_registry_controller.window_by_session[session_id]
|
|
177
|
+
session = self.session_registry_controller.session_by_id[session_id]
|
|
178
|
+
if session.mode == "file_history" and (session.search_pending or oid not in session.visible_oid_set):
|
|
179
|
+
return
|
|
180
|
+
if not window.search_pending and oid in window.history_model.row_by_oid and window.commit is not None and window.commit.oid != oid:
|
|
181
|
+
window.reveal_commit(oid)
|
|
182
|
+
return
|
|
183
|
+
window.commit_list.clearSelection()
|
|
184
|
+
self.read_selection(session_id, oid)
|
|
185
|
+
|
|
186
|
+
def _finish_navigation(self, session_id: str, commit: Commit) -> None:
|
|
187
|
+
"""Reveal the real row and announce only a filter that hid this target."""
|
|
188
|
+
if not self.operation_policy_controller.allows(session_id, WindowCapability.CONSULT):
|
|
189
|
+
return
|
|
190
|
+
session = self.session_registry_controller.session_by_id[session_id]
|
|
191
|
+
window = self.session_registry_controller.window_by_session[session_id]
|
|
192
|
+
if session.mode == "file_history" and (session.search_pending or commit.oid not in session.visible_oid_set):
|
|
193
|
+
return
|
|
194
|
+
session.navigation_oid = None
|
|
195
|
+
session.navigation_commit = None
|
|
196
|
+
if (session.snapshot is not None and commit.oid not in session.snapshot.parent_oids_by_oid
|
|
197
|
+
and not any(explicit.oid == commit.oid for explicit in session.snapshot.explicit_commits)):
|
|
198
|
+
session.snapshot.explicit_commits.append(summarize_commit(commit))
|
|
199
|
+
window.reveal_commit(commit.oid)
|
|
200
|
+
window.commit = commit
|
|
201
|
+
window.commit_panel.show_commit(commit)
|
|
202
|
+
if session.navigation_filter_cleared:
|
|
203
|
+
window.statusBar().showMessage(f"Filter cleared to show commit {commit.short_oid}.")
|
|
204
|
+
session.navigation_filter_cleared = False
|
|
205
|
+
|
|
206
|
+
def _read_relation(self, session_id: str) -> None:
|
|
207
|
+
"""Associate each relative read with the active detail and index completeness."""
|
|
208
|
+
if not self.operation_policy_controller.allows(session_id, WindowCapability.CONSULT):
|
|
209
|
+
return
|
|
210
|
+
session = self.session_registry_controller.session_by_id[session_id]
|
|
211
|
+
commit = self.session_registry_controller.window_by_session[session_id].commit_panel.commit
|
|
212
|
+
if commit is None or commit.oid != session.selected_oid:
|
|
213
|
+
return
|
|
214
|
+
session.relation_generation += 1
|
|
215
|
+
identity = RequestIdentity(session_id, session.generation, session.selection_generation, commit.oid, relation_generation=session.relation_generation)
|
|
216
|
+
session.relation_identity = identity
|
|
217
|
+
self.session_registry_controller.window_by_session[session_id].commit_panel.child_label.setText(SharedText.CHILD_LOADING.value)
|
|
218
|
+
self.repository.read_relation(commit, session.snapshot, identity)
|
|
219
|
+
|
|
220
|
+
def relation_read(self, identity: RequestIdentity, relation: CommitRelation) -> None:
|
|
221
|
+
"""Reject late relatives from another selection, capture or index read."""
|
|
222
|
+
session = self.session_registry_controller.session_by_id.get(identity.session_id)
|
|
223
|
+
if (session is None or session.relation_identity != identity or session.generation != identity.generation
|
|
224
|
+
or session.selection_generation != identity.selection_generation or session.selected_oid != relation.oid):
|
|
225
|
+
return
|
|
226
|
+
if session.mode == "file_history":
|
|
227
|
+
parents = []
|
|
228
|
+
child_links = []
|
|
229
|
+
for link in relation.parents:
|
|
230
|
+
parents.append(replace(link, available=link.available and link.oid in session.visible_oid_set))
|
|
231
|
+
for link in relation.child_links:
|
|
232
|
+
child_links.append(replace(link, available=link.available and link.oid in session.visible_oid_set))
|
|
233
|
+
relation = replace(relation, parents=parents, child_links=child_links)
|
|
234
|
+
self.session_registry_controller.window_by_session[identity.session_id].show_commit_relation(relation)
|
|
235
|
+
|
|
236
|
+
def retry_selection(self, session_id: str, oid: str) -> None:
|
|
237
|
+
"""Give an explicit retry a fresh identity so another diagnostic can be shown."""
|
|
238
|
+
session = self.session_registry_controller.session_by_id.get(session_id)
|
|
239
|
+
if session is None or session.selected_oid != oid:
|
|
240
|
+
return
|
|
241
|
+
session.selection_generation += 1
|
|
242
|
+
self.read_selection(session_id, oid)
|
|
243
|
+
|
|
244
|
+
def read_visible_tree(self, session_id: str) -> None:
|
|
245
|
+
"""Read the historical tree of the selected commit once the File tree tab is on screen.
|
|
246
|
+
|
|
247
|
+
The tree of a commit nobody looks at is never read. The read belongs to the current selection and panel
|
|
248
|
+
generation, so the answer for a commit the user has left is dropped, and showing the tab again for the same
|
|
249
|
+
selection, an empty tree or a failed read included, asks for nothing more; Retry is the way to read again.
|
|
250
|
+
An answer that arrived while the tab was hidden is built now from the entries already received, without a new
|
|
251
|
+
Git read, provided it still belongs to the current selection.
|
|
252
|
+
"""
|
|
253
|
+
session = self.session_registry_controller.session_by_id.get(session_id)
|
|
254
|
+
window = self.session_registry_controller.window_by_session.get(session_id)
|
|
255
|
+
if session is None or session.is_closed or window is None or not isValid(window):
|
|
256
|
+
return
|
|
257
|
+
if window.detail_tabs.currentWidget() is not window.file_tree_panel:
|
|
258
|
+
return
|
|
259
|
+
# Only the commit read for the current selection and generation can own the read: a commit still shown from
|
|
260
|
+
# before a refresh or a new selection waits for its own detail read, which asks again.
|
|
261
|
+
request = session.commit_identity
|
|
262
|
+
commit = window.commit
|
|
263
|
+
if (request is None or commit is None or commit.detail_error or request.oid != commit.oid or request.oid != session.selected_oid
|
|
264
|
+
or request.generation != session.generation or request.selection_generation != session.selection_generation):
|
|
265
|
+
return
|
|
266
|
+
pending = session.pending_tree_identity
|
|
267
|
+
session.pending_tree_identity = None
|
|
268
|
+
if pending is not None and self.file_is_current(pending):
|
|
269
|
+
window.file_tree_panel.show_tree(session.tree_entries)
|
|
270
|
+
return
|
|
271
|
+
if session.file_identity_by_kind.get(FilePanel.TREE) is not None:
|
|
272
|
+
return
|
|
273
|
+
tree_identity = FileReadIdentity(request, FilePanel.TREE, session.panel_generation)
|
|
274
|
+
session.file_identity_by_kind[FilePanel.TREE] = tree_identity
|
|
275
|
+
self.repository.read_tree(commit.oid, tree_identity)
|
|
276
|
+
|
|
277
|
+
def tree_read(self, identity: FileReadIdentity, entries: list[TreeEntry]) -> None:
|
|
278
|
+
"""Present paths only for the active commit and repository window, and only while the File tree tab is shown.
|
|
279
|
+
|
|
280
|
+
The entries are kept in every case because choosing a file of the tree relies on them. Building thousands of
|
|
281
|
+
tree rows for a tab the user already left would only block the window, so a hidden tab keeps the answer as
|
|
282
|
+
pending and the tree is built when the tab comes back.
|
|
283
|
+
"""
|
|
284
|
+
if not self.file_is_current(identity):
|
|
285
|
+
return
|
|
286
|
+
session_id = identity.request_identity.session_id
|
|
287
|
+
session = self.session_registry_controller.session_by_id[session_id]
|
|
288
|
+
session.tree_entries = entries
|
|
289
|
+
window = self.session_registry_controller.window_by_session[session_id]
|
|
290
|
+
if window.detail_tabs.currentWidget() is not window.file_tree_panel:
|
|
291
|
+
session.pending_tree_identity = identity
|
|
292
|
+
return
|
|
293
|
+
window.file_tree_panel.show_tree(entries)
|
|
294
|
+
|
|
295
|
+
def read_blob(self, session_id: str, entry: TreeEntry) -> None:
|
|
296
|
+
"""Resolve the selected tree object independently of the diff selection."""
|
|
297
|
+
session = self.session_registry_controller.session_by_id.get(session_id)
|
|
298
|
+
if session is None or entry not in session.tree_entries:
|
|
299
|
+
return
|
|
300
|
+
session.panel_generation += 1
|
|
301
|
+
session.selected_tree_entry = entry
|
|
302
|
+
request = RequestIdentity(session_id, session.generation, session.selection_generation, session.selected_oid)
|
|
303
|
+
identity = FileReadIdentity(request, FilePanel.BLOB, session.panel_generation, new_path=entry.path)
|
|
304
|
+
session.file_identity_by_kind[FilePanel.BLOB] = identity
|
|
305
|
+
panel = self.session_registry_controller.window_by_session[session_id].file_tree_panel
|
|
306
|
+
panel.selected_entry = entry
|
|
307
|
+
panel.begin_loading()
|
|
308
|
+
self.repository.cancel_file(session_id, FilePanel.BLOB)
|
|
309
|
+
if entry.mode == "160000":
|
|
310
|
+
panel.show_submodule(entry.oid)
|
|
311
|
+
return
|
|
312
|
+
self.repository.read_blob(entry.oid, identity)
|
|
313
|
+
|
|
314
|
+
def blob_read(self, identity: FileReadIdentity, content: BlobContent) -> None:
|
|
315
|
+
"""Ignore late file bytes even when they belong to another file in the same tree."""
|
|
316
|
+
if not self.file_is_current(identity):
|
|
317
|
+
return
|
|
318
|
+
session_id = identity.request_identity.session_id
|
|
319
|
+
entry = self.session_registry_controller.session_by_id[session_id].selected_tree_entry
|
|
320
|
+
if entry is None or entry.oid != content.oid:
|
|
321
|
+
return
|
|
322
|
+
self.session_registry_controller.window_by_session[session_id].file_tree_panel.show_blob(content)
|
|
323
|
+
|
|
324
|
+
def changes_read(self, identity: FileReadIdentity, groups: list[ComparisonGroup]) -> None:
|
|
325
|
+
"""Publish file groups only for the active comparison request."""
|
|
326
|
+
if not self.file_is_current(identity):
|
|
327
|
+
return
|
|
328
|
+
session = self.session_registry_controller.session_by_id[identity.request_identity.session_id]
|
|
329
|
+
session.diff_groups = groups
|
|
330
|
+
window = self.session_registry_controller.window_by_session[session.session_id]
|
|
331
|
+
panel = window.diff_panel
|
|
332
|
+
selected_entry = panel.selected_entry
|
|
333
|
+
panel.show_changes(groups)
|
|
334
|
+
if selected_entry not in panel.item_by_entry and session.file_history_result is not None and window.commit is not None:
|
|
335
|
+
selected_entry = session.file_history_result.select_entry(window.commit, groups)
|
|
336
|
+
if selected_entry not in panel.item_by_entry:
|
|
337
|
+
selected_entry = panel.first_entry()
|
|
338
|
+
if selected_entry in panel.item_by_entry:
|
|
339
|
+
panel.file_tree.setCurrentItem(panel.item_by_entry[selected_entry])
|
|
340
|
+
|
|
341
|
+
def retry_file_tree(self, session_id: str, kind: FilePanel | None) -> None:
|
|
342
|
+
"""Retry only the failed tree or blob with fresh ownership in either window mode; any other panel, or none, is not retried here."""
|
|
343
|
+
session = self.session_registry_controller.session_by_id.get(session_id)
|
|
344
|
+
if session is None or session.is_closed or session.selected_oid is None:
|
|
345
|
+
return
|
|
346
|
+
if kind == FilePanel.BLOB and session.selected_tree_entry is not None:
|
|
347
|
+
self.read_blob(session_id, session.selected_tree_entry)
|
|
348
|
+
return
|
|
349
|
+
if kind != FilePanel.TREE:
|
|
350
|
+
return
|
|
351
|
+
session.panel_generation += 1
|
|
352
|
+
session.pending_tree_identity = None
|
|
353
|
+
request = RequestIdentity(session_id, session.generation, session.selection_generation, session.selected_oid)
|
|
354
|
+
identity = FileReadIdentity(request, FilePanel.TREE, session.panel_generation)
|
|
355
|
+
session.file_identity_by_kind[FilePanel.TREE] = identity
|
|
356
|
+
self.session_registry_controller.window_by_session[session_id].file_tree_panel.show_error("Loading files…")
|
|
357
|
+
self.repository.read_tree(session.selected_oid, identity)
|
|
358
|
+
|
|
359
|
+
def file_failed(self, identity: FileReadIdentity, failure: RepositoryFailure) -> None:
|
|
360
|
+
"""Present a file-specific diagnostic without marking commit metadata unavailable."""
|
|
361
|
+
if identity in self.session_registry_controller.export_owner_by_identity:
|
|
362
|
+
self.system_tool.fail_export(identity, failure.cause)
|
|
363
|
+
return
|
|
364
|
+
if not self.file_is_current(identity):
|
|
365
|
+
return
|
|
366
|
+
diagnostic = Diagnostic(failure.operation, failure.repository_dir, failure.program_filename, failure.arguments, failure.cause, failure.stderr, failure.return_code).prepare()
|
|
367
|
+
window = self.session_registry_controller.window_by_session[identity.request_identity.session_id]
|
|
368
|
+
if identity.panel_kind == FilePanel.DIFF:
|
|
369
|
+
window.diff_panel.show_error(diagnostic.detail)
|
|
370
|
+
else:
|
|
371
|
+
window.file_tree_panel.show_error(diagnostic.detail, retry_kind=identity.panel_kind)
|
|
372
|
+
if self.diagnostic_log is not None:
|
|
373
|
+
self.diagnostic_log.write(diagnostic)
|
|
374
|
+
|
|
375
|
+
def snapshot_read(self, identity: RequestIdentity, snapshot: HistorySnapshot) -> None:
|
|
376
|
+
"""Route one capture through its owning session to the native model."""
|
|
377
|
+
if identity.session_id in self.session_registry_controller.navigation_owner_by_id:
|
|
378
|
+
return
|
|
379
|
+
if self.session_registry_controller.identity_by_session.get(identity.session_id) != identity:
|
|
380
|
+
return
|
|
381
|
+
session = self.session_registry_controller.session_by_id[identity.session_id]
|
|
382
|
+
if session.file_history_engine is not None and session.snapshot is not snapshot:
|
|
383
|
+
return
|
|
384
|
+
session.snapshot = snapshot
|
|
385
|
+
if session.file_history_anchor is not None:
|
|
386
|
+
if snapshot.complete and session.file_history_engine is None:
|
|
387
|
+
self._calculate_file_history(session)
|
|
388
|
+
return
|
|
389
|
+
if session.search_pending or session.filter_text or session.refresh_active:
|
|
390
|
+
if snapshot.complete and session.search_identity is None:
|
|
391
|
+
self.start_search(identity.session_id)
|
|
392
|
+
return
|
|
393
|
+
self.session_registry_controller.window_by_session[identity.session_id].show_snapshot(snapshot)
|
|
394
|
+
if snapshot.complete:
|
|
395
|
+
self._read_relation(identity.session_id)
|
|
396
|
+
|
|
397
|
+
def _calculate_file_history(self, session: RepositorySession) -> None:
|
|
398
|
+
"""Publish a captured file membership only to the still-open owning session."""
|
|
399
|
+
session.file_history_generation += 1
|
|
400
|
+
generation = session.file_history_generation
|
|
401
|
+
engine = FileHistoryEngine(self.repository)
|
|
402
|
+
session.file_history_engine = engine
|
|
403
|
+
|
|
404
|
+
def receive(result: FileHistoryResult) -> None:
|
|
405
|
+
"""Start message filtering only after the file membership is known."""
|
|
406
|
+
if session.is_closed or session.file_history_generation != generation:
|
|
407
|
+
return
|
|
408
|
+
session.file_history_result = result
|
|
409
|
+
self.session_registry_controller.window_by_session[session.session_id].file_history_result = result
|
|
410
|
+
session.search(session.filter_text)
|
|
411
|
+
self.session_registry_controller.window_by_session[session.session_id].begin_search()
|
|
412
|
+
self.start_search(session.session_id)
|
|
413
|
+
|
|
414
|
+
def progress(count: int, total: int) -> None:
|
|
415
|
+
"""Show progress without publishing an unfinished file membership."""
|
|
416
|
+
if not session.is_closed and session.file_history_generation == generation:
|
|
417
|
+
self.session_registry_controller.window_by_session[session.session_id].statusBar().showMessage(f"Reading file history… {count}/{total} commits")
|
|
418
|
+
|
|
419
|
+
engine.finished.connect(receive)
|
|
420
|
+
engine.progressed.connect(progress)
|
|
421
|
+
engine.calculate(session.snapshot, session.file_history_anchor, self.session_registry_controller.identity_by_session[session.session_id])
|
|
422
|
+
|
|
423
|
+
def commit_batch_read(self, identity: RequestIdentity, commits: list[Commit]) -> None:
|
|
424
|
+
"""Keep the rows the history read just parsed, and append them, in Git's order, to the live generation of the whole history.
|
|
425
|
+
|
|
426
|
+
The rows are pushed as the history is read, and kept with their history so that an empty query can show them
|
|
427
|
+
again without reading Git. A file history, a search, a filter or a refresh shows its own rows instead, so it
|
|
428
|
+
does not append these; neither does a walk into another repository, whose window is not replaced yet.
|
|
429
|
+
"""
|
|
430
|
+
if identity.session_id in self.session_registry_controller.navigation_owner_by_id:
|
|
431
|
+
return
|
|
432
|
+
snapshot = self.repository.snapshot_by_session.get(identity.session_id)
|
|
433
|
+
if self.session_registry_controller.identity_by_session.get(identity.session_id) != identity:
|
|
434
|
+
return
|
|
435
|
+
session = self.session_registry_controller.session_by_id[identity.session_id]
|
|
436
|
+
if snapshot is not None:
|
|
437
|
+
session.streamed_history = keep_streamed_commits(session.streamed_history, snapshot, commits)
|
|
438
|
+
if session.mode == "file_history" or session.snapshot is None:
|
|
439
|
+
return
|
|
440
|
+
if session.search_pending or session.filter_text or session.refresh_active:
|
|
441
|
+
return
|
|
442
|
+
self.session_registry_controller.window_by_session[identity.session_id].append_history(commits)
|
|
443
|
+
|
|
444
|
+
def start_search(self, session_id: str) -> None:
|
|
445
|
+
"""Start one scan only once the captured ancestry is complete.
|
|
446
|
+
|
|
447
|
+
An empty query answers on a later turn with the rows already read; that answer is dropped if the window's history
|
|
448
|
+
list is destroyed first.
|
|
449
|
+
"""
|
|
450
|
+
session = self.session_registry_controller.session_by_id.get(session_id)
|
|
451
|
+
if session is None or session.snapshot is None or not session.snapshot.complete or session.search_identity is not None:
|
|
452
|
+
return
|
|
453
|
+
if self.session_registry_controller.window_by_session[session_id].search_timer.isActive():
|
|
454
|
+
return
|
|
455
|
+
if session.mode == "file_history" and session.file_history_result is None:
|
|
456
|
+
return
|
|
457
|
+
if not session.search_pending:
|
|
458
|
+
session.search(session.filter_text)
|
|
459
|
+
identity = RequestIdentity(session_id, session.generation, search_generation=session.search_generation)
|
|
460
|
+
session.search_identity = identity
|
|
461
|
+
# An empty query shows every commit: the rows the history read already delivered are the answer. They go
|
|
462
|
+
# through the same path as a search's answer, on a later turn of the event loop as a search's would.
|
|
463
|
+
streamed_history = session.streamed_history
|
|
464
|
+
commits = streamed_history.complete_commits(session.snapshot) if streamed_history is not None and not session.filter_text else None
|
|
465
|
+
if commits is not None:
|
|
466
|
+
# Tied to the history list: a window closed before the next turn takes the answer with it, instead of
|
|
467
|
+
# publishing into a list that no longer exists.
|
|
468
|
+
QTimer.singleShot(0, self.session_registry_controller.window_by_session[session_id].commit_list, lambda: self.matching_read(identity, commits, len(commits), True))
|
|
469
|
+
return
|
|
470
|
+
window = self.session_registry_controller.window_by_session[session_id]
|
|
471
|
+
self.repository.read_filter_matches(session.snapshot, session.filter_text, window.filter_mode, identity)
|
|
472
|
+
|
|
473
|
+
def matching_read(self, identity: RequestIdentity, commits: list[Commit], scanned_count: int, complete: bool) -> None:
|
|
474
|
+
"""Publish a coherent graph only after the current query has checked every OID."""
|
|
475
|
+
if identity.session_id in self.session_registry_controller.navigation_owner_by_id:
|
|
476
|
+
return
|
|
477
|
+
session = self.session_registry_controller.session_by_id.get(identity.session_id)
|
|
478
|
+
if session is None or session.search_identity != identity or not session.search_pending:
|
|
479
|
+
return
|
|
480
|
+
for commit in commits:
|
|
481
|
+
if session.file_history_result is None or commit.oid in session.file_history_result.touching_oid_set:
|
|
482
|
+
session.matching_commits.append(commit)
|
|
483
|
+
window = self.session_registry_controller.window_by_session[identity.session_id]
|
|
484
|
+
if not complete:
|
|
485
|
+
if session.refresh_active:
|
|
486
|
+
window.statusBar().showMessage(f"Refreshing… {scanned_count} commits scanned")
|
|
487
|
+
else:
|
|
488
|
+
window.show_search_progress(len(session.matching_commits), scanned_count)
|
|
489
|
+
return
|
|
490
|
+
session.search_pending = False
|
|
491
|
+
refresh_anchor = session.view_anchor
|
|
492
|
+
navigation_commit = session.navigation_commit
|
|
493
|
+
if (session.mode != "file_history" and navigation_commit is not None and navigation_commit.oid == session.selected_oid
|
|
494
|
+
and not any(commit.oid == navigation_commit.oid for commit in session.matching_commits)):
|
|
495
|
+
session.matching_commits.append(summarize_commit(navigation_commit))
|
|
496
|
+
session.visible_oid_set = set()
|
|
497
|
+
for commit in session.matching_commits:
|
|
498
|
+
session.visible_oid_set.add(commit.oid)
|
|
499
|
+
parent_map = visible_parent_map(session.snapshot, session.visible_oid_set) if session.mode == "file_history" else None
|
|
500
|
+
missing_checkout_selection = (identity.session_id in self.refresh_controller.checkout_refresh_session_set and session.selected_oid is not None
|
|
501
|
+
and session.selected_oid not in session.visible_oid_set)
|
|
502
|
+
if missing_checkout_selection:
|
|
503
|
+
with QSignalBlocker(window):
|
|
504
|
+
window.show_matches(session.snapshot, session.matching_commits, session.filter_text, session.selected_oid, parent_map)
|
|
505
|
+
if session.refresh_active and refresh_anchor is not None:
|
|
506
|
+
window.restore_anchor(refresh_anchor)
|
|
507
|
+
window.clear_commit_selection()
|
|
508
|
+
session.selected_oid = None
|
|
509
|
+
session.selection_generation += 1
|
|
510
|
+
session.invalidate_file_selection()
|
|
511
|
+
session.navigation_commit = None
|
|
512
|
+
navigation_commit = None
|
|
513
|
+
else:
|
|
514
|
+
window.show_matches(session.snapshot, session.matching_commits, session.filter_text, session.selected_oid, parent_map)
|
|
515
|
+
if session.refresh_active and refresh_anchor is not None:
|
|
516
|
+
window.restore_anchor(refresh_anchor)
|
|
517
|
+
self.refresh_controller.checkout_refresh_session_set.discard(identity.session_id)
|
|
518
|
+
# Only the last read of a refresh moves the view: a read queued behind this one keeps the position it starts from.
|
|
519
|
+
if session.refresh_active and session.refresh_queued is None and identity.session_id in self.history_top_session_set:
|
|
520
|
+
self.history_top_session_set.discard(identity.session_id)
|
|
521
|
+
window.scroll_history_to_top()
|
|
522
|
+
self.history_published.emit(identity.session_id, identity.generation)
|
|
523
|
+
session.matching_commits = []
|
|
524
|
+
if navigation_commit is not None and navigation_commit.oid == session.selected_oid:
|
|
525
|
+
self._finish_navigation(identity.session_id, navigation_commit)
|
|
526
|
+
if not window.history_model.commits:
|
|
527
|
+
session.selected_oid = None
|
|
528
|
+
session.selection_generation += 1
|
|
529
|
+
session.invalidate_file_selection()
|
|
530
|
+
window.diff_panel.clear_selection()
|
|
531
|
+
window.file_tree_panel.clear_selection()
|
|
532
|
+
if session.refresh_active and session.refresh_queued is None:
|
|
533
|
+
# The history just published wrote its own status line: a line owed about an ended rebase is added to it,
|
|
534
|
+
# naming the HEAD this very refresh read.
|
|
535
|
+
self.history_status_written.emit(identity.session_id, self.reference_read_controller.reference_snapshot_by_session.get(identity.session_id))
|
|
536
|
+
if session.refresh_active:
|
|
537
|
+
session.refresh_active = False
|
|
538
|
+
session.view_anchor = None
|
|
539
|
+
self.refresh_controller.recovery_request_set.discard(self.session_registry_controller.identity_by_session[identity.session_id])
|
|
540
|
+
self.refresh_controller.leave_full_refresh_read(identity.session_id)
|
|
541
|
+
if session.refresh_queued is not None:
|
|
542
|
+
request = session.refresh_queued
|
|
543
|
+
session.refresh_queued = None
|
|
544
|
+
self.refresh_controller.submit_refresh_request(identity.session_id, request)
|
|
545
|
+
|
|
546
|
+
def commit_read(self, identity: RequestIdentity, commit: Commit) -> None:
|
|
547
|
+
"""Discard superseded detail responses without disturbing ongoing history reads."""
|
|
548
|
+
session = self.session_registry_controller.session_by_id.get(identity.session_id)
|
|
549
|
+
if session is None or session.generation != identity.generation or session.selection_generation != identity.selection_generation or session.selected_oid != commit.oid or identity.oid != commit.oid:
|
|
550
|
+
return
|
|
551
|
+
window = self.session_registry_controller.window_by_session[identity.session_id]
|
|
552
|
+
window.commit = commit
|
|
553
|
+
session.commit_identity = identity
|
|
554
|
+
window.commit_panel.show_commit(commit)
|
|
555
|
+
if not commit.detail_error:
|
|
556
|
+
file_identity = FileReadIdentity(identity, panel_generation=session.panel_generation)
|
|
557
|
+
if session.file_identity_by_kind.get(FilePanel.DIFF) is None:
|
|
558
|
+
session.file_identity_by_kind[FilePanel.DIFF] = file_identity
|
|
559
|
+
self.repository.read_changes(commit, file_identity)
|
|
560
|
+
self.read_visible_tree(identity.session_id)
|
|
561
|
+
if session.navigation_oid == commit.oid and not commit.detail_error:
|
|
562
|
+
if commit.oid in window.history_model.row_by_oid and not window.search_pending:
|
|
563
|
+
self._finish_navigation(identity.session_id, commit)
|
|
564
|
+
elif session.mode == "file_history":
|
|
565
|
+
session.navigation_commit = commit
|
|
566
|
+
else:
|
|
567
|
+
session.navigation_commit = commit
|
|
568
|
+
# The files a commit changes come from another read than this one, so in the file name mode the filter is
|
|
569
|
+
# cleared to show the commit: a commit the finished search left out is one the filter hides.
|
|
570
|
+
hidden_by_filter = window.filter_mode == HistoryFilterMode.FILES or not TextQuery(session.filter_text).matches(commit.message)
|
|
571
|
+
if session.filter_text and hidden_by_filter:
|
|
572
|
+
session.navigation_filter_cleared = True
|
|
573
|
+
window.clear_filter()
|
|
574
|
+
else:
|
|
575
|
+
session.search(session.filter_text)
|
|
576
|
+
window.begin_search()
|
|
577
|
+
self.start_search(identity.session_id)
|
|
578
|
+
self._read_relation(identity.session_id)
|
|
579
|
+
|
|
580
|
+
def failed(self, identity: RequestIdentity, failure: RepositoryFailure) -> None:
|
|
581
|
+
"""Present one prepared diagnostic while allowing a failed opening to retry."""
|
|
582
|
+
if identity.session_id in self.session_registry_controller.navigation_owner_by_id:
|
|
583
|
+
return
|
|
584
|
+
session = self.session_registry_controller.session_by_id.get(identity.session_id)
|
|
585
|
+
selected_request = session is not None and identity.oid is not None and session.generation == identity.generation and session.selection_generation == identity.selection_generation and session.selected_oid == identity.oid
|
|
586
|
+
relation_request = selected_request and bool(identity.relation_generation)
|
|
587
|
+
if relation_request and session.relation_identity != identity:
|
|
588
|
+
return
|
|
589
|
+
search_request = session is not None and session.search_identity == identity and session.search_pending
|
|
590
|
+
if self.session_registry_controller.identity_by_session.get(identity.session_id) != identity and not selected_request and not search_request:
|
|
591
|
+
return
|
|
592
|
+
if identity in self.reported_request_set:
|
|
593
|
+
return
|
|
594
|
+
self.reported_request_set.add(identity)
|
|
595
|
+
diagnostic = Diagnostic(failure.operation, failure.repository_dir, failure.program_filename, failure.arguments, failure.cause, failure.stderr, failure.return_code).prepare()
|
|
596
|
+
clone_dialog = self.session_registry_controller.clone_dialog_by_session.get(identity.session_id)
|
|
597
|
+
if clone_dialog is not None and clone_dialog.operation_id is not None and identity.session_id not in self.session_registry_controller.window_by_session:
|
|
598
|
+
run = self.clone.run_by_id[clone_dialog.operation_id]
|
|
599
|
+
if run.clone_status == OperationStatus.SUCCESS.value or run.incomplete_repository:
|
|
600
|
+
self.session_registry_controller.identity_by_session.pop(identity.session_id, None)
|
|
601
|
+
self.session_registry_controller.welcome_by_session[identity.session_id].set_loading(False)
|
|
602
|
+
self.clone.open_result(run.operation_id, False, diagnostic)
|
|
603
|
+
clone_dialog.show_failure(failure_summary(diagnostic), partial=True)
|
|
604
|
+
run.trace.append_diagnostic(run.operation_id, "error", diagnostic, OperationStatus.FAILED)
|
|
605
|
+
return
|
|
606
|
+
if relation_request:
|
|
607
|
+
self.session_registry_controller.window_by_session[identity.session_id].commit_panel.child_label.setText("Children unknown — incomplete read: " + diagnostic.cause)
|
|
608
|
+
elif selected_request:
|
|
609
|
+
self.session_registry_controller.window_by_session[identity.session_id].commit_panel.show_error(identity.oid, diagnostic.cause)
|
|
610
|
+
parent = self.session_registry_controller.welcome_by_session.get(identity.session_id) or self.session_registry_controller.window_by_session.get(identity.session_id)
|
|
611
|
+
if parent is None:
|
|
612
|
+
return
|
|
613
|
+
if isinstance(parent, WelcomeWindow):
|
|
614
|
+
parent.set_loading(False)
|
|
615
|
+
parent.mark_unavailable(self.session_registry_controller.opening_dir_by_session.get(identity.session_id, failure.repository_dir))
|
|
616
|
+
self.session_registry_controller.identity_by_session.pop(identity.session_id, None)
|
|
617
|
+
else:
|
|
618
|
+
session = self.session_registry_controller.session_by_id[identity.session_id]
|
|
619
|
+
if session.refresh_active and not selected_request:
|
|
620
|
+
self.refresh_failed.emit(identity.session_id, diagnostic)
|
|
621
|
+
session.refresh_active = False
|
|
622
|
+
session.refresh_queued = None
|
|
623
|
+
self.refresh_controller.leave_full_refresh_read(identity.session_id)
|
|
624
|
+
session.search_pending = False
|
|
625
|
+
session.search_identity = None
|
|
626
|
+
parent.search_pending = False
|
|
627
|
+
parent.commit_list.setEnabled(True)
|
|
628
|
+
parent.history_state_label.hide()
|
|
629
|
+
parent.statusBar().showMessage("State not refreshed — references not refreshed")
|
|
630
|
+
elif search_request:
|
|
631
|
+
session.search_pending = False
|
|
632
|
+
session.search_identity = None
|
|
633
|
+
parent.statusBar().showMessage("State not refreshed — search incomplete")
|
|
634
|
+
elif not selected_request and session.snapshot is not None and session.snapshot.generation == identity.generation:
|
|
635
|
+
session.snapshot.error = diagnostic.cause
|
|
636
|
+
parent.update_history_status()
|
|
637
|
+
else:
|
|
638
|
+
parent.statusBar().showMessage("State not refreshed — references not refreshed")
|
|
639
|
+
if self.diagnostic_log is not None:
|
|
640
|
+
self.diagnostic_log.write(diagnostic)
|
|
641
|
+
if self.diagnostic_log.warning:
|
|
642
|
+
if isinstance(parent, WelcomeWindow):
|
|
643
|
+
parent.show_status(self.diagnostic_log.warning)
|
|
644
|
+
else:
|
|
645
|
+
parent.statusBar().showMessage(self.diagnostic_log.warning)
|
|
646
|
+
if identity in self.refresh_controller.recovery_request_set:
|
|
647
|
+
self.refresh_controller.recovery_request_set.discard(identity)
|
|
648
|
+
return
|
|
649
|
+
dialog = ErrorDialog(diagnostic, parent, self.diagnostic_log.log_filename if self.diagnostic_log is not None else None)
|
|
650
|
+
if isinstance(parent, WelcomeWindow):
|
|
651
|
+
dialog.setWindowTitle("Could not open repository")
|
|
652
|
+
opening_diagnostic = Diagnostic(failure.operation, failure.repository_dir, failure.program_filename, [],
|
|
653
|
+
f"Cannot open {failure.repository_dir}: {failure.cause}. Check the path and access permissions, then try again.", failure.stderr).prepare()
|
|
654
|
+
dialog.cause_label.setText(opening_diagnostic.cause)
|
|
655
|
+
self.session_registry_controller.dialogs.append(dialog)
|
|
656
|
+
|
|
657
|
+
def finish() -> None:
|
|
658
|
+
"""Attempt one recovery after dismissal, provided the original view still exists."""
|
|
659
|
+
if dialog in self.session_registry_controller.dialogs:
|
|
660
|
+
self.session_registry_controller.dialogs.remove(dialog)
|
|
661
|
+
navigation_failed = (selected_request and not relation_request and session is not None and session.navigation_oid == identity.oid
|
|
662
|
+
and session.selection_generation == identity.selection_generation)
|
|
663
|
+
if navigation_failed:
|
|
664
|
+
session.navigation_oid = None
|
|
665
|
+
session.navigation_commit = None
|
|
666
|
+
if (self.session_registry_controller.identity_by_session.get(identity.session_id) == identity or navigation_failed) and identity.session_id in self.session_registry_controller.window_by_session:
|
|
667
|
+
self.refresh_controller.refresh(identity.session_id, recovery=True)
|
|
668
|
+
dialog.deleteLater()
|
|
669
|
+
|
|
670
|
+
dialog.finished.connect(finish)
|
|
671
|
+
open_window_modal(dialog)
|