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,566 @@
|
|
|
1
|
+
"""Time each commit in the optional --log file, from the click on Commit to the displayed result.
|
|
2
|
+
|
|
3
|
+
The click line is written when Prepare changes sends its commit request, from Commit or from Commit & push. The result
|
|
4
|
+
line is written once the new commit is painted in the history of every repository window that manages the worktree, and
|
|
5
|
+
no work of that repository is left:
|
|
6
|
+
refreshes and searches of its sessions, preparation reads, processes of the two runners started in one of its folders,
|
|
7
|
+
worktree continuations and file tasks of it, its reservations, repository parsing and callbacks (those queued for the
|
|
8
|
+
next turn of the event loop included), reference continuations
|
|
9
|
+
and shared refreshes of its sessions. The repository's folders are its common Git folder and the worktree of each of its
|
|
10
|
+
open sessions and Prepare changes windows, the submodules inside them included. Work whose repository is not known counts,
|
|
11
|
+
and work of another repository open in GitPane does not, so the time of a commit does not grow with a transfer elsewhere.
|
|
12
|
+
In a measured test, which opens only one repository, these are the bounds of the commit measurement, so a time read in the
|
|
13
|
+
log of a real commit compares with the time the commit measurement of the tests reads; that measurement checks that they agree
|
|
14
|
+
with its own.
|
|
15
|
+
|
|
16
|
+
A window whose list shows the new commit's row outside its visible part, as after the user scrolled it, has no paint to
|
|
17
|
+
wait for: the published history that holds the commit is what the user sees by scrolling back. A commit not seen
|
|
18
|
+
displayed within COMMIT_TIMING_LIMIT_MS of its click writes an "abandoned" line and is no longer followed, so a window
|
|
19
|
+
whose history never shows it is not watched for the rest of the session.
|
|
20
|
+
|
|
21
|
+
The push of Commit & push is work of the same repository started by the same click, so the result line comes after it.
|
|
22
|
+
|
|
23
|
+
A commit refused or failed, or one whose refresh fails, writes a failure line instead, and never a result line after
|
|
24
|
+
it; so does a Commit & push whose request is dropped before the commit service gives a result, as when the reading of
|
|
25
|
+
where to push is refused. Without an open log, nothing is subscribed: the commit goes the same way as without this controller, and a log that
|
|
26
|
+
a disk error closes on the way ends every timing without an error.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import os
|
|
33
|
+
import time
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from enum import Enum
|
|
36
|
+
from typing import TYPE_CHECKING
|
|
37
|
+
|
|
38
|
+
from PySide6.QtCore import QEvent, QObject, QTimer
|
|
39
|
+
from shiboken6 import isValid
|
|
40
|
+
|
|
41
|
+
from gitpane.config import COMMIT_TIMING_LIMIT_MS, NANOSECONDS_PER_MILLISECOND
|
|
42
|
+
from gitpane.operation.operation_status import OperationStatus
|
|
43
|
+
from gitpane.process.process_log import local_now
|
|
44
|
+
from gitpane.system.path_identity import canonical_path
|
|
45
|
+
from gitpane.ui.repository.history_model import MESSAGE_COLUMN
|
|
46
|
+
|
|
47
|
+
if TYPE_CHECKING:
|
|
48
|
+
from datetime import datetime
|
|
49
|
+
|
|
50
|
+
from PySide6.QtCore import QRect
|
|
51
|
+
from PySide6.QtWidgets import QWidget
|
|
52
|
+
|
|
53
|
+
from gitpane.account.authentication import AuthenticationService
|
|
54
|
+
from gitpane.commit.commit_write import CommitOutcome, CommitRequest
|
|
55
|
+
from gitpane.controller.history_read_controller import HistoryReadController
|
|
56
|
+
from gitpane.controller.reference_read_controller import ReferenceReadController
|
|
57
|
+
from gitpane.controller.refresh_controller import RefreshController
|
|
58
|
+
from gitpane.controller.session_registry import RepositorySession, SessionRegistry
|
|
59
|
+
from gitpane.controller.staging_session_registry import StagingSessionRegistry
|
|
60
|
+
from gitpane.controller.worktree_state_controller import WorktreeStateController
|
|
61
|
+
from gitpane.history.repository import GitRepository
|
|
62
|
+
from gitpane.history.repository_value import RepositoryContext
|
|
63
|
+
from gitpane.operation.diagnostic import DiagnosticLog, PreparedDiagnostic
|
|
64
|
+
from gitpane.operation.repository_operation import OperationIntent, RepositoryOperationService
|
|
65
|
+
from gitpane.process.process_runner import ProcessRunner
|
|
66
|
+
from gitpane.ui.repository.repository_window import RepositoryWindow
|
|
67
|
+
from gitpane.worktree.staging import StagingSession
|
|
68
|
+
from gitpane.worktree.worktree_service import WorktreeService
|
|
69
|
+
from gitpane.worktree.worktree_snapshot import WorktreeKey
|
|
70
|
+
|
|
71
|
+
COMMIT_TIMING_LOG_PREFIX = "Commit timing: "
|
|
72
|
+
# The complete shape of a record, in writing order, the same for every event: a reader takes each field without looking
|
|
73
|
+
# for it, like the process lines written next to it.
|
|
74
|
+
COMMIT_TIMING_LOG_KEYS = ("time", "event", "operation_id", "elapsed_ms")
|
|
75
|
+
# The end is checked one turn of the event loop after what may end the commit: the slots of a published history, of a
|
|
76
|
+
# painted row or of a finished process still run their own work after it, and that work must be seen before the result
|
|
77
|
+
# is called displayed.
|
|
78
|
+
FINISH_CHECK_DELAY_MS = 0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class CommitTimingEvent(Enum):
|
|
82
|
+
"""The moments of a commit written in the log."""
|
|
83
|
+
|
|
84
|
+
CLICKED = "clicked"
|
|
85
|
+
SHOWN = "shown"
|
|
86
|
+
FAILED = "failed"
|
|
87
|
+
# The commit was not seen displayed before the time limit after its click, and is no longer followed.
|
|
88
|
+
ABANDONED = "abandoned"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class PendingWork(Enum):
|
|
92
|
+
"""A kind of repository work still running after a commit; the value names it in a measurement's failure message."""
|
|
93
|
+
|
|
94
|
+
REPOSITORY_REFRESH = "repository refresh or search"
|
|
95
|
+
PREPARATION = "preparation refresh or operation"
|
|
96
|
+
PROCESS_RUNNER = "process runner"
|
|
97
|
+
WORKTREE_TASK = "worktree continuation or file task"
|
|
98
|
+
RESERVATION = "repository reservation"
|
|
99
|
+
REPOSITORY_PARSING = "repository parsing or callback"
|
|
100
|
+
REFERENCE_CONTINUATION = "reference continuation"
|
|
101
|
+
SHARED_REFRESH = "shared refresh"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def format_commit_timing_line(event: CommitTimingEvent, operation_id: str, now: datetime, elapsed_ms: int | None) -> str:
|
|
105
|
+
"""Return the one-line record of a commit moment, which never holds the message, a path or a command.
|
|
106
|
+
|
|
107
|
+
``elapsed_ms`` is the time since the click, absent from the click's own line.
|
|
108
|
+
"""
|
|
109
|
+
value_by_key: dict[str, object] = {
|
|
110
|
+
"time": now.isoformat(timespec="milliseconds"),
|
|
111
|
+
"event": event.value,
|
|
112
|
+
"operation_id": operation_id,
|
|
113
|
+
"elapsed_ms": elapsed_ms,
|
|
114
|
+
}
|
|
115
|
+
# ASCII only, like a process line: a record stays one physical line whatever a reader treats as a line break.
|
|
116
|
+
return COMMIT_TIMING_LOG_PREFIX + json.dumps(value_by_key, ensure_ascii=True)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def commit_row_rectangle(window: RepositoryWindow, oid: str) -> QRect | None:
|
|
120
|
+
"""Return where the message of this commit's row sits in the history list, or nothing when the list has no such row."""
|
|
121
|
+
model = window.history_model
|
|
122
|
+
row = model.row_by_oid.get(oid)
|
|
123
|
+
if row is None:
|
|
124
|
+
return None
|
|
125
|
+
return window.commit_list.visualRect(model.index(row, MESSAGE_COLUMN))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def commit_row_current(window: RepositoryWindow, session: RepositorySession | None, oid: str | None, published_generation: int | None) -> bool:
|
|
129
|
+
"""Tell whether the window's complete current history, the one this publication made, holds this commit in its list.
|
|
130
|
+
|
|
131
|
+
The history must be complete and without error, the generation published equal to the session's and to the shown
|
|
132
|
+
history's, the list model built on that history and holding every one of its commits.
|
|
133
|
+
"""
|
|
134
|
+
snapshot = window.snapshot
|
|
135
|
+
if (session is None or snapshot is None or not snapshot.complete or snapshot.error or oid is None
|
|
136
|
+
or published_generation != session.generation or snapshot.generation != session.generation
|
|
137
|
+
or window.history_model.snapshot is not snapshot):
|
|
138
|
+
return False
|
|
139
|
+
return window.history_model.rowCount() == len(snapshot.ordered_oids) and oid in snapshot.ordered_oids
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def commit_row_in_view(window: RepositoryWindow, oid: str) -> bool:
|
|
143
|
+
"""Tell whether this commit's row crosses the visible part of the history list."""
|
|
144
|
+
rectangle = commit_row_rectangle(window, oid)
|
|
145
|
+
if rectangle is None:
|
|
146
|
+
return False
|
|
147
|
+
viewport = window.commit_list.viewport()
|
|
148
|
+
return viewport.isVisible() and not rectangle.isEmpty() and viewport.rect().intersects(rectangle)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def commit_row_visible(window: RepositoryWindow, session: RepositorySession | None, oid: str | None, published_generation: int | None) -> bool:
|
|
152
|
+
"""Tell whether the window shows this commit in its complete current history, in the visible part of its list."""
|
|
153
|
+
return commit_row_current(window, session, oid, published_generation) and commit_row_in_view(window, oid)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass
|
|
157
|
+
class CommitTiming:
|
|
158
|
+
"""One commit followed from its click until its result is displayed in the windows that manage its worktree.
|
|
159
|
+
|
|
160
|
+
Each followed window is known by its session id: the generation its history had at the click, the generation a
|
|
161
|
+
refresh published after it, the one whose paint is waiting for confirmation, and the one confirmed painted.
|
|
162
|
+
``limit_timer`` ends the timing with an "abandoned" line once the time limit since the click has passed.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
operation_id: str
|
|
166
|
+
key: WorktreeKey
|
|
167
|
+
common_dir: str
|
|
168
|
+
clicked_ns: int
|
|
169
|
+
initial_generation_by_session_id: dict[str, int]
|
|
170
|
+
viewport_by_session_id: dict[str, QWidget]
|
|
171
|
+
oid: str | None = None
|
|
172
|
+
published_generation_by_session_id: dict[str, int] = field(default_factory=dict)
|
|
173
|
+
confirming_generation_by_session_id: dict[str, int] = field(default_factory=dict)
|
|
174
|
+
painted_generation_by_session_id: dict[str, int] = field(default_factory=dict)
|
|
175
|
+
limit_timer: QTimer | None = None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class CommitTimingController(QObject):
|
|
179
|
+
"""Write the click and the outcome of every commit into the --log file, when there is one.
|
|
180
|
+
|
|
181
|
+
The outcome is the displayed result of a commit that succeeded, the failure of a commit refused, failed or whose
|
|
182
|
+
refresh failed, or the end of a commit not seen displayed within the time limit after its click. The end conditions
|
|
183
|
+
read here look at the work of the commit's repository only; the commit measurement, which looks at the work of the
|
|
184
|
+
whole application, checks that they agree with its own.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
def __init__(self, *, session_registry_controller: SessionRegistry, staging_session_registry_controller: StagingSessionRegistry, worktree_state_controller: WorktreeStateController,
|
|
188
|
+
history_read_controller: HistoryReadController, refresh_controller: RefreshController, reference_read_controller: ReferenceReadController, repository: GitRepository,
|
|
189
|
+
authentication: AuthenticationService, worktree_service: WorktreeService, repository_operation: RepositoryOperationService, diagnostic_log: DiagnosticLog | None,
|
|
190
|
+
parent: QObject | None = None) -> None:
|
|
191
|
+
"""Receive every state the end of a commit is read from; subscribe to nothing unless the log is open.
|
|
192
|
+
|
|
193
|
+
The condition is on the open file, like the process lines: without a log, no slot is added to the path every
|
|
194
|
+
process and every published history goes through.
|
|
195
|
+
"""
|
|
196
|
+
super().__init__(parent)
|
|
197
|
+
self.session_registry_controller = session_registry_controller
|
|
198
|
+
self.staging_session_registry_controller = staging_session_registry_controller
|
|
199
|
+
self.worktree_state_controller = worktree_state_controller
|
|
200
|
+
self.history_read_controller = history_read_controller
|
|
201
|
+
self.refresh_controller = refresh_controller
|
|
202
|
+
self.reference_read_controller = reference_read_controller
|
|
203
|
+
self.repository = repository
|
|
204
|
+
self.authentication = authentication
|
|
205
|
+
self.worktree_service = worktree_service
|
|
206
|
+
self.repository_operation = repository_operation
|
|
207
|
+
self.diagnostic_log = diagnostic_log
|
|
208
|
+
# The two runners GitPane starts Git with: the repository's, and the one of the operations that use an account.
|
|
209
|
+
self.runners: list[ProcessRunner] = [repository.runner, authentication.runner]
|
|
210
|
+
self.timing_by_operation_id: dict[str, CommitTiming] = {}
|
|
211
|
+
self.finish_check_timer = QTimer(self)
|
|
212
|
+
self.finish_check_timer.setSingleShot(True)
|
|
213
|
+
self.finish_check_timer.setInterval(FINISH_CHECK_DELAY_MS)
|
|
214
|
+
self.finish_check_timer.timeout.connect(self._check_finished)
|
|
215
|
+
if not self._log_open():
|
|
216
|
+
return
|
|
217
|
+
history_read_controller.history_published.connect(self._history_published)
|
|
218
|
+
history_read_controller.refresh_failed.connect(self._refresh_failed)
|
|
219
|
+
for runner in self.runners:
|
|
220
|
+
runner.finished.connect(self._work_finished)
|
|
221
|
+
# A file task, the last continuation of a read or a queued repository callback can end after the last process:
|
|
222
|
+
# their end is told by these.
|
|
223
|
+
worktree_service.idle.connect(self._work_finished)
|
|
224
|
+
repository_operation.idle.connect(self._work_finished)
|
|
225
|
+
repository.next_turn_work_done.connect(self._work_finished)
|
|
226
|
+
|
|
227
|
+
def _log_open(self) -> bool:
|
|
228
|
+
"""Tell whether a line written now reaches the file; a disk error closes it for good."""
|
|
229
|
+
return self.diagnostic_log is not None and self.diagnostic_log.stream is not None
|
|
230
|
+
|
|
231
|
+
def pending_work(self, common_dir: str) -> list[str]:
|
|
232
|
+
"""Name the work of this repository still running, without mistaking kept histories or generation maps for requests.
|
|
233
|
+
|
|
234
|
+
``common_dir`` is the canonical common Git folder of the repository. Its sessions and Prepare changes windows are
|
|
235
|
+
known by that folder. A process counts when it runs in one of the repository's folders, a worktree continuation
|
|
236
|
+
or file task or a reservation when one of its repositories or reserved folders is one of them, and repository
|
|
237
|
+
parsing, reference continuations and shared refreshes when they serve one of its sessions. Work whose repository
|
|
238
|
+
is not known here counts: a sequence the worktree service holds between two steps, a read with no session, a
|
|
239
|
+
session already forgotten. Work of another repository does not. One name is given per kind of running work, per
|
|
240
|
+
runner and per shared refresh, so the same name can come more than once.
|
|
241
|
+
"""
|
|
242
|
+
repository_dir_set = self._repository_dir_set(common_dir)
|
|
243
|
+
reasons: list[str] = []
|
|
244
|
+
for session in tuple(self.session_registry_controller.session_by_id.values()):
|
|
245
|
+
if session.is_closed or canonical_path(session.repository_context.common_git_dir) != common_dir:
|
|
246
|
+
continue
|
|
247
|
+
if session.refresh_active or session.refresh_queued is not None or session.search_pending:
|
|
248
|
+
reasons.append(PendingWork.REPOSITORY_REFRESH.value)
|
|
249
|
+
for staging in tuple(self.staging_session_registry_controller.staging_by_key.values()):
|
|
250
|
+
if canonical_path(staging.context.common_git_dir) != common_dir:
|
|
251
|
+
continue
|
|
252
|
+
if staging.refresh_active or staging.refresh_queued or staging.explicit_refresh or staging.probe_active or staging.busy:
|
|
253
|
+
reasons.append(PendingWork.PREPARATION.value)
|
|
254
|
+
for runner in self.runners:
|
|
255
|
+
requests = [*runner.pending_requests]
|
|
256
|
+
for active in tuple(runner.active_by_id.values()):
|
|
257
|
+
requests.append(active.request)
|
|
258
|
+
for request in requests:
|
|
259
|
+
if self._inside_repository(request.working_dir, repository_dir_set):
|
|
260
|
+
reasons.append(PendingWork.PROCESS_RUNNER.value)
|
|
261
|
+
break
|
|
262
|
+
if self._worktree_task_running(repository_dir_set):
|
|
263
|
+
reasons.append(PendingWork.WORKTREE_TASK.value)
|
|
264
|
+
for intent in tuple(self.repository_operation.active_by_id.values()):
|
|
265
|
+
if self._intent_in_repository(intent, repository_dir_set):
|
|
266
|
+
reasons.append(PendingWork.RESERVATION.value)
|
|
267
|
+
break
|
|
268
|
+
if self._repository_parsing_running(repository_dir_set):
|
|
269
|
+
reasons.append(PendingWork.REPOSITORY_PARSING.value)
|
|
270
|
+
for session_id in tuple(self.reference_read_controller.reference_continuation_by_session):
|
|
271
|
+
if self._session_in_repository(session_id, repository_dir_set):
|
|
272
|
+
reasons.append(PendingWork.REFERENCE_CONTINUATION.value)
|
|
273
|
+
break
|
|
274
|
+
for shared in tuple(self.refresh_controller.full_refresh_read_by_key.values()):
|
|
275
|
+
for session_id in tuple(shared.session_generation_by_id):
|
|
276
|
+
if self._session_in_repository(session_id, repository_dir_set):
|
|
277
|
+
reasons.append(PendingWork.SHARED_REFRESH.value)
|
|
278
|
+
break
|
|
279
|
+
return reasons
|
|
280
|
+
|
|
281
|
+
def _repository_dir_set(self, common_dir: str) -> set[str]:
|
|
282
|
+
"""Return the canonical folders of the repository: its common Git folder and the worktree of each of its open sessions and Prepare changes windows."""
|
|
283
|
+
repository_dir_set = {common_dir}
|
|
284
|
+
for session in tuple(self.session_registry_controller.session_by_id.values()):
|
|
285
|
+
if not session.is_closed and canonical_path(session.repository_context.common_git_dir) == common_dir:
|
|
286
|
+
repository_dir_set.add(canonical_path(session.repository_context.repository_dir))
|
|
287
|
+
for staging in tuple(self.staging_session_registry_controller.staging_by_key.values()):
|
|
288
|
+
if canonical_path(staging.context.common_git_dir) == common_dir:
|
|
289
|
+
repository_dir_set.add(canonical_path(staging.context.repository_dir))
|
|
290
|
+
return repository_dir_set
|
|
291
|
+
|
|
292
|
+
@staticmethod
|
|
293
|
+
def _inside_repository(folder: str, repository_dir_set: set[str]) -> bool:
|
|
294
|
+
"""Tell whether a folder is one of the repository's folders or inside one; a folder on another drive is outside."""
|
|
295
|
+
canonical_folder = canonical_path(folder)
|
|
296
|
+
for repository_dir in repository_dir_set:
|
|
297
|
+
try:
|
|
298
|
+
if os.path.commonpath((canonical_folder, repository_dir)) == repository_dir:
|
|
299
|
+
return True
|
|
300
|
+
except ValueError:
|
|
301
|
+
continue
|
|
302
|
+
return False
|
|
303
|
+
|
|
304
|
+
def _context_in_repository(self, context: RepositoryContext, repository_dir_set: set[str]) -> bool:
|
|
305
|
+
"""Tell whether a repository location is the repository's own, one of its linked worktrees, or one of its submodules."""
|
|
306
|
+
return self._inside_repository(context.common_git_dir, repository_dir_set) or self._inside_repository(context.repository_dir, repository_dir_set)
|
|
307
|
+
|
|
308
|
+
def _session_in_repository(self, session_id: str, repository_dir_set: set[str]) -> bool:
|
|
309
|
+
"""Tell whether work done for a session may be the repository's; a session no longer known counts, its repository being unknown."""
|
|
310
|
+
session = self.session_registry_controller.session_by_id.get(session_id)
|
|
311
|
+
if session is None:
|
|
312
|
+
return True
|
|
313
|
+
return self._context_in_repository(session.repository_context, repository_dir_set)
|
|
314
|
+
|
|
315
|
+
def _intent_in_repository(self, intent: OperationIntent, repository_dir_set: set[str]) -> bool:
|
|
316
|
+
"""Tell whether a reservation holds the repository, by its repository, the other repositories it reserves or its reserved folders."""
|
|
317
|
+
for context in (intent.context, *intent.reserved_contexts):
|
|
318
|
+
if self._context_in_repository(context, repository_dir_set):
|
|
319
|
+
return True
|
|
320
|
+
for reserved_dir in intent.reserved_repository_dirs:
|
|
321
|
+
if self._inside_repository(reserved_dir, repository_dir_set):
|
|
322
|
+
return True
|
|
323
|
+
return False
|
|
324
|
+
|
|
325
|
+
def _worktree_task_running(self, repository_dir_set: set[str]) -> bool:
|
|
326
|
+
"""Tell whether a worktree continuation or file task of the repository runs, or a sequence is held between two steps.
|
|
327
|
+
|
|
328
|
+
A sequence the service holds with no continuation or file task running is between two of its steps, where its
|
|
329
|
+
repository is not known: it counts.
|
|
330
|
+
"""
|
|
331
|
+
worktree = self.worktree_service
|
|
332
|
+
running_sequence_set: set[object] = set()
|
|
333
|
+
found = False
|
|
334
|
+
for sequence, _callback, context, *_rest in (*worktree.continuation_by_id.values(), *worktree.file_task_by_id.values()):
|
|
335
|
+
running_sequence_set.add(sequence)
|
|
336
|
+
if self._context_in_repository(context, repository_dir_set):
|
|
337
|
+
found = True
|
|
338
|
+
if found:
|
|
339
|
+
return True
|
|
340
|
+
for sequence in tuple(worktree.operation_id_by_sequence):
|
|
341
|
+
if sequence not in running_sequence_set:
|
|
342
|
+
return True
|
|
343
|
+
return False
|
|
344
|
+
|
|
345
|
+
def _repository_parsing_running(self, repository_dir_set: set[str]) -> bool:
|
|
346
|
+
"""Tell whether a read of the repository is parsed or waits for its callback, by the session its request was made for.
|
|
347
|
+
|
|
348
|
+
A request with no identity has no known session: it counts. A callback the repository queued for the next turn
|
|
349
|
+
of the event loop, such as a cached commit or diff about to be published, is held by no request: it is judged by
|
|
350
|
+
the session it was queued for, and counts when that session is unknown. Without it the result line could be
|
|
351
|
+
written one turn before the commit's details are shown; the one service serves every open window, so a callback
|
|
352
|
+
queued for a window of another repository does not count.
|
|
353
|
+
"""
|
|
354
|
+
repository = self.repository
|
|
355
|
+
for session_id in tuple(repository.next_turn_session_id_by_token.values()):
|
|
356
|
+
if session_id is None or self._session_in_repository(session_id, repository_dir_set):
|
|
357
|
+
return True
|
|
358
|
+
request_id_set = {*repository.stream_by_id, *repository.batch_read_by_request_id, *repository.callback_by_id, *repository.result_callback_by_id}
|
|
359
|
+
for request_id in request_id_set:
|
|
360
|
+
identity = repository.identity_by_id.get(request_id)
|
|
361
|
+
if identity is None or self._session_in_repository(identity.session_id, repository_dir_set):
|
|
362
|
+
return True
|
|
363
|
+
return False
|
|
364
|
+
|
|
365
|
+
def commit_clicked(self, staging: StagingSession, request: CommitRequest) -> None:
|
|
366
|
+
"""Start the clock of a commit Prepare changes has just sent, and watch the history of each window of its worktree.
|
|
367
|
+
|
|
368
|
+
Commit and Commit & push both come here. The end is checked once the click's own slots have run, in case the
|
|
369
|
+
request was already dropped by then.
|
|
370
|
+
"""
|
|
371
|
+
if not self._log_open():
|
|
372
|
+
return
|
|
373
|
+
clicked_ns = time.monotonic_ns()
|
|
374
|
+
initial_generation_by_session_id: dict[str, int] = {}
|
|
375
|
+
viewport_by_session_id: dict[str, QWidget] = {}
|
|
376
|
+
for session_id, window in self.worktree_state_controller.managed_windows_of_worktree(staging.key):
|
|
377
|
+
initial_generation_by_session_id[session_id] = self.session_registry_controller.session_by_id[session_id].generation
|
|
378
|
+
viewport = window.commit_list.viewport()
|
|
379
|
+
viewport.installEventFilter(self)
|
|
380
|
+
viewport_by_session_id[session_id] = viewport
|
|
381
|
+
timing = CommitTiming(request.operation_id, staging.key, canonical_path(staging.context.common_git_dir), clicked_ns, initial_generation_by_session_id, viewport_by_session_id)
|
|
382
|
+
timing.limit_timer = QTimer(self)
|
|
383
|
+
timing.limit_timer.setSingleShot(True)
|
|
384
|
+
timing.limit_timer.setInterval(COMMIT_TIMING_LIMIT_MS)
|
|
385
|
+
timing.limit_timer.timeout.connect(lambda: self._limit_reached(request.operation_id))
|
|
386
|
+
timing.limit_timer.start()
|
|
387
|
+
self.timing_by_operation_id[request.operation_id] = timing
|
|
388
|
+
self._write(CommitTimingEvent.CLICKED, request.operation_id, None)
|
|
389
|
+
self.finish_check_timer.start()
|
|
390
|
+
|
|
391
|
+
def commit_finished(self, outcome: CommitOutcome) -> None:
|
|
392
|
+
"""Keep the new commit of a followed click, so its row is looked for in the refreshed histories.
|
|
393
|
+
|
|
394
|
+
Any other result, a commit that succeeded with a warning included, ends the timing with a failure line: no
|
|
395
|
+
result of this click will be called displayed.
|
|
396
|
+
"""
|
|
397
|
+
timing = self.timing_by_operation_id.get(outcome.operation_id)
|
|
398
|
+
if timing is None:
|
|
399
|
+
return
|
|
400
|
+
if outcome.status != OperationStatus.SUCCESS or outcome.after is None or not outcome.after.snapshot.head_oid:
|
|
401
|
+
self._finish(timing, CommitTimingEvent.FAILED)
|
|
402
|
+
return
|
|
403
|
+
timing.oid = outcome.after.snapshot.head_oid
|
|
404
|
+
self.finish_check_timer.start()
|
|
405
|
+
|
|
406
|
+
def _refresh_failed(self, session_id: str, diagnostic: PreparedDiagnostic) -> None:
|
|
407
|
+
"""End with a failure line every followed commit of the repository whose window could not read its history again."""
|
|
408
|
+
session = self.session_registry_controller.session_by_id.get(session_id)
|
|
409
|
+
if session is None or not self.timing_by_operation_id:
|
|
410
|
+
return
|
|
411
|
+
common_dir = canonical_path(session.repository_context.common_git_dir)
|
|
412
|
+
for timing in tuple(self.timing_by_operation_id.values()):
|
|
413
|
+
if timing.common_dir == common_dir:
|
|
414
|
+
self._finish(timing, CommitTimingEvent.FAILED)
|
|
415
|
+
|
|
416
|
+
def _history_published(self, session_id: str, generation: int) -> None:
|
|
417
|
+
"""Take a history published after the click as the one to see painted, forgetting the paint of an older one."""
|
|
418
|
+
if not self.timing_by_operation_id:
|
|
419
|
+
return
|
|
420
|
+
session = self.session_registry_controller.session_by_id.get(session_id)
|
|
421
|
+
if session is None or generation != session.generation:
|
|
422
|
+
return
|
|
423
|
+
for timing in self.timing_by_operation_id.values():
|
|
424
|
+
initial_generation = timing.initial_generation_by_session_id.get(session_id)
|
|
425
|
+
if initial_generation is None or generation <= initial_generation:
|
|
426
|
+
continue
|
|
427
|
+
timing.published_generation_by_session_id[session_id] = generation
|
|
428
|
+
timing.confirming_generation_by_session_id.pop(session_id, None)
|
|
429
|
+
timing.painted_generation_by_session_id.pop(session_id, None)
|
|
430
|
+
self.finish_check_timer.start()
|
|
431
|
+
|
|
432
|
+
def _work_finished(self, *arguments: object) -> None:
|
|
433
|
+
"""Look at the end again once the slots of this finished work have run, when a commit is followed."""
|
|
434
|
+
if self.timing_by_operation_id:
|
|
435
|
+
self.finish_check_timer.start()
|
|
436
|
+
|
|
437
|
+
def eventFilter(self, watched: QObject, event: QEvent) -> bool:
|
|
438
|
+
"""Note that Qt is painting the new commit's row; the paint is confirmed after Qt has processed it.
|
|
439
|
+
|
|
440
|
+
Nothing is built or changed during the paint: only the generation to confirm is noted, and the event goes on.
|
|
441
|
+
"""
|
|
442
|
+
if event.type() != QEvent.Type.Paint or not self.timing_by_operation_id:
|
|
443
|
+
return False
|
|
444
|
+
for timing in self.timing_by_operation_id.values():
|
|
445
|
+
for session_id, viewport in timing.viewport_by_session_id.items():
|
|
446
|
+
if viewport is not watched:
|
|
447
|
+
continue
|
|
448
|
+
window = self.session_registry_controller.window_by_session.get(session_id)
|
|
449
|
+
session = self.session_registry_controller.session_by_id.get(session_id)
|
|
450
|
+
published_generation = timing.published_generation_by_session_id.get(session_id)
|
|
451
|
+
if window is None or not isValid(window) or not commit_row_visible(window, session, timing.oid, published_generation):
|
|
452
|
+
continue
|
|
453
|
+
rectangle = commit_row_rectangle(window, timing.oid)
|
|
454
|
+
if rectangle is None or not event.region().intersects(rectangle):
|
|
455
|
+
continue
|
|
456
|
+
timing.confirming_generation_by_session_id[session_id] = published_generation
|
|
457
|
+
self.finish_check_timer.start()
|
|
458
|
+
return False
|
|
459
|
+
|
|
460
|
+
def _confirm_paints(self, timing: CommitTiming) -> None:
|
|
461
|
+
"""Keep a noted paint only if the row it painted still shows the same published history."""
|
|
462
|
+
for session_id, generation in tuple(timing.confirming_generation_by_session_id.items()):
|
|
463
|
+
del timing.confirming_generation_by_session_id[session_id]
|
|
464
|
+
window = self.session_registry_controller.window_by_session.get(session_id)
|
|
465
|
+
session = self.session_registry_controller.session_by_id.get(session_id)
|
|
466
|
+
if (window is not None and isValid(window) and generation == timing.published_generation_by_session_id.get(session_id)
|
|
467
|
+
and commit_row_visible(window, session, timing.oid, generation)):
|
|
468
|
+
timing.painted_generation_by_session_id[session_id] = generation
|
|
469
|
+
|
|
470
|
+
def _displayed_everywhere(self, timing: CommitTiming) -> bool:
|
|
471
|
+
"""Tell whether every followed window the user can see shows the new commit in its last published history.
|
|
472
|
+
|
|
473
|
+
A window closed, hidden or minimized since the click shows nothing to wait for. A row outside the visible part
|
|
474
|
+
of the list has no paint to wait for either: the published history that holds it is what the user sees by
|
|
475
|
+
scrolling to it. A row in view must have been painted.
|
|
476
|
+
"""
|
|
477
|
+
for session_id in timing.initial_generation_by_session_id:
|
|
478
|
+
session = self.session_registry_controller.session_by_id.get(session_id)
|
|
479
|
+
window = self.session_registry_controller.window_by_session.get(session_id)
|
|
480
|
+
if session is None or session.is_closed or window is None or not isValid(window) or window.is_closed or not window.isVisible() or window.isMinimized():
|
|
481
|
+
continue
|
|
482
|
+
published_generation = timing.published_generation_by_session_id.get(session_id)
|
|
483
|
+
if published_generation is None or not commit_row_current(window, session, timing.oid, published_generation):
|
|
484
|
+
return False
|
|
485
|
+
if commit_row_in_view(window, timing.oid) and timing.painted_generation_by_session_id.get(session_id) != published_generation:
|
|
486
|
+
return False
|
|
487
|
+
return True
|
|
488
|
+
|
|
489
|
+
def _check_finished(self) -> None:
|
|
490
|
+
"""Write the result line of every followed commit now displayed with no work of its repository left.
|
|
491
|
+
|
|
492
|
+
A followed commit without a new commit whose request was dropped ends with a failure line instead.
|
|
493
|
+
"""
|
|
494
|
+
if not self._log_open():
|
|
495
|
+
self._forget_every_timing()
|
|
496
|
+
return
|
|
497
|
+
for timing in tuple(self.timing_by_operation_id.values()):
|
|
498
|
+
if timing.oid is None and self._request_dropped(timing):
|
|
499
|
+
self._finish(timing, CommitTimingEvent.FAILED)
|
|
500
|
+
continue
|
|
501
|
+
self._confirm_paints(timing)
|
|
502
|
+
if timing.oid is None or not self._displayed_everywhere(timing) or self.pending_work(timing.common_dir):
|
|
503
|
+
continue
|
|
504
|
+
self._finish(timing, CommitTimingEvent.SHOWN)
|
|
505
|
+
|
|
506
|
+
def _request_dropped(self, timing: CommitTiming) -> bool:
|
|
507
|
+
"""Tell whether the commit's request was dropped before the commit service gave a result for it.
|
|
508
|
+
|
|
509
|
+
Prepare changes clears its active request and gives the result in the same call, with no turn of the event
|
|
510
|
+
loop between them: a request cleared without a result cannot be a commit about to finish. That is a Commit &
|
|
511
|
+
push refused while it read where to push, or a Prepare changes session that no longer exists.
|
|
512
|
+
"""
|
|
513
|
+
staging = self.staging_session_registry_controller.staging_by_key.get(timing.key)
|
|
514
|
+
if staging is None:
|
|
515
|
+
return True
|
|
516
|
+
request = staging.active_commit_request
|
|
517
|
+
return request is None or request.operation_id != timing.operation_id
|
|
518
|
+
|
|
519
|
+
def _limit_reached(self, operation_id: str) -> None:
|
|
520
|
+
"""End with an "abandoned" line a commit still followed when the time limit since its click has passed."""
|
|
521
|
+
timing = self.timing_by_operation_id.get(operation_id)
|
|
522
|
+
if timing is None:
|
|
523
|
+
return
|
|
524
|
+
elapsed_ms = (time.monotonic_ns() - timing.clicked_ns) // NANOSECONDS_PER_MILLISECOND
|
|
525
|
+
if elapsed_ms < COMMIT_TIMING_LIMIT_MS and timing.limit_timer is not None:
|
|
526
|
+
# Qt lets a coarse timer fire up to a twentieth of its interval early: wait for the rest of the limit.
|
|
527
|
+
timing.limit_timer.start(COMMIT_TIMING_LIMIT_MS - elapsed_ms)
|
|
528
|
+
return
|
|
529
|
+
self._finish(timing, CommitTimingEvent.ABANDONED)
|
|
530
|
+
|
|
531
|
+
def _finish(self, timing: CommitTiming, event: CommitTimingEvent) -> None:
|
|
532
|
+
"""Write the last line of a followed commit, with the time since its click, then stop following it."""
|
|
533
|
+
elapsed_ms = (time.monotonic_ns() - timing.clicked_ns) // NANOSECONDS_PER_MILLISECOND
|
|
534
|
+
self._forget(timing)
|
|
535
|
+
self._write(event, timing.operation_id, elapsed_ms)
|
|
536
|
+
|
|
537
|
+
def _forget(self, timing: CommitTiming) -> None:
|
|
538
|
+
"""Stop following a commit: its time limit is stopped and released, and the filter taken off every viewport no other followed commit watches."""
|
|
539
|
+
self.timing_by_operation_id.pop(timing.operation_id, None)
|
|
540
|
+
if timing.limit_timer is not None:
|
|
541
|
+
timing.limit_timer.stop()
|
|
542
|
+
timing.limit_timer.deleteLater()
|
|
543
|
+
timing.limit_timer = None
|
|
544
|
+
for session_id, viewport in timing.viewport_by_session_id.items():
|
|
545
|
+
if not isValid(viewport):
|
|
546
|
+
continue
|
|
547
|
+
watched_elsewhere = False
|
|
548
|
+
for other in self.timing_by_operation_id.values():
|
|
549
|
+
if other.viewport_by_session_id.get(session_id) is viewport:
|
|
550
|
+
watched_elsewhere = True
|
|
551
|
+
if not watched_elsewhere:
|
|
552
|
+
viewport.removeEventFilter(self)
|
|
553
|
+
|
|
554
|
+
def _forget_every_timing(self) -> None:
|
|
555
|
+
"""Stop following every commit once the log can take no more lines."""
|
|
556
|
+
for timing in tuple(self.timing_by_operation_id.values()):
|
|
557
|
+
self._forget(timing)
|
|
558
|
+
|
|
559
|
+
def _write(self, event: CommitTimingEvent, operation_id: str, elapsed_ms: int | None) -> None:
|
|
560
|
+
"""Write one line; a disk error that closes the log ends every timing without raising."""
|
|
561
|
+
if not self._log_open():
|
|
562
|
+
self._forget_every_timing()
|
|
563
|
+
return
|
|
564
|
+
self.diagnostic_log.write_line(format_commit_timing_line(event, operation_id, local_now(), elapsed_ms))
|
|
565
|
+
if not self._log_open():
|
|
566
|
+
self._forget_every_timing()
|