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,254 @@
|
|
|
1
|
+
"""Detect a change in the files a queued write was checked against, from their metadata, never their content.
|
|
2
|
+
|
|
3
|
+
Every native write the application queues is fully checked on a worker, then waits for the launcher. Its launch
|
|
4
|
+
check runs on the Qt thread at the moment Git starts, so it must stay cheap: it reads again the metadata of exactly
|
|
5
|
+
the paths the worker signed and refuses the launch on the smallest difference. The commit, rebase, stash and replay
|
|
6
|
+
commands all share this one definition, so they detect the same changes.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import stat
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from gitpane.commit.commit_metadata import read_path_metadata
|
|
17
|
+
from gitpane.system.path_identity import canonical_path
|
|
18
|
+
from gitpane.worktree.worktree_file import stat_identity
|
|
19
|
+
from gitpane.worktree.worktree_snapshot import read_index
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from collections.abc import Callable
|
|
23
|
+
|
|
24
|
+
from gitpane.history.repository_value import RepositoryContext
|
|
25
|
+
|
|
26
|
+
# Git names the Git directory of a worktree through this entry at its root: a directory for the main worktree, a
|
|
27
|
+
# file pointing at the private directory for a linked one.
|
|
28
|
+
WORKTREE_GIT_ENTRY_BASENAME = ".git"
|
|
29
|
+
# A linked worktree's private Git directory names the shared one through this file.
|
|
30
|
+
LINKED_COMMON_DIR_BASENAME = "commondir"
|
|
31
|
+
PATH_MOVED_CAUSE = "A checked file changed while its launch signature was taken. Refresh before writing."
|
|
32
|
+
SCOPE_MISMATCH_CAUSE = "Two launch signatures of different scope cannot be compared."
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class LaunchSignature:
|
|
37
|
+
"""The metadata, without any content, of everything a queued command was checked against just before it waits.
|
|
38
|
+
|
|
39
|
+
Files and watched folders keep their identity, change time, link count and redirection tag, so a same-size
|
|
40
|
+
rewrite whose modification time was put back, an added hard link or a file replaced by a reparse point all still
|
|
41
|
+
differ; a watched folder changes when an entry appears in it. Containing folders keep only their identity, type,
|
|
42
|
+
redirection and resolved path, so unrelated activity next to a checked file changes nothing while replacing or
|
|
43
|
+
redirecting the folder does. Each entry keeps its path, and absence is recorded as None, so an absent file, an
|
|
44
|
+
empty file and two lists of different scope never compare equal. It proves nothing and authorizes nothing by itself.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
file_signatures: tuple[tuple[str, tuple[int, ...] | None], ...]
|
|
48
|
+
directory_signatures: tuple[tuple[str, tuple[int, ...] | None], ...]
|
|
49
|
+
parent_identities: tuple[tuple[str, tuple[int | str, ...] | None], ...]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def launch_path_metadata(filename: str) -> tuple[int, ...] | None:
|
|
53
|
+
"""Read identity, type and permissions, change time, links and redirection of one path, refusing a path that moves.
|
|
54
|
+
|
|
55
|
+
The identity reader already compares the path before and after its change time; the last lstat adds the link count
|
|
56
|
+
and the redirection tag only if it still shows that same identity. A same-size write whose modification time was
|
|
57
|
+
put back still changes the change time, so it is seen here without reading a byte of the file.
|
|
58
|
+
"""
|
|
59
|
+
metadata = read_path_metadata(filename)
|
|
60
|
+
try:
|
|
61
|
+
current = os.lstat(filename)
|
|
62
|
+
except FileNotFoundError:
|
|
63
|
+
current = None
|
|
64
|
+
if metadata is None and current is None:
|
|
65
|
+
return None
|
|
66
|
+
if metadata is None or current is None:
|
|
67
|
+
raise ValueError(PATH_MOVED_CAUSE)
|
|
68
|
+
identity = stat_identity(current)
|
|
69
|
+
if metadata[:len(identity)] != identity:
|
|
70
|
+
raise ValueError(PATH_MOVED_CAUSE)
|
|
71
|
+
return (*metadata, current.st_nlink, getattr(current, "st_reparse_tag", 0))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def parent_identity(directory: str) -> tuple[int | str, ...] | None:
|
|
75
|
+
"""Identify a containing folder by device, inode, type, redirection tag and resolved path, never by its activity.
|
|
76
|
+
|
|
77
|
+
Activity in a shared parent, such as another file appearing next to a checked one, leaves this identity alone;
|
|
78
|
+
replacing or redirecting the folder itself, or one of its own parents, does not.
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
metadata = os.lstat(directory)
|
|
82
|
+
except FileNotFoundError:
|
|
83
|
+
return None
|
|
84
|
+
return (metadata.st_dev, metadata.st_ino, stat.S_IFMT(metadata.st_mode), getattr(metadata, "st_reparse_tag", 0), canonical_path(directory))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def sign_launch_paths(filenames: list[str] | tuple[str, ...], watched_dirs: list[str] | tuple[str, ...], parent_dirs: list[str] | tuple[str, ...]) -> LaunchSignature:
|
|
88
|
+
"""Take the metadata of the given paths in sorted order, so the result never depends on how the folders were listed.
|
|
89
|
+
|
|
90
|
+
Watched folders are compared on their own metadata, so an entry created in them changes the signature even when it
|
|
91
|
+
was not yet one of the listed files. Any read error is propagated: an unreadable file is never taken for an absent one.
|
|
92
|
+
"""
|
|
93
|
+
file_signatures = []
|
|
94
|
+
for filename in sorted(set(filenames)):
|
|
95
|
+
file_signatures.append((filename, launch_path_metadata(filename)))
|
|
96
|
+
directory_signatures = []
|
|
97
|
+
for directory in sorted(set(watched_dirs)):
|
|
98
|
+
directory_signatures.append((directory, launch_path_metadata(directory)))
|
|
99
|
+
parent_identities = []
|
|
100
|
+
for directory in sorted(set(parent_dirs)):
|
|
101
|
+
parent_identities.append((directory, parent_identity(directory)))
|
|
102
|
+
return LaunchSignature(tuple(file_signatures), tuple(directory_signatures), tuple(parent_identities))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def signed_paths(signature: LaunchSignature) -> tuple[list[str], list[str], list[str]]:
|
|
106
|
+
"""List again exactly the files, watched folders and containing folders a signature covers, so a later look never walks a folder."""
|
|
107
|
+
filenames = []
|
|
108
|
+
for filename, _value in signature.file_signatures:
|
|
109
|
+
filenames.append(filename)
|
|
110
|
+
watched_dirs = []
|
|
111
|
+
for directory, _value in signature.directory_signatures:
|
|
112
|
+
watched_dirs.append(directory)
|
|
113
|
+
parent_dirs = []
|
|
114
|
+
for directory, _value in signature.parent_identities:
|
|
115
|
+
parent_dirs.append(directory)
|
|
116
|
+
return filenames, watched_dirs, parent_dirs
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def changed_launch_paths(before: LaunchSignature, after: LaunchSignature) -> tuple[str, ...]:
|
|
120
|
+
"""Name every path whose metadata differs between two signatures of the same scope.
|
|
121
|
+
|
|
122
|
+
Two signatures of different scope differ everywhere by construction, so their comparison is refused outright.
|
|
123
|
+
"""
|
|
124
|
+
changed_paths = []
|
|
125
|
+
for before_entries, after_entries in ((before.file_signatures, after.file_signatures), (before.directory_signatures, after.directory_signatures),
|
|
126
|
+
(before.parent_identities, after.parent_identities)):
|
|
127
|
+
before_paths = []
|
|
128
|
+
for path, _value in before_entries:
|
|
129
|
+
before_paths.append(path)
|
|
130
|
+
after_paths = []
|
|
131
|
+
for path, _value in after_entries:
|
|
132
|
+
after_paths.append(path)
|
|
133
|
+
if before_paths != after_paths:
|
|
134
|
+
raise ValueError(SCOPE_MISMATCH_CAUSE)
|
|
135
|
+
for (path, before_value), (_path, after_value) in zip(before_entries, after_entries, strict=True):
|
|
136
|
+
if before_value != after_value:
|
|
137
|
+
changed_paths.append(path)
|
|
138
|
+
return tuple(changed_paths)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def exclude_covered_launch_paths(signature: LaunchSignature, covered: LaunchSignature) -> LaunchSignature:
|
|
142
|
+
"""Keep, from a signature, only the entries whose path another signature does not list, with their earlier values.
|
|
143
|
+
|
|
144
|
+
A launch check compares both signatures; a path both list is left to `covered`, which holds the most recent
|
|
145
|
+
accepted value, such as a message the command itself rewrote after the first signature was taken.
|
|
146
|
+
"""
|
|
147
|
+
covered_path_set = set()
|
|
148
|
+
for entries in (covered.file_signatures, covered.directory_signatures, covered.parent_identities):
|
|
149
|
+
for path, _value in entries:
|
|
150
|
+
covered_path_set.add(path)
|
|
151
|
+
kept_entries_by_kind = {}
|
|
152
|
+
for kind, entries in (("file", signature.file_signatures), ("directory", signature.directory_signatures), ("parent", signature.parent_identities)):
|
|
153
|
+
kept_entries = []
|
|
154
|
+
for path, value in entries:
|
|
155
|
+
if path not in covered_path_set:
|
|
156
|
+
kept_entries.append((path, value))
|
|
157
|
+
kept_entries_by_kind[kind] = tuple(kept_entries)
|
|
158
|
+
return LaunchSignature(kept_entries_by_kind["file"], kept_entries_by_kind["directory"], kept_entries_by_kind["parent"])
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def verify_launch_signature(signature: LaunchSignature, changed_cause: str, unreadable_cause: str | None = None) -> None:
|
|
162
|
+
"""Take the same metadata again and refuse the launch on the smallest difference or on any read error.
|
|
163
|
+
|
|
164
|
+
This check opens no content and walks no folder: it reads again exactly the paths the signature lists, so it is
|
|
165
|
+
cheap enough for the moment a queued command is handed to Git. An unreadable path is refused with
|
|
166
|
+
`unreadable_cause` when the caller names one, with `changed_cause` otherwise.
|
|
167
|
+
"""
|
|
168
|
+
try:
|
|
169
|
+
current = sign_launch_paths(*signed_paths(signature))
|
|
170
|
+
except OSError as error:
|
|
171
|
+
raise ValueError(unreadable_cause or changed_cause) from error
|
|
172
|
+
if changed_launch_paths(signature, current):
|
|
173
|
+
raise ValueError(changed_cause)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@dataclass(frozen=True)
|
|
177
|
+
class WriteLaunchSignature:
|
|
178
|
+
"""What one native write command was checked against, from its first look at the repository until Git starts.
|
|
179
|
+
|
|
180
|
+
``reference_state`` is the complete reference and configuration read taken before the Git checks, and
|
|
181
|
+
``index_content`` the index the command was checked with, when it depends on one. ``metadata`` holds only metadata:
|
|
182
|
+
the files the reference inventory lists (and that index), the files that name this worktree's Git directories, the
|
|
183
|
+
extra files and folders the caller watches, the folders whose new entries change references, and the identity of
|
|
184
|
+
the folders that contain them. It lets the launch check tell a change from metadata alone; nothing here authorizes
|
|
185
|
+
a command by itself.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
reference_state: tuple
|
|
189
|
+
index_filename: str | None
|
|
190
|
+
index_content: bytes | None
|
|
191
|
+
metadata: LaunchSignature
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def capture_write_launch_signature(context: RepositoryContext, changed_cause: str, read_references: Callable[[RepositoryContext], tuple],
|
|
195
|
+
read_reference_paths: Callable[[RepositoryContext], tuple[list[str], list[str]]], index_filename: str | None = None,
|
|
196
|
+
index_content: bytes | None = None, *, extra_filenames: list[str] | tuple[str, ...] = (),
|
|
197
|
+
extra_watched_dirs: list[str] | tuple[str, ...] = ()) -> WriteLaunchSignature:
|
|
198
|
+
"""Take the first look a native write command relies on, before any of its Git checks: the complete reference read between two identical metadata looks.
|
|
199
|
+
|
|
200
|
+
Runs on a worker. The paths come from the one reference inventory, the worktree location files and the extra files
|
|
201
|
+
and folders the caller names (a replay watches the pause files of its worktree); the Git folders and the worktree
|
|
202
|
+
folder are compared by identity only, so unrelated activity next to a checked file changes nothing. The caller hands
|
|
203
|
+
in the reference reader and its inventory: the reference module reaches this one again through the modules it
|
|
204
|
+
imports, so this module cannot import it.
|
|
205
|
+
"""
|
|
206
|
+
reference_filenames, reference_dirs = read_reference_paths(context)
|
|
207
|
+
location_filenames, location_parent_dirs = worktree_location_paths(context)
|
|
208
|
+
filenames = [*reference_filenames, *location_filenames, *extra_filenames]
|
|
209
|
+
watched_dirs = [*reference_dirs, *extra_watched_dirs]
|
|
210
|
+
parent_dirs = list(location_parent_dirs)
|
|
211
|
+
if index_filename is not None:
|
|
212
|
+
filenames.append(index_filename)
|
|
213
|
+
parent_dirs.append(os.path.dirname(index_filename))
|
|
214
|
+
before = sign_launch_paths(filenames, watched_dirs, parent_dirs)
|
|
215
|
+
reference_state = read_references(context)
|
|
216
|
+
after = sign_launch_paths(filenames, watched_dirs, parent_dirs)
|
|
217
|
+
if changed_launch_paths(before, after):
|
|
218
|
+
raise ValueError(changed_cause)
|
|
219
|
+
return WriteLaunchSignature(reference_state, index_filename, index_content, after)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def prepare_write_launch_signature(context: RepositoryContext, baseline: WriteLaunchSignature, changed_cause: str,
|
|
223
|
+
read_references: Callable[[RepositoryContext], tuple]) -> WriteLaunchSignature:
|
|
224
|
+
"""Check, just before the native write command is handed to the launcher, that nothing moved since the first look.
|
|
225
|
+
|
|
226
|
+
Runs on a worker. The reference contents are compared with the first look, read by the same reader, and the index
|
|
227
|
+
with the one the command was checked with, between two identical metadata looks that must also match the first
|
|
228
|
+
one. A change is refused, never adopted: the answer is the very signature the launch check compares with.
|
|
229
|
+
"""
|
|
230
|
+
verify_launch_signature(baseline.metadata, changed_cause)
|
|
231
|
+
if read_references(context) != baseline.reference_state:
|
|
232
|
+
raise ValueError(changed_cause)
|
|
233
|
+
if baseline.index_filename is not None and read_index(baseline.index_filename) != baseline.index_content:
|
|
234
|
+
raise ValueError(changed_cause)
|
|
235
|
+
verify_launch_signature(baseline.metadata, changed_cause)
|
|
236
|
+
return baseline
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def worktree_location_paths(context: RepositoryContext) -> tuple[list[str], list[str]]:
|
|
240
|
+
"""Name the files and containing folders that tell Git where this worktree's Git directories are.
|
|
241
|
+
|
|
242
|
+
Files: the pointer to the shared Git directory a linked worktree keeps, and the `.git` pointer file at the root of
|
|
243
|
+
a linked worktree. Containing folders: both Git directories, the worktree root and its `.git` entry. A worktree
|
|
244
|
+
redirected elsewhere while a command waits therefore changes the signature even when every reference is unchanged.
|
|
245
|
+
"""
|
|
246
|
+
filenames = [os.path.join(context.git_dir, LINKED_COMMON_DIR_BASENAME)]
|
|
247
|
+
parent_dirs = [context.git_dir, context.common_git_dir]
|
|
248
|
+
if context.worktree_dir is None:
|
|
249
|
+
return filenames, parent_dirs
|
|
250
|
+
git_entry = os.path.join(context.worktree_dir, WORKTREE_GIT_ENTRY_BASENAME)
|
|
251
|
+
parent_dirs.extend((context.worktree_dir, git_entry))
|
|
252
|
+
if os.path.isfile(git_entry):
|
|
253
|
+
filenames.append(git_entry)
|
|
254
|
+
return filenames, parent_dirs
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Name the statuses an operation reports, from a wait to its end.
|
|
2
|
+
|
|
3
|
+
An operation reports the end of its work, or of one of its steps, with one of these members, and a wait still under way
|
|
4
|
+
with PENDING. A process end becomes an operation end only through ``operation_status``, which keeps the word the
|
|
5
|
+
process wrote, so the console keeps its text.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Self
|
|
12
|
+
|
|
13
|
+
from gitpane.process.process_runner import ProcessStatus
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OperationStatus(Enum):
|
|
17
|
+
"""How an operation, or one of its steps, ended, or that it still waits; the value is the word its log and its JSON write.
|
|
18
|
+
|
|
19
|
+
``end_word`` is the word that ends the readable line of the operation in its console, such as ``done`` for a
|
|
20
|
+
success. A status without a word of its own keeps its value, so it stays visible rather than hidden.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
SUCCESS = ("success", "done")
|
|
24
|
+
"""The operation reached its goal."""
|
|
25
|
+
FAILED = ("failed", "failed")
|
|
26
|
+
"""The operation ran and did not reach its goal."""
|
|
27
|
+
REFUSED = ("refused", "not started")
|
|
28
|
+
"""GitPane refused the operation before launching Git, such as a checkout into a folder that no longer exists."""
|
|
29
|
+
PARTIAL = ("partial", "partially done")
|
|
30
|
+
"""The operation did part of its work, such as a Stash pop that applied the stash but stopped on conflicts.
|
|
31
|
+
|
|
32
|
+
An account exchange uses it too: the identity was accepted but the secure store did not keep the secret, or a secret
|
|
33
|
+
was erased but the saved list could not be written.
|
|
34
|
+
"""
|
|
35
|
+
UNKNOWN = ("unknown", "outcome unknown")
|
|
36
|
+
"""The effect of the operation could not be observed, such as a Push whose server could not be read again after sending."""
|
|
37
|
+
PAUSED = ("paused", "paused")
|
|
38
|
+
"""The operation stopped to wait for the user, such as a rebase, a cherry-pick or a revert stopped on a conflict."""
|
|
39
|
+
ABORTED = ("aborted", "aborted")
|
|
40
|
+
"""The result of a requested native Abort, reported only once the abandonment has been verified."""
|
|
41
|
+
CANCELLED = ("cancelled", "cancelled")
|
|
42
|
+
"""The user stopped the operation."""
|
|
43
|
+
TIMEOUT = ("timeout", "timeout")
|
|
44
|
+
"""The command of the operation was stopped because it ran past its time limit."""
|
|
45
|
+
IDLE_TIMEOUT = ("idle_timeout", "idle_timeout")
|
|
46
|
+
"""The command of the operation was stopped because it wrote nothing for too long."""
|
|
47
|
+
VOLUME = ("volume", "volume")
|
|
48
|
+
"""The command of the operation was stopped because it wrote more than its output limit."""
|
|
49
|
+
START_FAILED = ("start_failed", "start_failed")
|
|
50
|
+
"""The command of the operation could not be launched."""
|
|
51
|
+
QUEUE_FULL = ("queue_full", "queue_full")
|
|
52
|
+
"""The command of the operation was refused because too many were already waiting; it never ran."""
|
|
53
|
+
NOT_STARTED = ("not-started", "not-started")
|
|
54
|
+
"""A step that never began, because an earlier step of the same operation did not let it.
|
|
55
|
+
|
|
56
|
+
The steps of a checkout, a branch or a tag change write the same meaning with a space, ``STEP_NOT_STARTED``; a
|
|
57
|
+
check for a step that never began must look for both members.
|
|
58
|
+
"""
|
|
59
|
+
SIGN_IN_TIMEOUT = ("timed_out", "timed_out")
|
|
60
|
+
"""A sign-in in the browser that was not finished in time."""
|
|
61
|
+
STEP_NOT_STARTED = ("not started", "not started")
|
|
62
|
+
"""A step of a branch, tag or checkout change that never began, written with a space as its console shows it.
|
|
63
|
+
|
|
64
|
+
The steps of a transport write the same end as ``NOT_STARTED``; both members are kept so no console changes.
|
|
65
|
+
"""
|
|
66
|
+
NO_CHANGE = ("no_change", "no_change")
|
|
67
|
+
"""A stash was asked for while nothing had changed, so Git created none."""
|
|
68
|
+
PENDING = ("pending", "pending")
|
|
69
|
+
"""An operation that still waits for the answer of the system, reported before its end, such as a move to the Trash."""
|
|
70
|
+
|
|
71
|
+
def __new__(cls, value: str, end_word: str) -> Self:
|
|
72
|
+
"""Keep the status word as the value, with the word that ends the operation's line in its console."""
|
|
73
|
+
member = object.__new__(cls)
|
|
74
|
+
member._value_ = value
|
|
75
|
+
member.end_word = end_word
|
|
76
|
+
return member
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def operation_status(process_status: ProcessStatus) -> OperationStatus:
|
|
80
|
+
"""Return the operation end that reports a process end, with the same word, so the console keeps its text.
|
|
81
|
+
|
|
82
|
+
This is the only place where a process status becomes an operation status. A process still running has not ended, so
|
|
83
|
+
it has no operation status and is refused with ``ValueError``.
|
|
84
|
+
"""
|
|
85
|
+
if process_status == ProcessStatus.RUNNING:
|
|
86
|
+
raise ValueError("A running process has not ended")
|
|
87
|
+
return OperationStatus(process_status.value)
|